multiplexing
xv6的进程调度通过两个机制实现:程序通过调用sleep/wakeup进行的自主切换 和 定时中断驱动的强制切换
要进行对用户进程透明的方式进行进程切换,就需要进行上下文切换,而为了维护切换过程中的不变量(invariant),还需要在适当的时候加锁
xv6中上下文切换如图:
用户进程trap到该进程的内核线程(内核态),然后进行上下文切换(这里的切换,和前面的切换,区别在于,后者是通过函数调用switch函数,而前者不是,所以后者切换时只需要保存callee-saved register和ra,sp,而前者需要保存全部的寄存器)到cpu的调度器线程,调度器线程选出下一个要运行的进程,上下文切换到该进程的内核栈,然后trap return到该进程的用户线程,这样,完成了一个进程到另一个进程的切换
code:context switching
上下文切换需要保存原进程的寄存器(内存不用管,因为不同进程的内存不会重叠,他们映射到不同区域),我们需要保存的是callee-saved寄存器,以及返回地址ra,栈顶指针sp,根据riscv的函数调用约定,他们是:
// Saved registers for kernel context switches.
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;
};
真正执行保存恢复寄存器的是switch.S,他有两个参数,分别指向旧进程context和新进程context(保存在a0,a1里)
swtch:
sd ra, 0(a0)
sd sp, 8(a0)
sd s0, 16(a0)
sd s1, 24(a0)
sd s2, 32(a0)
sd s3, 40(a0)
sd s4, 48(a0)
sd s5, 56(a0)
sd s6, 64(a0)
sd s7, 72(a0)
sd s8, 80(a0)
sd s9, 88(a0)
sd s10, 96(a0)
sd s11, 104(a0)
ld ra, 0(a1)
ld sp, 8(a1)
ld s0, 16(a1)
ld s1, 24(a1)
ld s2, 32(a1)
ld s3, 40(a1)
ld s4, 48(a1)
ld s5, 56(a1)
ld s6, 64(a1)
ld s7, 72(a1)
ld s8, 80(a1)
ld s9, 88(a1)
ld s10, 96(a1)
ld s11, 104(a1)
ret
switch.S的内容很简单,即根据结构体不同成员变量的偏移量(都是uint64,所以都是相隔8个字节)进行保存,加载
值得注意的是,最后面的ret,它将ra的内容设置为pc,注意这里的ra是要切换到的进程之前执行switch的时候保存的ra(可以看到基地址分别时a0和a1,对应第一第二个函数参数),也就是此次执行完switch之后,不是接着当前位置的下一行运行,而是另一处switch的下一行运行,举xv6的做法为例说明:
xv6中只有两处调用switch:
void
sched(void)
{
....
swtch(&p->context, &mycpu()->scheduler);
....
}
void
scheduler(void)
{
...
swtch(&c->scheduler, &p->context);
...
}
可以看出这里没有两个用户进程之间的直接切换,只有用户进程和调度器线程之间的切换:
xv6中要主动让出cpu的进程都是通过调用exit