MIT 6.1810: xv6 book Chapter8: Scheduling 笔记

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 进程(用户态根进程)从内核态开始执行的起点