Skip to content

代码:调度

上一节我们研究了swtch的底层细节;现在让我们将swtch视为一个给定的功能,并研究从一个进程的内核线程通过调度器切换到另一个进程的过程。调度器以每个CPU一个特殊线程的形式存在,每个线程运行scheduler函数。该函数负责选择下一个要运行的进程。希望放弃CPU的进程必须获取自己的进程锁p->lock,释放它持有的任何其他锁,更新自己的状态(p->state),然后调用sched。你可以在yieldsleepexit中看到这个序列。sched会仔细检查其中一些要求,然后检查一个推论:由于持有一个锁,中断应该被禁用。最后,sched调用swtch将当前上下文保存在p->context中,并切换到cpu->context中的调度器上下文。swtch在调度器的栈上返回,就好像schedulerswtch返回了一样。调度器继续其for循环,找到一个要运行的进程,切换到它,然后循环重复。

我们刚刚看到xv6在调用swtch期间持有p->lockswtch的调用者必须已经持有该锁,并且锁的控制权传递给被切换到的代码。这种安排不寻常:更常见的是获取锁的线程也释放它。Xv6的上下文切换必须打破这个惯例,因为p->lock保护进程的statecontext字段的不变性,这些不变性在swtch中执行时是不成立的。例如,如果在swtch期间没有持有p->lock,另一个CPU可能会在yield将其状态设置为RUNNABLE之后,但在swtch使其停止使用自己的内核栈之前,决定运行该进程。结果将是两个CPU在同一个栈上运行,这会引起混乱。一旦yield开始修改一个正在运行的进程的状态以使其变为RUNNABLEp->lock必须保持持有直到不变性恢复:最早的正确释放点是在scheduler(在其自己的栈上运行)清除c->proc之后。类似地,一旦scheduler开始将一个RUNNABLE的进程转换为RUNNING,锁就不能被释放,直到进程的内核线程完全运行(在swtch之后,例如在yield中)。

内核线程放弃其CPU的唯一地方是在sched中,并且它总是切换到scheduler中的相同位置,而scheduler(几乎)总是切换到某个先前调用了sched的内核线程。因此,如果有人打印出xv6切换线程的行号,他会观察到以下简单的模式:proc.c:swtchproc.c:swtchproc.c:swtch,等等。通过线程切换有意地将控制权相互转移的过程有时被称为协程(coroutines);在这个例子中,schedscheduler是彼此的协程。

有一种情况是调度器的swtch调用不会在sched中结束。allocproc将新进程的上下文ra寄存器设置为forkret,以便其第一次swtch“返回”到该函数的开头。forkret的存在是为了释放p->lock;否则,由于新进程需要像从fork返回一样返回到用户空间,它可以在usertrapret处开始。

scheduler运行一个循环:找到一个要运行的进程,运行它直到它让出,重复。调度器遍历进程表寻找一个可运行的进程,即p->state == RUNNABLE的进程。一旦找到一个进程,它就会设置每个CPU的当前进程变量c->proc,将进程标记为RUNNING,然后调用swtch开始运行它。