本文共 3414 字,大约阅读时间需要 11 分钟。
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中发现的问题一样。转载地址:http://xevqa.baihongyu.com/