MIT 6.S081 Thread switching

发布时间 2023-07-20 10:43:44作者: zwyyy456

Multiplexing

xv6 通过将 cpu 从一个进程切换到另一个进程来实现 multiplex(多路复用),进程的切换会在两种情形下发生:

  1. xv6 的 sleep 与 wakeup 机制在进程等待 IO 完成或者等待子进程退出又或者在 sleep 系统调用中等待的时候切换进程。
  2. xv6 会周期性地强制切换进程,从而应对那些长时间切换而未 sleep 的进程。

这个 multiplex 机制会让进程产生一种自己完全拥有 cpu 的错觉,就像 xv6 用虚拟内存和 page table 机制让进程觉得自己拥有完整的内存空间一样。

xv6 使用硬件定时器中断来保证 context switch(上下文切换)。

Code: Context switching

用户进程之间的切换步骤如下图所示:

b41nAVjQsSLyDBv

用户进程之间的切换其实会经过两次 context switch,以上图为例,第一次是从 shell 用户进程的 kernel thread 切换到 cpu 的 scheduler thread;第二次从 cpu 的 scheduler thread 切换到新用户进程(例如 cat)的 kernel thread。

在 xv6 中,我们可以认为每个用户进程,包含一个内核线程与一个用户线程,然后每个 cpu 包含一个 scheduler thread,schedular thread 工作在内核中,有只属于它的 kernel stack。

swtch 执行为内核线程切换的保存和恢复工作。swtch 的主要工作就是保存和恢复 riscv 的寄存器,又被称为上下文。

当一个进程要让出 cpu 时,该进程的内核线程会调用 swtch 来把该进程的 context 保存起来,并返回到 scheduler 线程的 context,每个 context 都包含于 strcut contextcontext 结构体可能是 struct proc 的成员或者 struct cpu 的成员( scheduler 线程)。

struct context {
  uint64 ra;
  uint64 sp;
  // callee-saved
  uint64 s0;
  uint64 s1;
  uint64 s2;
  uint64 s3;
  uint64 s4;
  uint64 s5;
  uint64 s6;
  uint64 s7;
  uint64 s8;
  uint64 s9;
  uint64 s10;
  uint64 s11;
};

swtch 函数实际上是利用汇编实现的,它接受两个参数 struct context *oldstruct context *new,将当前寄存器保存在 old,中然后从 new 中加载内容到当前寄存器,然后 return

用户进程要让出 cpu,切换到 scheduler 线程,会经历 usertrap => yield => sched => swtch 的过程,swtch 把当前进程的上下文保存在 p->context 中,从 cpu->context 加载当前 cpu 的 scheduler 线程的 context。

swtch 只会保存 callee-saved 寄存器(这里 sched 是 caller, swtch 是 callee)。swtch 不保存 pc,而是保存 ra 寄存器(ra 寄存器存放了 swtch 调用结束后应该返回到的地址,可以理解为 swtch 语句的下一条语句)。

scheduler 的 context 是在 scheduler 线程调用 swtch 的时候被保存在 cpu->context 的。

进程切换的流程可以这么理解,进程 a 调用 swtch,将进程 a 的 context 保存在 proca->context(包括进程 a 的 ra 寄存器),从 cpu->context 中加载 context,由于 context 中包含了 ra 寄存器,而 swtch 函数的最后一条指令就是 ret,因此会跳转到 ra 寄存器的地址处继续执行,这里应该就是执行 scheduler 函数中的 c->proc = 0,由于 scheduler 本身是个死循环,c->proc = 0 的下一个语句就是一个新的 for 循环,就又会执行到 swtch,将 scheduler 线程的 context 保存在 cpu->context 中,然后从 procb->context 中加载 context(包括进程 b 的 ra 寄存器),然后跳转到进程 b 的 ra 寄存器的地址处继续执行(这里类比进程 a,就是进程 b 调用的 swtch 语句的下一条语句),这里从结果来看,就是让进程 b 会从系统调用或者中断响应中退出,继续执行它的本职工作。

Code: Scheduling

scheduler 的实现如下:

void scheduler(void) {
    struct proc *p;
    struct cpu *c = mycpu();

    c->proc = 0;
    for (;;) {
        // Avoid deadlock by ensuring that devices can interrupt.
        intr_on();

        for (p = proc; p < &proc[NPROC]; p++) {
            acquire(&p->lock);
            if (p->state == RUNNABLE) {
                // Switch to chosen process.  It is the process's job
                // to release its lock and then reacquire it
                // before jumping back to us.
                p->state = RUNNING;
                c->proc = p;
                swtch(&c->context, &p->context);

                // Process is done running for now.
                // It should have changed its p->state before coming back.
                c->proc = 0;
            }
            release(&p->lock);
        }
    }
}

我们可以发现,在调用 swtch 时,xv6 持有 &p->lock,但是这个锁,会在 scheduler 线程中被解锁

彼此之间有意识地通过线程传递 cpu 控制权的进程有时候也被称作协程(coroutines)schedscheduler 就可以看作是彼此的协程。

There is one case when the scheduler’s call to swtch does not end up in sched. allocproc sets the context ra register of a new process to forkret (kernel/proc.c:508), so that its first swtch “returns” to the start of that function. Forkret exists to release the p->lock; otherwise, since the new process needs to return to user space as if returning from fork, it could instead start at usertrapret.