1*151251b5Slogin# 与“等待”相关的api(C语言) 2*151251b5Slogin 3*151251b5Slogin:::{warning} 4*151251b5Slogin 5*151251b5Slogin随着内核的发展,我们将会逐步将C语言的等待机制替换为Rust语言的等待机制。在这个过程中,我们将会同时保留C语言和Rust语言的等待机制,以便于我们在开发过程中进行对比。 6*151251b5Slogin待时机成熟,我们将会逐步将C语言的等待机制移除。 7*151251b5Slogin::: 8*151251b5Slogin 9*151251b5Slogin  如果几个进程需要等待某个事件发生,才能被运行,那么就需要一种“等待”的机制,以实现进程同步。 10*151251b5Slogin 11*151251b5Slogin## 一. wait_queue等待队列 12*151251b5Slogin 13*151251b5Slogin  wait_queue是一种进程同步机制,中文名为“等待队列”。它可以将当前进程挂起,并在时机成熟时,由另一个进程唤醒他们。 14*151251b5Slogin 15*151251b5Slogin  当您需要等待一个事件完成时,使用wait_queue机制能减少进程同步的开销。相比于滥用自旋锁以及信号量,或者是循环使用usleep(1000)这样的函数来完成同步,wait_queue是一个高效的解决方案。 16*151251b5Slogin 17*151251b5Slogin:::{warning} 18*151251b5Slogin`wait_queue.h`中的等待队列的实现并没有把队列头独立出来,同时没有考虑为等待队列加锁。所以在后来的开发中加入了`wait_queue_head_t`的队列头实现,实质上就是链表+自旋锁。它与`wait_queue.h`中的队列`wait_queue_node_t`是兼容的,当你使用`struct wait_queue_head`作为队列头时,你同样可以使用等待队列添加节点的函数。 19*151251b5Slogin::: 20*151251b5Slogin 21*151251b5Slogin### 简单用法 22*151251b5Slogin 23*151251b5Slogin  等待队列的使用方法主要分为以下几部分: 24*151251b5Slogin 25*151251b5Slogin- 创建并初始化一个等待队列 26*151251b5Slogin- 使用`wait_queue_sleep_on_`系列的函数,将当前进程挂起。晚挂起的进程将排在队列的尾部。 27*151251b5Slogin- 通过`wait_queue_wakeup()`函数,依次唤醒在等待队列上的进程,将其加入调度队列 28*151251b5Slogin 29*151251b5Slogin  要使用wait_queue,您需要`#include<common/wait_queue.h>`,并创建一个`wait_queue_node_t`类型的变量,作为等待队列的头部。这个结构体只包含两个成员变量: 30*151251b5Slogin 31*151251b5Slogin```c 32*151251b5Slogintypedef struct 33*151251b5Slogin{ 34*151251b5Slogin struct List wait_list; 35*151251b5Slogin struct process_control_block *pcb; 36*151251b5Slogin} wait_queue_node_t; 37*151251b5Slogin``` 38*151251b5Slogin 39*151251b5Slogin  对于等待队列,这里有一个好的命名方法: 40*151251b5Slogin 41*151251b5Slogin```c 42*151251b5Sloginwait_queue_node_t wq_keyboard_interrupt_received; 43*151251b5Slogin``` 44*151251b5Slogin 45*151251b5Slogin  这样的命名方式能增加代码的可读性,更容易让人明白这里到底在等待什么。 46*151251b5Slogin 47*151251b5Slogin### 初始化等待队列 48*151251b5Slogin 49*151251b5Slogin  函数`wait_queue_init(wait_queue_node_t *wait_queue, struct process_control_block *pcb)`提供了初始化wait_queue结点的功能。 50*151251b5Slogin 51*151251b5Slogin  当您初始化队列头部时,您仅需要将wait_queue首部的结点指针传入,第二个参数请设置为NULL 52*151251b5Slogin 53*151251b5Slogin 54*151251b5Slogin 55*151251b5Slogin### 将结点插入等待队列 56*151251b5Slogin 57*151251b5Slogin  您可以使用以下函数,将当前进程挂起,并插入到指定的等待队列。这些函数大体功能相同,只是在一些细节上有所不同。 58*151251b5Slogin 59*151251b5Slogin| 函数名 | 解释 | 60*151251b5Slogin| ----------------------------------- | ----------------------------------------------------------- | 61*151251b5Slogin| wait_queue_sleep_on() | 将当前进程挂起,并设置挂起状态为PROC_UNINTERRUPTIBLE | 62*151251b5Slogin| wait_queue_sleep_on_unlock() | 将当前进程挂起,并设置挂起状态为PROC_UNINTERRUPTIBLE。待当前进程被插入等待队列后,解锁给定的自旋锁 | 63*151251b5Slogin| wait_queue_sleep_on_interriptible() | 将当前进程挂起,并设置挂起状态为PROC_INTERRUPTIBLE | 64*151251b5Slogin 65*151251b5Slogin### 从等待队列唤醒一个进程 66*151251b5Slogin 67*151251b5Slogin  您可以使用`void wait_queue_wakeup(wait_queue_node_t * wait_queue_head, int64_t state);`函数,从指定的等待队列中,唤醒第一个挂起时的状态与指定的`state`相同的进程。 68*151251b5Slogin 69*151251b5Slogin  当没有符合条件的进程时,将不会唤醒任何进程,仿佛无事发生。 70*151251b5Slogin 71*151251b5Slogin 72*151251b5Slogin------------------------------------------------------------ 73*151251b5Slogin   74*151251b5Slogin   75*151251b5Slogin   76*151251b5Slogin 77*151251b5Slogin 78*151251b5Slogin## 二. wait_queue_head等待队列头 79*151251b5Slogin 80*151251b5Slogin   数据结构定义如下: 81*151251b5Slogin 82*151251b5Slogin```c 83*151251b5Slogintypedef struct 84*151251b5Slogin{ 85*151251b5Slogin struct List wait_list; 86*151251b5Slogin spinlock_t lock; // 队列需要有一个自旋锁,虽然目前内部并没有使用,但是以后可能会用. 87*151251b5Slogin} wait_queue_head_t; 88*151251b5Slogin``` 89*151251b5Slogin 90*151251b5Slogin   等待队列头的使用逻辑与等待队列实际是一样的,因为他同样也是等待队列的节点(仅仅多了一把锁)。且wait_queue_head的函数基本上与wait_queue一致,只不过多了\*\*\*\_with\_node\_\*\*\*的字符串。 91*151251b5Slogin 92*151251b5Slogin   同时,wait_queue.h文件中提供了很多的宏,可以方便您的工作。 93*151251b5Slogin 94*151251b5Slogin### 提供的宏 95*151251b5Slogin| 宏 | 解释 | 96*151251b5Slogin| ----------------------------------- | ----------------------------------------------------------- | 97*151251b5Slogin| DECLARE_WAIT_ON_STACK(name, pcb) | 在栈上声明一个wait_queue节点,同时把pcb所代表的进程与该节点绑定 | 98*151251b5Slogin| DECLARE_WAIT_ON_STACK_SELF(name) | 传在栈上声明一个wait_queue节点,同时当前进程(即自身进程)与该节点绑定 | 99*151251b5Slogin| DECLARE_WAIT_ALLOC(name, pcb) | 使用`kzalloc`声明一个wait_queue节点,同时把pcb所代表的进程与该节点绑定,请记得使用kfree释放空间 | 100*151251b5Slogin| DECLARE_WAIT_ALLOC_SELF(name) | 使用`kzalloc`声明一个wait_queue节点,同时当前进程(即自身进程)与该节点绑定,请记得使用kfree释放空间 | 101*151251b5Slogin 102*151251b5Slogin 103*151251b5Slogin 104*151251b5Slogin### 创建等待队列头 105*151251b5Slogin   您可以直接调用宏 106*151251b5Slogin```c 107*151251b5SloginDECLARE_WAIT_QUEUE_HEAD(m_wait_queue_head); // 在栈上声明一个队列头变量 108*151251b5Slogin``` 109*151251b5Slogin   也可以手动声明 110*151251b5Slogin```c 111*151251b5Sloginstruct wait_queue_head_t m_wait_queue_head = {0}; 112*151251b5Sloginwait_queue_head_init(&m_wait_queue_head); 113*151251b5Slogin``` 114*151251b5Slogin 115*151251b5Slogin 116*151251b5Slogin### 将结点插入等待队列 117*151251b5Slogin 118*151251b5Slogin| 函数名 | 解释 | 119*151251b5Slogin| ----------------------------------- | ----------------------------------------------------------- | 120*151251b5Slogin| wait_queue_sleep_with_node(wait_queue_head_t *head, wait_queue_node_t *wait_node) | 传入一个等待队列节点,并设置该节点的挂起状态为PROC_UNINTERRUPTIBLE | 121*151251b5Slogin| wait_queue_sleep_with_node_unlock(wait_queue_head_t *q, wait_queue_node_t *wait, void *lock) | 传入一个等待队列节点,将该节点的pcb指向的进程挂起,并设置挂起状态为PROC_UNINTERRUPTIBLE。待当前进程被插入等待队列后,解锁给定的自旋锁 | 122*151251b5Slogin| wait_queue_sleep_with_node_interriptible(wait_queue_head_t *q, wait_queue_node_t *wait) | 传入一个等待队列节点,将该节点的pcb指向的进程挂起,并设置挂起状态为PROC_INTERRUPTIBLE | 123*151251b5Slogin 124*151251b5Slogin 125*151251b5Slogin 126*151251b5Slogin### 从等待队列唤醒一个进程 127*151251b5Slogin   在`wait_queue.h`中的`wait_queue_wakeup`函数直接kfree掉了wait_node节点。对于在栈上的wait_node,您可以选择`wait_queue_wakeup_on_stack(wait_queue_head_t *q, int64_t state)`来唤醒队列里面的队列头节点。 128*151251b5Slogin 129*151251b5Slogin------------------------------------------------------------ 130*151251b5Slogin   131*151251b5Slogin   132*151251b5Slogin   133*151251b5Slogin 134*151251b5Slogin## 三. completion完成量 135*151251b5Slogin 136*151251b5Slogin 137*151251b5Slogin### 简单用法 138*151251b5Slogin  完成量的使用方法主要分为以下几部分: 139*151251b5Slogin 140*151251b5Slogin- 声明一个完成量(可以在栈中/使用kmalloc/使用数组) 141*151251b5Slogin- 使用wait_for_completion等待事件完成 142*151251b5Slogin- 使用complete唤醒等待的进程 143*151251b5Slogin 144*151251b5Slogin   等待操作 145*151251b5Slogin```c 146*151251b5Sloginvoid wait_fun() { 147*151251b5Slogin DECLARE_COMPLETION_ON_STACK(comp); // 声明一个completion 148*151251b5Slogin 149*151251b5Slogin // .... do somethind here 150*151251b5Slogin // 大部分情况是你使用kthread_run()创建了另一个线程 151*151251b5Slogin // 你需要把comp变量传给这个线程, 然后当前线程就会等待他的完成 152*151251b5Slogin 153*151251b5Slogin if (!try_wait_for_completion(&comp)) // 进入等待 154*151251b5Slogin wait_for_completion(&comp); 155*151251b5Slogin} 156*151251b5Slogin``` 157*151251b5Slogin 158*151251b5Slogin   完成操作 159*151251b5Slogin```c 160*151251b5Sloginvoid kthread_fun(struct completion *comp) { 161*151251b5Slogin // ...... 做一些事 ....... 162*151251b5Slogin // 这里你确定你完成了目标事件 163*151251b5Slogin 164*151251b5Slogin complete(&comp); 165*151251b5Slogin // 或者你使用complete_all 166*151251b5Slogin complete_all(&comp); 167*151251b5Slogin} 168*151251b5Slogin``` 169*151251b5Slogin 170*151251b5Slogin### 更多用法 171*151251b5Slogin   kernel/sched/completion.c文件夹中,你可以看到 __test 开头的几个函数,他们是completion模块的测试代码,基本覆盖了completion的大部分函数.你可以在这里查询函数使用方法. 172*151251b5Slogin 173*151251b5Slogin### 初始化完成量 174*151251b5Slogin   函数`completion_init(struct completion *x)`提供了初始化completion的功能。当你使用`DECLARE_COMPLETION_ON_STACK`来创建(在栈上创建)的时候,会自动初始化. 175*151251b5Slogin 176*151251b5Slogin### 关于完成量的wait系列函数 177*151251b5Slogin 178*151251b5Slogin| 函数名 | 解释 | 179*151251b5Slogin| ----------------------------------- | ----------------------------------------------------------- | 180*151251b5Slogin| wait_for_completion(struct completion *x) | 将当前进程挂起,并设置挂起状态为PROC_UNINTERRUPTIBLE。 | 181*151251b5Slogin| wait_for_completion_timeout(struct completion *x, long timeout) | 将当前进程挂起,并设置挂起状态为PROC_UNINTERRUPTIBLE。当等待timeout时间(jiffies时间片)之后,自动唤醒进程。 | 182*151251b5Slogin| wait_for_completion_interruptible(struct completion *x) | 将当前进程挂起,并设置挂起状态为PROC_INTERRUPTIBLE。 | 183*151251b5Slogin| wait_for_completion_interruptible_timeout(struct completion *x, long timeout) | 将当前进程挂起,并设置挂起状态为PROC_INTERRUPTIBLE。当等待timeout时间(jiffies时间片)之后,自动唤醒进程。 | 184*151251b5Slogin| wait_for_multicompletion(struct completion x[], int n)| 将当前进程挂起,并设置挂起状态为PROC_UNINTERRUPTIBLE。(等待数组里面的completion的完成) | 185*151251b5Slogin 186*151251b5Slogin 187*151251b5Slogin 188*151251b5Slogin### 关于完成量的complete系列函数 189*151251b5Slogin 190*151251b5Slogin| 函数名 | 解释 | 191*151251b5Slogin| ----------------------------------- | ----------------------------------------------------------- | 192*151251b5Slogin| complete(struct completion *x) | 表明一个事件被完成,从等待队列中唤醒一个进程 | 193*151251b5Slogin| complete_all(struct completion *x) | 表明与该completion有关的事件被标记为永久完成,并唤醒等待队列中的所有进程 | 194*151251b5Slogin 195*151251b5Slogin 196*151251b5Slogin### 其他用于查询信息的函数 197*151251b5Slogin| 函数名 | 解释 | 198*151251b5Slogin| ----------------------------------- | ----------------------------------------------------------- | 199*151251b5Slogin| completion_done(struct completion *x) | 查询completion的done变量是不是大于0,如果大于0,返回true;否则返回false。在等待前加上这个函数有可能加速?(暂未经过实验测试,有待证明) | 200*151251b5Slogin| try_wait_for_completion(struct completion *x) | 查询completion的done变量是不是大于0,如果大于0,返回true(同时令done-=1);否则返回false。在等待前加上这个函数有可能加速?(该函数和`completion_done`代码逻辑基本一致,但是会主动令completion的done变量减1) | 201*151251b5Slogin 202*151251b5Slogin 203