2.2.2 等待队列
前一节我们曾简要的提到进程(也就是正在运行的程序)可以转入休眠状态以等待某个特
定事件,当该事件发生时这些进程能够被再次唤醒。内核实现这一功能的技术要点是把等
待队列(wait queue)和每一个事件联系起来。需要等待事件的进程在转入休眠状态后插
入到队列中。当事件发生之后,内核遍历相应队列,唤醒休眠的任务让它投入运行状态。
任务负责将自己从等待队列中清除。
等待队列的功能强大得令人吃惊,它们被广泛应用于整个内核中。更重要的是,实现等待
队列的代码量并不大。
1. wait_queue结构
18662:简单的数据结构就是等待队列节点,它包含两个元素:
* task—指向struct task_struct结构的指针,它代表一个进程。从16325行开始的
struct task_struct结构将在第7章中进行介绍。
* next—指向队列中下一节点的指针。因而,等待队列实际上是一个单链表。
通常,我们用指向等待队列队首的指针来表示等待队列。例如,printk使用的等待队列
log_wait(25647行)。
2. wait_event
16840:通过使用这个宏,内核代码能够使当前执行的进程在等待队列wq中等待直至给定
condition(可能是任何的表达式)得到满足。
16842:如果条件已经为真,当前进程显然也就无需等待了。
16844:否则,进程必须等待给定条件转变为真。这可以通过调用__wait_event来实现(
16824行),我们将在下一节介绍它。由于__wait_event已经同wait_event分离,已知条
件为假的部分内核代码可以直接调用__wait_queue,而不用通过宏来进行冗余的(特别是
在这些情况下)测试,实际上也没有代码会真正这样处理。更为重要的是,如果条件已经
为真,wait_event会跳过将进程插入等待队列的代码。
注意wait_event的主体是用一个比较特殊的结构封闭起来的:
奇怪的是,这个小技巧并没有得到应有的重视。这里的主要思路是使被封闭的代码能够像
一个单句一样使用。考虑下面这个宏,该宏的目的是如果p是一个非空指针,则调用free

除非你在如下所述的情况下使用FREE1,否则所有调用都是正确有效的:
FREE1经扩展以后,else就和错误的if(FREE1的if)联系在一起。
有些程序员通过如下途径解决这种问题:
这两种方法都不尽人意,程序员在调用宏以后自然而然使用的分号会把扩展信息弄乱。以
FREE2为例,在宏展开之后,为了使编译器能更准确地识别,我们还需要进行一定的缩进
调节,最终代码如下所示:
这样就会引起语法错误—else和任何一个if都不匹配。FREE3从本质上讲也存在同样的问
题。而且在研究问题产生原因的同时,就能够明白为什么宏体里是否包含if是无关紧要的
。不管宏体内部内容如何,只要使用一组括号来指定宏体,就会碰到相同的问题。
引入do/while(0)技巧能够克服前面所出现的所有问题,现在我们可以编写FREE4。
将FREE4和其他宏一样插入相同代码之后,宏展开后其代码如下所示(为清晰起见,我们
再次调整了缩进格式):
这段代码当然可以正确执行。编译器能够优化这个伪循环,舍弃循环控制,因此执行代码
并没有速度的损失,我们也从而得到了能够实现理想功能的宏。
虽然这是一个可以接受的解决方案,但是我们不能不提到的是编写函数要比编写宏好得多
。不过如果你不能提供函数调用所需的开销,那么就需要使用内联函数。这种情况虽然在
内核中经常出现,但是在其他地方就要少得多。(不可否认,当使用C++、gcc或者任何实
现了将要出现的修正版ISO标准C的编译器时,这种方案只是一种选择,就是最后为C增加
内联函数。)
3. __wait_event
16824:__wait_event使当前进程在等待队列wq中等待,直至condition为真。
16829:通过调用add_wait_queue(16791行),局部变量__wait可以被链接到队列上。注
意__wait是在堆栈中而不是在内核堆中分配空间,这是内核中常用的一种技巧。在宏运行
结束之前,__wait就已经被从等待队列中移走了,因此等待队列中指向它的指针总是有效
的。
16830:重复分配CPU给另一个进程直至条件满足,这一点将在下面几节中讨论。
16831:进程被置为TASK_UNINTERRUPTIBLE状态(16190行)。这意味着进程处于休眠状态
,不应被唤醒,即使是信号也不能打断该进程的休眠。信号在第6章中介绍,而进程状态
则在第7章中介绍。
16832:如果条件已经满足,则可以退出循环。
请注意如果在第一次循环时条件就已经满足,那么前面一行的赋值就浪费了(因为在循环
结束之后进程状态会立刻被再次赋值)。__wait_event假定宏开始执行时条件还没有得到
满足。而且,这种对进程状态变量state的延迟赋值也并没有什么害处。在某些特殊情况
下,这种方法还十分有益。例如当__wait_event开始执行时条件为假,但是在执行到
16832行时就为真了。这种变化只有在为有关进程状态的代码计算condition变量值时才会
出现问题。但是在代码中这种情况我没有发现。
16834:调用schedule(26686行,在第7章中讨论)将CPU转移给另一个进程。直到进程再
次获得CPU时,对schedule的调用才会返回。这种情况只有当等待队列中的进程被唤醒时
才会发生。
16836:进程已经退出了,因此条件必定已经得到了满足。进程重置TASK_RUNNING的状态
(16188行),使其适合CPU运行。
16837:通过调用remove_wait_queue(16814行)将进程从等待队列中移去。
wait_event_interruptible和__wait_event_interruptible(分别参见16868行和16847)
基本上与wait_event和__wait_event相同,但不同的是它们允许休眠的进程可以被信号中
断。信号将在第6章中介绍。
请注意wait_event是被如下结构所包含的。
和do/while(0)技巧一样,这样可以使被封闭起来的代码能够像一个单元一样运行。这样
的封闭代码就是一个独立的表达式,而不是一个独立的语句。也就是说,它可以求值以供
其他更复杂的表达式使用。发生这种情况的原因主要在于一些不可移植的gcc特有代码的
存在。通过使用这类技巧,一个程序块中的最后一个表达式的值将定义为整个程序块的最
终值。当在表达式中使用wait_event_interruptible时,执行宏体后赋__ret的值为宏体
的值(参见16873行)。对于有Lisp背景知识的程序员来说,这是个很常见的概念。但是
如果你仅仅了解一点C和其他一些相关的过程性程序设计语言,你可能就会觉得比较奇怪

__wake_up
26829:该函数用来唤醒等待队列中正在休眠的进程。它由wake_up和wake_up_
interruptible调用(请分别参见16612行和16614行)。这些宏提供mode参数,只有状态
满足mode所包含的状态之一的进程才可能被唤醒。
26833:正如将在第10章中详细讨论的那样,锁(lock)是用来限制对资源的访问,这在
SMP逻辑单元中尤其重要,因为在这种情况下当一个CPU在修改某数据结构时,另一个CPU
可能正在从该数据结构中读取数据,或者也有可能两个CPU同时对同一个数据结构进行修
改,等等。在这种情况下,受保护的资源显然是等待队列。非常有趣的是所有的等待队列
都使用同一个锁来保护。虽然这种方法要比为每一个等待队列定义一个新锁简单得多,但
是这就意味着SMP逻辑单元可能经常会发现自己正在等待一个实际上并不必须的锁。
26838:本段代码遍历非空队列,为队列中正确状态的每一个进程调用wake_up_process(
26356行)。如前所述,进程(队列节点)在此可能并没有从队列中移走。这在很大程度
上是由于即使队列中的进程正在被唤醒,它仍然可能希望继续存在于等待队列中,这一点
正如我们在__wait_event中发现的问题一样。