Skip to content

Commit

Permalink
7.3
Browse files Browse the repository at this point in the history
  • Loading branch information
MeouSker77 committed Mar 19, 2023
1 parent 23e18c2 commit 0d9914f
Showing 1 changed file with 37 additions and 0 deletions.
37 changes: 37 additions & 0 deletions src/ch07.tex
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,40 @@ \section{代码:上下文切换}
当我们正在追踪的\texttt{swtch}返回时,它会返回到\texttt{scheduler}而不是\texttt{sched},并且栈指针现在指向当前CPU的调度器栈。

\section{代码:调度}
上一节介绍了\texttt{swtch}的底层细节,现在让我们看看如何使用\texttt{swtch}从一个进程的内核线程经过调度器到达另一个进程。
调度器是每个CPU的特殊线程,这个线程运行\texttt{scheduler}函数。
这个函数负责选择接下来运行哪个进程。
一个想要放弃CPU的进程必须获取它自己的进程锁\texttt{p->lock},释放它持有的其他所有锁,更新它的状态(\texttt{p->state}),然后调用\texttt{sched}。
你可以在\texttt{yield}\href{https://github.com/mit-pdos/xv6-riscv/blob/risc/kernel/proc.c#L503}{(kernel/proc.c:503)}、\texttt{sleep}、\texttt{exit}中看到这个过程。
\texttt{sched}再次检查这些要求中的一些\href{https://github.com/mit-pdos/xv6-riscv/blob/risc/kernel/proc.c#L487-L492}{(kernel/proc.c:487-492)}然后检查一个隐式条件:因为有一个锁被持有来,所以中断应该被禁用了。
最后,\texttt{sched}调用\texttt{swtch}把当前的上下文保存在\texttt{p->context}中,然后切换到\texttt{cpu->context}中的调度器上下文中。
\texttt{swtch}在调度器的栈中返回,就好像\texttt{scheduler}的\texttt{swtch}返回了一样\href{https://github.com/mit-pdos/xv6-riscv/blob/risc/kernel/proc.c#L463}{(kernel/proc.c:463)}。
然后调度器继续它的\texttt{for}循环,寻找下一个要运行的进程,切换到它,然后重复这个过程。

我们刚才看到xv6在调用\texttt{swtch}的整个过程中持有着\texttt{p->lock}:\texttt{swtch}的调用者必须已经持有了这个锁,然后锁的控制权被传递到切换到的新代码中。
这种转换很不寻常,通常情况下获取锁的线程也应该负责释放锁,这样更容易确保正确性。
但对于上下文来说,打破这个惯例是必须的,因为\texttt{p->lock}负责保护进程的\texttt{state}和\texttt{context}字段上的不变量,但是这些不变量在执行\texttt{swtch}期间并不成立。
一个可能出现的问题是如果在执行\texttt{swtch}期间没有持有\texttt{p->lock},另一个CPU可能会在\texttt{yield}把这个进程的状态设置为\texttt{RUNNABLE}之后但在\texttt{swtch}让它停止使用自己的内核栈之前决定运行它。
结果会是两个CPU在同一个栈上运行,这将导致错误。

内核线程唯一放弃CPU的地方就是\texttt{sched}里,并且它总是切换到\texttt{scheduler}里的同一个地方,(几乎)总是之前调用\texttt{sched}切换到某个内核线程的那个地方。
因此,如果打印出xv6切换线程的行号,将会观测到下面的一个简单模式:\href{https://github.com/mit-pdos/xv6-riscv/blob/risc/kernel/proc.c#L463}{(kernel/proc.c:463)}、\href{https://github.com/mit-pdos/xv6-riscv/blob/risc/kernel/proc.c#L497}{(kernel/proc.c:497)}、\href{https://github.com/mit-pdos/xv6-riscv/blob/risc/kernel/proc.c#L463}{(kernel/proc.c:463)}、\href{https://github.com/mit-pdos/xv6-riscv/blob/risc/kernel/proc.c#L497}{(kernel/proc.c:497)}等等。
主动通过线程切换互相转移控制权的过程有时也被称为\emph{协程(coroutine)},
在这个例子中,\texttt{sched}和\texttt{scheduler}就是彼此的协程。

有一种情况下调度器对\texttt{swtch}的调用并不会以\texttt{sched}结束。
\texttt{allocproc}把新进程的上下文\texttt{ra}寄存器设置为\texttt{forkret}\href{https://github.com/mit-pdos/xv6-riscv/blob/risc/kernel/proc.c#L515}{(kernel/proc.c:515)},因此它的第一个\texttt{swtch}会“返回”到那个函数的起始位置。
\texttt{forkret}的存在是为了释放\texttt{p->lock};否则,由于新进程需要像从\texttt{fork}返回一样返回用户空间,它可能会从\texttt{usertrapret}开始。

\texttt{scheduler}\href{https://github.com/mit-pdos/xv6-riscv/blob/risc/kernel/proc.c#L445}{(kernel/proc.c:445)}运行一个循环:找到一个要运行的进程、运行它直到它让出CPU,然后重复。
调度器循环遍历进程表寻找一个可以运行的进程,即\texttt{p->state == RUNNABLE}的进程。
一旦它找到了一个进程,它会设置每个CPU用于表示当前进程的变量\texttt{c->proc},然后把进程标记为\texttt{RUNNING},并调用\texttt{swtch}开始运行它\href{https://github.com/mit-pdos/xv6-riscv/blob/risc/kernel/proc.c#L458-L463}{(kernel/proc.c:458-463)}。

考虑调度器代码结构的一种思路是它对每个进程强迫执行一组不变量,并在不变量不成立时持有\texttt{p->lock}。
一个不变量是如果一个进程是\texttt{RUNNING},那么时钟中断的\texttt{yield}必须能安全地切换离开这个进程;这意味着CPU寄存器必须持有这个进程的寄存器的值(即,\texttt{swtch}还没有把它们移动到\texttt{context}中),并且\texttt{c->proc}必须指向这个进程。
另一个不变量是如果一个进程是\texttt{RUNNABLE},一个空闲CPU的\texttt{scheduler}运行它必须是安全的;这意味着\texttt{p->context}必须持有这个进程的寄存器(即,它们并不在真正的寄存器中),并且没有CPU正在这个进程的内核栈上执行,并且没有CPU的\texttt{c->proc}指向这个进程。
\texttt{p->lock}被持有时这些条件通常并不成立。

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

0 comments on commit 0d9914f

Please sign in to comment.