Professordeng's Blog

XV6(与其他 Unix 操作系统一样)中的隔离单元是一个进程。进程抽象防止一个进程破坏或监视另一个进程的内存、CPU、文件描述符等。它还防止一个进程破坏内核本身。内核必须小心地实现进程抽象,因为错误或恶意应用程序可能欺骗内核或硬件做一些不好的事情(例如,绕过强制隔离)。内核用于实现进程的机制包括用户 / 内核模式的标志、地址空间和线程的时间切片。

进程是一个抽象概念,它让一个程序可以假设它独占一台机器。进程向程序提供 “看上去” 私有的,其他进程无法读写的内存系统(或地址空间),以及一颗 “看上去” 仅执行该程序的 CPU。

XV6 使用页表(由硬件实现)来为每个进程提供其独有的地址空间。页表将虚拟地址(x86 指令所使用的地址)翻译(或说“映射”)为物理地址(处理器芯片向主存发送的地址)。

1-2

figure1-2

XV6 为每个进程维护了不同的页表,这样就能够合理地定义进程的地址空间了。如图表 1-1所示,一片地址空间包含了从虚拟地址 0 开始的 “用户内存”。它的地址最低处放置进程的指令,接下来则是全局变量,栈区,以及一个用户可按需拓展的 “堆” 区(malloc 用)。

和上面提到的 “用户内存” 一样,内核的指令和数据也会被进程映射到每个进程的地址空间中。当进程使用系统调用时,系统调用实际上会在进程地址空间中的内核区域执行。这种设计使得内核的系统调用代码可以直接指向用户内存。为了给用户留下足够的内存空间,XV6 将内核映射到了地址空间的高地址处,即从 0x80100000 开始。

XV6 使用 proc 结构体来维护一个进程的状态,其中最为重要的状态是进程的页表,内核栈,当前运行状态。我们接下来会用 p->xxx 来指代 proc 结构中的元素。

每个进程都有一个运行线程(或简称为 “线程”)来执行进程的指令。线程可以被暂时挂起,稍后再恢复运行。系统在进程之间切换实际上就是挂起当前运行的线程,恢复另一个进程的线程。线程的大多数状态(局部变量和函数调用的返回地址)都保存在线程的栈上。

每个进程都有用户栈和内核栈(p->kstack)。当进程运行用户指令时,只有其用户栈被使用,其内核栈则是空的。然而当进程(通过系统调用或中断)进入内核时,内核代码就在进程的内核栈中执行;进程处于内核中时,其用户栈仍然保存着数据,只是暂时处于不活跃状态。进程的线程交替地使用着用户栈和内核栈。要注意内核栈是用户代码无法使用的,这样即使一个进程破坏了自己的用户栈,内核也能保持运行。

当进程使用系统调用时,处理器转入内核栈中,提升硬件的特权级,然后运行系统调用对应的内核代码。当系统调用完成时,又从内核空间回到用户空间:降低硬件特权级,转入用户栈,恢复执行系统调用指令后面的那条用户指令。线程可以在内核中 “阻塞”,等待 IO, 在 IO 结束后再恢复运行。

p->state 指示了进程的状态:新建、准备运行、运行、等待 IO 或退出状态中。

p->pgdir 以 X86 硬件要求的格式保存了进程的页表。XV6 让分页硬件在进程运行时使用 p->pgdir。进程的页表还记录了保存进程内存的物理页的地址。