7.2 代码:上下文切换
图7.1概述了从一个用户进程(旧进程)切换到另一个用户进程(新进程)所涉及的步骤:一个到旧进程内核线程的用户-内核转换(系统调用或中断),一个到当前CPU调度程序线程的上下文切换,一个到新进程内核线程的上下文切换,以及一个返回到用户级进程的陷阱。调度程序在旧进程的内核栈上执行是不安全的:其他一些核心可能会唤醒进程并运行它,而在两个不同的核心上使用同一个栈将是一场灾难,因此xv6调度程序在每个CPU上都有一个专用线程(保存寄存器和栈)。在本节中,我们将研究在内核线程和调度程序线程之间切换的机制。
从一个线程切换到另一个线程需要保存旧线程的CPU寄存器,并恢复新线程先前保存的寄存器;栈指针和程序计数器被保存和恢复的事实意味着CPU将切换栈和执行中的代码。
函数swtch
为内核线程切换执行保存和恢复操作。swtch
对线程没有直接的了解;它只是保存和恢复寄存器集,称为上下文(contexts)。当某个进程要放弃CPU时,该进程的内核线程调用swtch
来保存自己的上下文并返回到调度程序的上下文。每个上下文都包含在一个struct context
(kernel/proc.h:2)中,这个结构体本身包含在一个进程的struct proc
或一个CPU的struct cpu
中。Swtch
接受两个参数:struct context *old
和struct context *new
。它将当前寄存器保存在old
中,从new
中加载寄存器,然后返回。
让我们跟随一个进程通过swtch
进入调度程序。我们在第4章中看到,中断结束时的一种可能性是usertrap
调用了yield
。依次地:Yield
调用sched
,sched
调用swtch
将当前上下文保存在p->context
中,并切换到先前保存在cpu->scheduler
(kernel/proc.c:517)中的调度程序上下文。
注:当前版本的XV6中调度程序上下文是
cpu->context
Swtch
(kernel/swtch.S:3)只保存被调用方保存的寄存器(callee-saved registers);调用方保存的寄存器(caller-saved registers)通过调用C代码保存在栈上(如果需要)。Swtch
知道struct context
中每个寄存器字段的偏移量。它不保存程序计数器。但swtch
保存ra
寄存器,该寄存器保存调用swtch
的返回地址。现在,swtch
从新进程的上下文中恢复寄存器,该上下文保存前一个swtch
保存的寄存器值。当swtch
返回时,它返回到由ra
寄存器指定的指令,即新线程以前调用swtch
的指令。另外,它在新线程的栈上返回。
注:关于callee-saved registers和caller-saved registers请回看视频课程LEC5以及文档《Calling Convention》
[!NOTE] 这里不太容易理解,这里举个课程视频中的例子:
以
cc
切换到ls
为例,且ls
此前运行过
XV6将
cc
程序的内核线程的内核寄存器保存在一个context
对象中因为要切换到
ls
程序的内核线程,那么ls
程序现在的状态必然是RUNABLE
,表明ls
程序之前运行了一半。这同时也意味着:a.
ls
程序的用户空间状态已经保存在了对应的trapframe中b.
ls
程序的内核线程对应的内核寄存器已经保存在对应的context
对象中所以接下来,XV6会恢复
ls
程序的内核线程的context
对象,也就是恢复内核线程的寄存器。之后
ls
会继续在它的内核线程栈上,完成它的中断处理程序- 恢复
ls
程序的trapframe中的用户进程状态,返回到用户空间的ls
程序中- 最后恢复执行
ls
在我们的示例中,sched
调用swtch
切换到cpu->scheduler
,即每个CPU的调度程序上下文。调度程序上下文之前通过scheduler
对swtch
(kernel/proc.c:475)的调用进行了保存。当我们追踪swtch
到返回时,他返回到scheduler
而不是sched
,并且它的栈指针指向当前CPU的调用程序栈(scheduler stack)。