重读 xv6(II)
xv6 进程机制
三 #
我们先分析进程机制的实现,源码 proc.c
创建进程 #
userinit 本质上是新建 init 进程,再通过 scheduler 执行该进程
我们调用 allocproc() 来请求进程资源,为什么使用“请求”这个字眼呢?
xv6 的进程资源是预先分配的,我们会维护一个固定的 proctable,根据状态来分配资源
对于一个进程,我们需要维护以下信息:
state、pid、parentid等 metadatatrapframe,用于切换syscall内核态pagetable,用于虚拟内存context,用于调度 CPU
allocproc 的流程:
- 遍历
proctable,取出UNUSED的空闲进程 - 申请
pagetable,初始化页表映射与trap - 初始化
context,用于进程调度
而 freeproc 把申请的资源全部释放即可
切换进程 #
设计进程抽象的动机是 CPU 调度,而 scheduler 依赖的核心机制就是 上下文切换
每个 CPU 都在不间断的运行调度器 scheduler,收到中断时调用 yield 让出 CPU 进行调度
这一逻辑由三层构成:
scheduler:找到状态为RUNNABLE的进程sched:proc 与scheduler间切换的胶水函数swtch:保存与替换寄存器内容,切换程序流
swtch切换了%ra寄存器的内容,改变了ret的返回地址,因此结束后 CPU 会在新进程上运行
程序是一个状态机,swtch 的威力就在于无缝替换了这个状态机的上下文
同时 scheduler 与 sched 本身作为函数,既发挥了临界区的作用,又为重定向 ret 积攒了势能
这样的设计为进程提供了独占 CPU 的假象——swtch结束后,寄存器等资源不变,程序会从之前 trap 的地方继续执行,整个调度过程对用户完全透明
要注意到,我们已经假设 swtch 到是一个执行到一半的旧进程,逻辑上必须存在第二个进程
可是创建进程时,它可从来没有被执行过,如何设置新进程的上下文呢?
我们选择捏造一个进程 forkret,假装在此之前一直是它在工作,调度后会先从 forkret 执行
而 forkret 什么都不做,只是 trap 回到用户态,从新进程的第一条指令开始执行
实际上每个 proc 都是先 trap 到内核态,在 scheduler 内部被踢走的,所以 forkret 才要调用 usertrap
主动调度 #
等待 lock 时,进程可能会主动选择休眠,让出 CPU,此时状态转为 SLEEPING
这一套 sleep-wakeup 的逻辑,简单遍历 proctable 即可实现
进程树 #
每个进程都是 init proc 直接或间接 fork 得到的,这也是我们在 I 中称其为 “dummy porc” 的原因,它是所有进程的祖先
这里我们只需要讨论三个函数:
wait循环扫描proctable,等待指定的子进程变为ZOMBIE,返回并释放结束的子进程kill标记进程应该被删除,没有立刻删除的能力,需要内核程序不间断地进行自我检查exit关闭进程,并把状态标记为ZOMBIE,等待上层清理
注意到一个有趣的事实,exit 并不负责释放资源,而是交给 parent 进程释放,同时 exit 会将剩余的子节点全部移交给 init
exit 必须获取锁,防止父子进程同时死亡,否则就没人给
child proc收尸了
因此 init proc 成为了事实上的垃圾回收者,循环执行 wait 以释放无主的 proctable 资源
这一节,我们回答了以下问题:
- 进程是如何被创建的?
- OS 怎么将调度过程透明化的?
- OS 如何自动管理进程资源?