7.2 代码:上下文切换

img

图7.1概述了从一个用户进程(旧进程)切换到另一个用户进程(新进程)所涉及的步骤:一个到旧进程内核线程的用户-内核转换(系统调用或中断),一个到当前CPU调度程序线程的上下文切换,一个到新进程内核线程的上下文切换,以及一个返回到用户级进程的陷阱。调度程序在旧进程的内核栈上执行是不安全的:其他一些核心可能会唤醒进程并运行它,而在两个不同的核心上使用同一个栈将是一场灾难,因此xv6调度程序在每个CPU上都有一个专用线程(保存寄存器和栈)。在本节中,我们将研究在内核线程和调度程序线程之间切换的机制。

从一个线程切换到另一个线程需要保存旧线程的CPU寄存器,并恢复新线程先前保存的寄存器;栈指针和程序计数器被保存和恢复的事实意味着CPU将切换栈和执行中的代码。

函数swtch为内核线程切换执行保存和恢复操作。swtch对线程没有直接的了解;它只是保存和恢复寄存器集,称为上下文(contexts)。当某个进程要放弃CPU时,该进程的内核线程调用swtch来保存自己的上下文并返回到调度程序的上下文。每个上下文都包含在一个struct contextkernel/proc.h:2)中,这个结构体本身包含在一个进程的struct proc或一个CPU的struct cpu中。Swtch接受两个参数:struct context *oldstruct context *new。它将当前寄存器保存在old中,从new中加载寄存器,然后返回。

让我们跟随一个进程通过swtch进入调度程序。我们在第4章中看到,中断结束时的一种可能性是usertrap调用了yield。依次地:Yield调用schedsched调用swtch将当前上下文保存在p->context中,并切换到先前保存在cpu->schedulerkernel/proc.c:517)中的调度程序上下文。

注:当前版本的XV6中调度程序上下文是cpu->context

Swtchkernel/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此前运行过

  1. XV6将cc程序的内核线程的内核寄存器保存在一个context对象中

  2. 因为要切换到ls程序的内核线程,那么ls 程序现在的状态必然是RUNABLE ,表明ls程序之前运行了一半。这同时也意味着:

    a. ls程序的用户空间状态已经保存在了对应的trapframe中

    b. ls程序的内核线程对应的内核寄存器已经保存在对应的context对象中

    所以接下来,XV6会恢复ls程序的内核线程的context对象,也就是恢复内核线程的寄存器。

  3. 之后ls会继续在它的内核线程栈上,完成它的中断处理程序

  4. 恢复ls程序的trapframe中的用户进程状态,返回到用户空间的ls程序中
  5. 最后恢复执行ls

在我们的示例中,sched调用swtch切换到cpu->scheduler,即每个CPU的调度程序上下文。调度程序上下文之前通过schedulerswtchkernel/proc.c:475)的调用进行了保存。当我们追踪swtch到返回时,他返回到scheduler而不是sched,并且它的栈指针指向当前CPU的调用程序栈(scheduler stack)。