7.6 代码:sleep和wakeup

让我们看看sleepkernel/proc.c:548)和wakeupkernel/proc.c:582)的实现。其基本思想是让sleep将当前进程标记为SLEEPING,然后调用sched释放CPU;wakeup查找在给定等待通道上休眠的进程,并将其标记为RUNNABLEsleepwakeup的调用者可以使用任何相互间方便的数字作为通道。Xv6通常使用等待过程中涉及的内核数据结构的地址。

sleep获得p->lockkernel/proc.c:559)。要进入睡眠的进程现在同时持有p->locklk。在调用者(示例中为P)中持有lk是必要的:它确保没有其他进程(在示例中指一个运行的V)可以启动wakeup(chan)调用。既然sleep持有p->lock,那么释放lk是安全的:其他进程可能会启动对wakeup(chan)的调用,但是wakeup将等待获取p->lock,因此将等待sleep把进程置于睡眠状态的完成,以防止wakeup错过sleep

还有一个小问题:如果lkp->lock是同一个锁,那么如果sleep试图获取p->lock就会自身死锁。但是,如果调用sleep的进程已经持有p->lock,那么它不需要做更多的事情来避免错过并发的wakeup。当waitkernel/proc.c:582)持有p->lock调用sleep时,就会出现这种情况。

由于sleep只持有p->lock而无其他,它可以通过记录睡眠通道、将进程状态更改为SLEEPING并调用schedkernel/proc.c:564-567)将进程置于睡眠状态。过一会儿,我们就会明白为什么在进程被标记为SLEEPING之前不将p->lock释放(由scheduler)是至关重要的。

在某个时刻,一个进程将获取条件锁,设置睡眠者正在等待的条件,并调用wakeup(chan)。在持有状态锁时调用wakeup非常重要[注]wakeup遍历进程表(kernel/proc.c:582)。它获取它所检查的每个进程的p->lock,这既是因为它可能会操纵该进程的状态,也是因为p->lock确保sleepwakeup不会彼此错过。当wakeup发现一个SLEEPING的进程且chan相匹配时,它会将该进程的状态更改为RUNNABLE。调度器下次运行时,将看到进程已准备好运行。

注:严格地说,wakeup只需跟在acquire之后就足够了(也就是说,可以在release之后调用wakeup

为什么sleepwakeup的用锁规则能确保睡眠进程不会错过唤醒?休眠进程从检查条件之前的某处到标记为休眠之后的某处,要么持有条件锁,要么持有其自身的p->lock或同时持有两者。调用wakeup的进程在wakeup的循环中同时持有这两个锁。因此,要么唤醒器(waker)在消费者线程检查条件之前使条件为真;要么唤醒器的wakeup在睡眠线程标记为SLEEPING后对其进行严格检查。然后wakeup将看到睡眠进程并将其唤醒(除非有其他东西首先将其唤醒)。

有时,多个进程在同一个通道上睡眠;例如,多个进程读取同一个管道。一个单独的wakeup调用就能把他们全部唤醒。其中一个将首先运行并获取与sleep一同调用的锁,并且(在管道例子中)读取在管道中等待的任何数据。尽管被唤醒,其他进程将发现没有要读取的数据。从他们的角度来看,醒来是“虚假的”,他们必须再次睡眠。因此,在检查条件的循环中总是调用sleep

如果两次使用sleep/wakeup时意外选择了相同的通道,则不会造成任何伤害:它们将看到虚假的唤醒,但如上所述的循环将容忍此问题。sleep/wakeup的魅力在于它既轻量级(不需要创建特殊的数据结构来充当睡眠通道),又提供了一层抽象(调用者不需要知道他们正在与哪个特定进程进行交互)。