7.6 代码:sleep和wakeup
让我们看看sleep
(kernel/proc.c:548)和wakeup
(kernel/proc.c:582)的实现。其基本思想是让sleep
将当前进程标记为SLEEPING
,然后调用sched
释放CPU;wakeup
查找在给定等待通道上休眠的进程,并将其标记为RUNNABLE
。sleep
和wakeup
的调用者可以使用任何相互间方便的数字作为通道。Xv6通常使用等待过程中涉及的内核数据结构的地址。
sleep
获得p->lock
(kernel/proc.c:559)。要进入睡眠的进程现在同时持有p->lock
和lk
。在调用者(示例中为P
)中持有lk
是必要的:它确保没有其他进程(在示例中指一个运行的V
)可以启动wakeup(chan)
调用。既然sleep
持有p->lock
,那么释放lk
是安全的:其他进程可能会启动对wakeup(chan)
的调用,但是wakeup
将等待获取p->lock
,因此将等待sleep
把进程置于睡眠状态的完成,以防止wakeup
错过sleep
。
还有一个小问题:如果lk
和p->lock
是同一个锁,那么如果sleep
试图获取p->lock
就会自身死锁。但是,如果调用sleep
的进程已经持有p->lock
,那么它不需要做更多的事情来避免错过并发的wakeup
。当wait
(kernel/proc.c:582)持有p->lock
调用sleep
时,就会出现这种情况。
由于sleep
只持有p->lock
而无其他,它可以通过记录睡眠通道、将进程状态更改为SLEEPING
并调用sched
(kernel/proc.c:564-567)将进程置于睡眠状态。过一会儿,我们就会明白为什么在进程被标记为SLEEPING
之前不将p->lock
释放(由scheduler
)是至关重要的。
在某个时刻,一个进程将获取条件锁,设置睡眠者正在等待的条件,并调用wakeup(chan)
。在持有状态锁时调用wakeup
非常重要[注]。wakeup
遍历进程表(kernel/proc.c:582)。它获取它所检查的每个进程的p->lock
,这既是因为它可能会操纵该进程的状态,也是因为p->lock
确保sleep
和wakeup
不会彼此错过。当wakeup
发现一个SLEEPING
的进程且chan
相匹配时,它会将该进程的状态更改为RUNNABLE
。调度器下次运行时,将看到进程已准备好运行。
注:严格地说,
wakeup
只需跟在acquire
之后就足够了(也就是说,可以在release
之后调用wakeup
)
为什么sleep
和wakeup
的用锁规则能确保睡眠进程不会错过唤醒?休眠进程从检查条件之前的某处到标记为休眠之后的某处,要么持有条件锁,要么持有其自身的p->lock
或同时持有两者。调用wakeup
的进程在wakeup
的循环中同时持有这两个锁。因此,要么唤醒器(waker)在消费者线程检查条件之前使条件为真;要么唤醒器的wakeup
在睡眠线程标记为SLEEPING
后对其进行严格检查。然后wakeup
将看到睡眠进程并将其唤醒(除非有其他东西首先将其唤醒)。
有时,多个进程在同一个通道上睡眠;例如,多个进程读取同一个管道。一个单独的wakeup
调用就能把他们全部唤醒。其中一个将首先运行并获取与sleep
一同调用的锁,并且(在管道例子中)读取在管道中等待的任何数据。尽管被唤醒,其他进程将发现没有要读取的数据。从他们的角度来看,醒来是“虚假的”,他们必须再次睡眠。因此,在检查条件的循环中总是调用sleep
。
如果两次使用sleep/wakeup
时意外选择了相同的通道,则不会造成任何伤害:它们将看到虚假的唤醒,但如上所述的循环将容忍此问题。sleep/wakeup
的魅力在于它既轻量级(不需要创建特殊的数据结构来充当睡眠通道),又提供了一层抽象(调用者不需要知道他们正在与哪个特定进程进行交互)。