Appearance
代码:调度
上一节我们研究了swtch
的底层细节;现在让我们将swtch
视为一个给定的功能,并研究从一个进程的内核线程通过调度器切换到另一个进程的过程。调度器以每个CPU一个特殊线程的形式存在,每个线程运行scheduler
函数。该函数负责选择下一个要运行的进程。希望放弃CPU的进程必须获取自己的进程锁p->lock
,释放它持有的任何其他锁,更新自己的状态(p->state
),然后调用sched
。你可以在yield
、sleep
和exit
中看到这个序列。sched
会仔细检查其中一些要求,然后检查一个推论:由于持有一个锁,中断应该被禁用。最后,sched
调用swtch
将当前上下文保存在p->context
中,并切换到cpu->context
中的调度器上下文。swtch
在调度器的栈上返回,就好像scheduler
的swtch
返回了一样。调度器继续其for
循环,找到一个要运行的进程,切换到它,然后循环重复。
我们刚刚看到xv6在调用swtch
期间持有p->lock
:swtch
的调用者必须已经持有该锁,并且锁的控制权传递给被切换到的代码。这种安排不寻常:更常见的是获取锁的线程也释放它。Xv6的上下文切换必须打破这个惯例,因为p->lock
保护进程的state
和context
字段的不变性,这些不变性在swtch
中执行时是不成立的。例如,如果在swtch
期间没有持有p->lock
,另一个CPU可能会在yield
将其状态设置为RUNNABLE
之后,但在swtch
使其停止使用自己的内核栈之前,决定运行该进程。结果将是两个CPU在同一个栈上运行,这会引起混乱。一旦yield
开始修改一个正在运行的进程的状态以使其变为RUNNABLE
,p->lock
必须保持持有直到不变性恢复:最早的正确释放点是在scheduler
(在其自己的栈上运行)清除c->proc
之后。类似地,一旦scheduler
开始将一个RUNNABLE
的进程转换为RUNNING
,锁就不能被释放,直到进程的内核线程完全运行(在swtch
之后,例如在yield
中)。
内核线程放弃其CPU的唯一地方是在sched
中,并且它总是切换到scheduler
中的相同位置,而scheduler
(几乎)总是切换到某个先前调用了sched
的内核线程。因此,如果有人打印出xv6切换线程的行号,他会观察到以下简单的模式:proc.c:swtch
,proc.c:swtch
,proc.c:swtch
,等等。通过线程切换有意地将控制权相互转移的过程有时被称为协程(coroutines);在这个例子中,sched
和scheduler
是彼此的协程。
有一种情况是调度器的swtch
调用不会在sched
中结束。allocproc
将新进程的上下文ra
寄存器设置为forkret
,以便其第一次swtch
“返回”到该函数的开头。forkret
的存在是为了释放p->lock
;否则,由于新进程需要像从fork
返回一样返回到用户空间,它可以在usertrapret
处开始。
scheduler
运行一个循环:找到一个要运行的进程,运行它直到它让出,重复。调度器遍历进程表寻找一个可运行的进程,即p->state == RUNNABLE
的进程。一旦找到一个进程,它就会设置每个CPU的当前进程变量c->proc
,将进程标记为RUNNING
,然后调用swtch
开始运行它。