7.3 代码:调度

上一节介绍了swtch的底层细节;现在,让我们以swtch为给定对象,检查从一个进程的内核线程通过调度程序切换到另一个进程的情况。调度器(scheduler)以每个CPU上一个特殊线程的形式存在,每个线程都运行scheduler函数。此函数负责选择下一个要运行的进程。想要放弃CPU的进程必须先获得自己的进程锁p->lock,并释放它持有的任何其他锁,更新自己的状态(p->state),然后调用schedYieldkernel/proc.c:515)遵循这个约定,sleepexit也遵循这个约定,我们将在后面进行研究。Sched对这些条件再次进行检查(kernel/proc.c:499-504),并检查这些条件的隐含条件:由于锁被持有,中断应该被禁用。最后,sched调用swtch将当前上下文保存在p->context中,并切换到cpu->scheduler中的调度程序上下文。Swtch在调度程序的栈上返回,就像是schedulerswtch返回一样。scheduler继续for循环,找到要运行的进程,切换到该进程,重复循环。

我们刚刚看到,xv6在对swtch的调用中持有p->lockswtch的调用者必须已经持有了锁,并且锁的控制权传递给切换到的代码。这种约定在锁上是不寻常的;通常,获取锁的线程还负责释放锁,这使得对正确性进行推理更加容易。对于上下文切换,有必要打破这个惯例,因为p->lock保护进程statecontext字段上的不变量,而这些不变量在swtch中执行时不成立。如果在swtch期间没有保持p->lock,可能会出现一个问题:在yield将其状态设置为RUNNABLE之后,但在swtch使其停止使用自己的内核栈之前,另一个CPU可能会决定运行该进程。结果将是两个CPU在同一栈上运行,这不可能是正确的。

内核线程总是在sched中放弃其CPU,并总是切换到调度程序中的同一位置,而调度程序(几乎)总是切换到以前调用sched的某个内核线程。因此,如果要打印xv6切换线程处的行号,将观察到以下简单模式:(kernel/proc.c:475),(kernel/proc.c:509),(kernel/proc.c:475),(kernel/proc.c:509)等等。在两个线程之间进行这种样式化切换的过程有时被称为协程(coroutines);在本例中,schedscheduler是彼此的协同程序。

存在一种情况使得调度程序对swtch的调用没有以sched结束。一个新进程第一次被调度时,它从forkretkernel/proc.c:527)开始。Forkret存在以释放p->lock;否则,新进程可以从usertrapret开始。

schedulerkernel/proc.c:457)运行一个简单的循环:找到要运行的进程,运行它直到它让步,然后重复循环。scheduler在进程表上循环查找可运行的进程,该进程具有p->state == RUNNABLE。一旦找到一个进程,它将设置CPU当前进程变量c->proc,将该进程标记为RUNINING,然后调用swtch开始运行它(kernel/proc.c:470-475)。

考虑调度代码结构的一种方法是,它为每个进程强制维持一个不变量的集合,并在这些不变量不成立时持有p->lock。其中一个不变量是:如果进程是RUNNING状态,计时器中断的yield必须能够安全地从进程中切换出去;这意味着CPU寄存器必须保存进程的寄存器值(即swtch没有将它们移动到context中),并且c->proc必须指向进程。另一个不变量是:如果进程是RUNNABLE状态,空闲CPU的调度程序必须安全地运行它;这意味着p->context必须保存进程的寄存器(即,它们实际上不在实际寄存器中),没有CPU在进程的内核栈上执行,并且没有CPU的c->proc引用进程。请注意,在保持p->lock时,这些属性通常不成立。

维护上述不变量是xv6经常在一个线程中获取p->lock并在另一个线程中释放它的原因,例如在yield中获取并在scheduler中释放。一旦yield开始修改一个RUNNING进程的状态为RUNNABLE,锁必须保持被持有状态,直到不变量恢复:最早的正确释放点是scheduler(在其自身栈上运行)清除c->proc之后。类似地,一旦scheduler开始将RUNNABLE进程转换为RUNNING,在内核线程完全运行之前(在swtch之后,例如在yield中)绝不能释放锁。

p->lock还保护其他东西:exitwait之间的相互作用,避免丢失wakeup的机制(参见第7.5节),以及避免一个进程退出和其他进程读写其状态之间的争用(例如,exit系统调用查看p->pid并设置p->killed(kernel/proc.c:611))。为了清晰起见,也许为了性能起见,有必要考虑一下p->lock的不同功能是否可以拆分。