BBS水木清华站∶精华区

发信人: axp33a (无聊中...), 信区: Linux 
标  题: Linux内核源代码分析2-2-2 
发信站: BBS 水木清华站 (Thu Aug  3 11:21:53 2000) WWW-POST 
 
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中发现的问题一样。 
 
 
-- 
※ 来源:·BBS 水木清华站 smth.org·[FROM: 166.111.196.22]  

BBS水木清华站∶精华区