Multiplexing
xv6在两种情况下切换CPU的进程实现多路复用:
1.进程的阻塞式系统调用(read, wait...),主动切换voluntary scheduling
2.周期性强制切换运行很久却不阻塞的进程(等待IO,等待另一个进程向pipe写数据),抢占式调度pre-emptive scheduling
Code: Context switching
一个线程的状态 = 内存中的数据 + CPU寄存器数据
内存中的数据被保存在内存的不同区域中不用单独保存(变量,堆中的数据)
但CPU只有一套寄存器,必须在切换线程时保存并更新
swtch()只保存了callee saved register,因为调用swtch()函数时,caller saved register会被C编译器保存在当前栈上
swtch.S最后的ret返回到切换后的进程在从前某个时刻调用swtch()放弃CPU的状态,切换后的进程在自己的栈上继续执行之前的指令
Code: Scheduling
xv6的每个CPU都有自己的调度器线程scheduler thread ,负责运行scheduler()函数,挑选下一个运行的进程
进程切换必然通过调度器线程,而不是直接切换
在yield sleep kexit等函数实现中,一个正要放弃CPU的进程必须持有它自己的线程锁p->lock,释放其他锁,更新它的状态p->state,然后调用sched,sched调用swtch()保存当前进程的寄存器到p->context,切换到cpu->context,swtch返回到调度器的栈上
schduler循环查找p->state == RUNNABLE的进程,swtch()切换到它,最终swtch()返回调度器线程,继续循环查找
这里关于p->lock的代码可以看到:acquire操作在swtch之前,release操作在swtch之后,二者不在同一线程中实现
这里的p->lock可保护三个步骤的原子性:
1.进程的状态从RUNNING切换为RUNNABLE
2.进程的寄存器全部保存在context中
3.停止使用当前进程的栈
Real world
xv6使用的调度算法是轮转round robin
现实操作系统中还有优先级调度等等算法
swtch() first called
第一次调用swtch()时,如fork以及系统启动时的第一个进程,会调用allocproc()
c
// File: proc.c
memset(&p->context, 0, sizeof(p->context));
p->context.ra = (uint64)forkret;
p->context.sp = p->kstack + PGSIZE;
这里设置好了ra为forkret,即PID=1 的 init 进程(用户态根进程)从内核态开始执行的起点