Appearance
代码: 休眠与唤醒
Xv6的sleep
和wakeup
提供了上一个例子中使用的接口。基本思想是让sleep
将当前进程标记为SLEEPING
,然后调用sched
以释放CPU;wakeup
寻找在给定等待通道上休眠的进程,并将其标记为RUNNABLE
。sleep
和wakeup
的调用者可以使用任何相互方便的数字作为通道。Xv6通常使用与等待相关的内核数据结构的地址。
sleep
获取p->lock
,然后才释放lk
。正如我们将看到的,sleep
在任何时候都持有这两个锁中的一个或另一个,这阻止了并发的wakeup
(它必须获取并持有这两个锁)采取行动。现在sleep
只持有p->lock
,它可以通过记录休眠通道、将进程状态更改为SLEEPING
并调用sched
来使进程休眠。稍后就会清楚,为什么在进程被标记为SLEEPING
之后,p->lock
不被(由scheduler
)释放是至关重要的。
在某个时刻,一个进程将获取条件锁,设置休眠者正在等待的条件,并调用wakeup(chan)
。重要的是wakeup
在持有条件锁的同时被调用。wakeup
遍历进程表。它获取它检查的每个进程的p->lock
。当wakeup
发现一个处于SLEEPING
状态且具有匹配chan
的进程时,它将该进程的状态更改为RUNNABLE
。下一次scheduler
运行时,它将看到该进程已准备好运行。
为什么sleep
和wakeup
的锁定规则能确保一个即将进入休眠的进程不会错过一个并发的唤醒?即将进入休眠的进程从检查条件之前到被标记为SLEEPING
之后,都持有条件锁或它自己的p->lock
,或两者兼有。调用wakeup
的进程在wakeup
的循环中持有两个锁。因此,唤醒者要么在消费线程检查条件之前使条件为真;要么唤醒者的wakeup
严格地在休眠线程被标记为SLEEPING
之后检查它。然后wakeup
将看到休眠的进程并唤醒它(除非有其他东西先唤醒它)。
有时多个进程在同一个通道上休眠;例如,多个进程从一个管道读取。一个对wakeup
的调用将唤醒所有这些进程。其中一个将首先运行并获取sleep
被调用时所持有的锁,并(在管道的情况下)读取任何等待的数据。其他进程会发现,尽管被唤醒,但没有数据可读。从它们的角度来看,唤醒是“虚假的”,它们必须再次休眠。因此,sleep
总是在一个检查条件的循环内调用。
如果两个sleep/wakeup的使用意外地选择了同一个通道,也不会造成伤害:它们会看到虚假的唤醒,但如上所述的循环将容忍这个问题。sleep/wakeup的魅力很大程度上在于它既是轻量级的(不需要创建特殊的数据结构来充当休眠通道),又提供了一层间接性(调用者不需要知道它们正在与哪个特定的进程交互)。