Xv6 笔记(一):隔离
当一个应用程序执行出错时,我们不希望操作系统或其他不相关的程序因此也出错,相反,操作系统应该要能发现并清理掉出错的程序,并继续运行其他程序。要实现有效的隔离首先要求操作系统和应用程序之间有明确的界限,普通程序不能写(甚至读)系统的数据结构和指令,也不能访问其他进程的内存
CPU 为隔离提供了硬件上的支持。例如 RISC-V 指令集提供了下列三种指令执行模式:
- 机器模式(machine mode):有完整的执行权限,主要用于启动时初始化计算机配置
- 管理员模式(supervisor mode):可以执行特权指令(privileged instruction),例如开关中断、读写一些特定的寄存器等。如果运行在用户模式下的执行特权指令,CPU 不会执行,而是会切到管理员模式并在管理员模式下杀掉试图越界的程序。运行在管理员模式(或者说内核空间)的软件称为内核
- 用户模式(user mode):程序代码只能执行用户模式指令,CPU 提供了
ecall指令来让用户模式下的程序切换到管理员模式并进入内核指定的入口
Xv6 整个操作系统运行都在管理员模式下(宏内核),而普通程序只能运行在用户模式,这样,通过 CPU 指令执行模式的限制就可以确保系统和程序间的隔离,程序只能通过内核提供的系统调用接口使用内核提供的服务,例如 fork、read、wirte 等,内核可以先检查确保参数和程序权限合法,再真正调用服务

和其它 Unix 操作系统一样,进程也是 xv6 的基本隔离单位。为帮助实现程序间隔离,进程机制为每个程序制造了它有一整台私有计算机的假象,程序”拥有“一个私有的内存系统(或者叫地址空间)和一个单独的 CPU 来执行它的指令。前者通过硬件提供的页表实现,RISC-V 使用页表把虚拟地址(RISC-V 指令操作的地址)映射到物理内存的地址上,xv6 为每个进程维护独立的页表,构成进程的地址空间
Xv6 所运行的 Sv39 模式使用 39 位的虚拟地址(64 位地址的前 25 位未使用),地址被分为三个 9 位的 VPN(virtual page number)和一个 12 位的页偏移量

存储在物理内存中的页表是一棵三层的树,树的根节点是一个 4096 字节的页表页(大小与物理内存中的页对齐),包含 29 个 8 字节的 PTE(page table entry),每个 PTE 包含指向下一层页表页的物理地址(和低 10 位描述 PTE 信息的标记位),第二层的每个页表页又包含 29 个指向第三层页表页的 PTE。页表硬件使用虚拟地址的高九位 VPN[2] 作为索引从根节点得到一个指向第二层页表页的地址,然后用 VPN[1] 从第二层获取第三层页表页的地址,最后通过 VPN[0] 选中最后的 PTE,从中获取 44 位的物理地址,拼上虚拟地址最后 12 位的偏移量,得到一个 56 位的物理地址,就是这个虚拟地址所映射的物理地址

每个 CPU 都有各自的 satp 寄存器(supervisor address translation and protection register),当把页表树根节点的物理地址写到 satp 后,CPU 后续执行的所有指令中的地址都会用这个页表来转换
当用户进程的内存分配器(例如 malloc)通过 sbrk 申请内存时,内核会按上述转化逻辑依次访问(或生成,如果没有的话)各层对应的 PTE,把新申请到的物理内存的地址保存到第三层的 PTE 里,然后向用户进程返回该虚拟地址;当用户进程访问某个虚拟地址时,如果地址转化过程中遇到的任意一个 PTE 不存在或一些标记与访问类型不符—-即程序访问了一个非法地址,分页硬件就会抛出页错误异常(page-fault)
这种三层结构允许页表懒加载页表页,很大范围内的虚拟地址的页表页没被访问就不会生成出来
通过页表,每个进程都拥有完整连续的虚拟地址(0~MAXVA),而实际的物理地址可以是不连续的;同时页表还可以把同一片物理内存映射到不同地址空间,在不同进程间复用同一份代码(例如每个进程用户地址空间都有的 trampoline 页)。内核有单独的页表,不同进程也有各自的页表,把虚拟地址转换成不同的物理内存页地址,从而实现程序间内存的隔离