# 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`的魅力在于它既轻量级（不需要创建特殊的数据结构来充当睡眠通道），又提供了一层抽象（调用者不需要知道他们正在与哪个特定进程进行交互）。