# 7.9 真实世界

xv6调度器实现了一个简单的调度策略：它依次运行每个进程。这一策略被称为轮询调度（round robin）。真实的操作系统实施更复杂的策略，例如，允许进程具有优先级。其思想是调度器将优先选择可运行的高优先级进程，而不是可运行的低优先级进程。这些策略可能变得很复杂，因为常常存在相互竞争的目标：例如，操作系统可能希望保证公平性和高吞吐量。此外，复杂的策略可能会导致意外的交互，例如优先级反转（priority inversion）和航队（convoys）。当低优先级进程和高优先级进程共享一个锁时，可能会发生优先级反转，当低优先级进程持有该锁时，可能会阻止高优先级进程前进。当许多高优先级进程正在等待一个获得共享锁的低优先级进程时，可能会形成一个长的等待进程航队；一旦航队形成，它可以持续很长时间。为了避免此类问题，在复杂的调度器中需要额外的机制。

睡眠和唤醒是一种简单有效的同步方法，但还有很多其他方法。所有这些问题中的第一个挑战是避免我们在本章开头看到的“丢失唤醒”问题。原始Unix内核的`sleep`只是禁用了中断，这就足够了，因为Unix运行在单CPU系统上。因为xv6在多处理器上运行，所以它为`sleep`添加了一个显式锁。FreeBSD的`msleep`采用了同样的方法。Plan 9的`sleep`使用一个回调函数，该函数在马上睡眠时获取调度锁，并在运行中持有；该函数用于在最后时刻检查睡眠条件，以避免丢失唤醒。Linux内核的`sleep`使用一个显式的进程队列，称为等待队列，而不是等待通道；队列有自己内部的锁。

在`wakeup`中扫描整个进程列表以查找具有匹配`chan`的进程效率低下。一个更好的解决方案是用一个数据结构替换`sleep`和`wakeup`中的`chan`，该数据结构包含在该结构上休眠的进程列表，例如Linux的等待队列。Plan 9的`sleep`和`wakeup`将该结构称为集结点（rendezvous point）或Rendez。许多线程库引用与条件变量相同的结构；在这种情况下，`sleep`和`wakeup`操作称为`wait`和`signal`。所有这些机制都有一个共同的特点：睡眠条件受到某种在睡眠过程中原子级释放的锁的保护。

`wakeup`的实现会唤醒在特定通道上等待的所有进程，可能有许多进程在等待该特定通道。操作系统将安排所有这些进程，它们将竞相检查睡眠条件。进程的这种行为有时被称为惊群效应（thundering herd），最好避免。大多数条件变量都有两个用于唤醒的原语：`signal`用于唤醒一个进程；`broadcast`用于唤醒所有等待进程。

信号量（Semaphores）通常用于同步。计数count通常对应于管道缓冲区中可用的字节数或进程具有的僵尸子进程数。使用显式计数作为抽象的一部分可以避免“丢失唤醒”问题：使用显式计数记录已经发生`wakeup`的次数。计数还避免了虚假唤醒和惊群效应问题。

终止进程并清理它们在xv6中引入了很多复杂性。在大多数操作系统中甚至更复杂，因为，例如，受害者进程可能在内核深处休眠，而展开其栈空间需要非常仔细的编程。许多操作系统使用显式异常处理机制（如`longjmp`）来展开栈。此外，还有其他事件可能导致睡眠进程被唤醒，即使它等待的事件尚未发生。例如，当一个Unix进程处于休眠状态时，另一个进程可能会向它发送一个`signal`。在这种情况下，进程将从中断的系统调用返回，返回值为-1，错误代码设置为`EINTR`。应用程序可以检查这些值并决定执行什么操作。Xv6不支持信号，因此不会出现这种复杂性。

Xv6对`kill`的支持并不完全令人满意：有一些`sleep`循环可能应该检查`p->killed`。一个相关的问题是，即使对于检查`p->killed`的`sleep`循环，`sleep`和`kill`之间也存在竞争；后者可能会设置`p->killed`，并试图在受害者的循环检查`p->killed`之后但在调用`sleep`之前尝试唤醒受害者。如果出现此问题，受害者将不会注意到`p->killed`，直到其等待的条件发生。这可能比正常情况要晚一点（例如，当virtio驱动程序返回受害者正在等待的磁盘块时）或永远不会发生（例如，如果受害者正在等待来自控制台的输入，但用户没有键入任何输入）。

> 注：上节中说到kill的工作方式，`kill`设置`p->killed`，如果遇到进程正在休眠，则会唤醒它，此后在`usertrap`中检测`p->killed`，并使进程退出

> 而如果像上面说的，在检查`p->killed`之后调用`sleep`之前唤醒受害者进程，那么接下来执行`sleep`就会导致进程无法进入内核，无法在`usertrap`中退出，而必须等待所需事件的发生再次唤醒

一个实际的操作系统将在固定时间内使用空闲列表找到自由的`proc`结构体，而不是`allocproc`中的线性时间搜索；xv6使用线性扫描是为了简单起见。