xv6 学习笔记:Lab Page tables
学习
- xv6 book 第 3 章
- kernel/vm.c, kernel/exec.c
- RISC-V privileged instructions
- RISC-V privileged architecture manual
页表
页表是现代操作系统用来管理内存的方式。通过页表,操作系统可以给每个进程独立的虚拟地址空间,并映射到物理内存上。这意味着同一个虚拟地址,对不同的进程,映射到的物理地址可能是不同的。
页是操作系统分配内存的基本单位,一页的大小为 4 KB,即 4096 字节。
每个进程有独立的虚拟地址空间,意味着每个进程要独立地保存一组虚拟地址到物理地址的映射表。这个映射表大小也刚好是一页,叫作页表。作为页表的页称为页表页。每个页表由 512 个 8 字节的 PTE (Page Table Element,页表项)组成,每个 PTE 包含了一个 PPN(Physical Page Number,物理页号,指向一个物理页),以及一些控制位(比如这一页是否有效、是否可读、是否可写)。如果读取 PTE 的有效位的时候发现该页无效,就不再读取物理页了,引发缺页异常。
到目前为止,已存在页表的情况下,进程要想通过一个虚拟地址访问精确到字节的内存数据,需要把虚拟地址分成两部分,一是 PTE 的索引,二是页内偏移量。每个页表都有 512 个 PTE,意味着可以用 log2 512 = 9 位表示使用哪个 PTE。使用 log2 4096 = 12 位表示页内偏移量。所以现在一个虚拟地址由 9 + 12 = 21 位组成,可以索引 221 = 2 MB 内存。
2 MB 对现代程序来说可能太小了。要增加程序可索引内存大小,只需要再增加一级页表。也就是说,原来程序拥有的那个页表,不再直接映射到实际存放数据的页,而是映射到另一些页表,这些页表才映射到存放数据的页。这样一来,虚拟地址就要增加 9 位用来索引中间这一层 PTE,于是程序就可以索引 230 = 1 GB 内存了。以此类推,存在更多级的页表:三级页表需要 39 位虚拟地址,可以索引 512 GB 内存;四级页表 48 位,256 TB;五级页表 57 位,128 PB。
satp
satp (supervisor address translation and protection) 寄存器,控制 S-mode 下的地址转换和保护。该寄存器存放了根页表的 PPN。MODE 指定了是否启用地址转换。启用地址转换前,内核代码访问的地址都是物理地址;启用地址转换后,不论是内核还是用户程序,访问的地址都是虚拟地址,如何映射由页表决定。xv6 在启用地址转换前会先运行一小段初始化代码,然后才启用地址转换。在 S-mode 和 U-mode 中 satp 才会激活。
地址转换
xv6 运行在 Sv39 RISC-V 上(Sv 指 Supervisor-level virtual-memory),意味着每个虚拟地址只用到 39 位,使用 9 * 3 = 27 位索引三级 PTE,使用 12 位索引页内偏移量。
xv6 为每个进程维护一个用于描述进程的用户地址空间的页表,外加一个单独的描述内核地址空间的页表。
xv6 内核的大部分地址是直接映射,除了 TRAMPLINE 页和内核栈页。
内核地址映射:
用户程序地址映射:
练习
Walk and print a two-level page table
用递归深入每一层页表。使用 riscv.h 文件结尾定义的宏和常量。位运算的时候注意整数的类型。
Detect which pages have been accessed
需要定义一个缓冲区来存放结果,然后 copyout 到用户程序的第三个参数。检查 PTE_A 是否已设置后,要清除 PTE_A。否则,将无法确定自上次调用 pgaccess() 以来是否访问了页面(即,该位将被永久设置)。
Speed up the getpid system call
模仿 TRAPFRAME 页是如何被创建和回收的。在程序分配了 pid 后,把 pid 也复制到 USYSCALL 页里。
Use superpages
查阅 RISC-V privileged architecture manual 第 12.3.2 节(图片是 Sv32,不过 Sv39 也大同小异):
可以发现,大页(2M页)相比小页最主要的区别就是 pte 在 level=1 的时候就是叶子 pte 了。并且叶子 pte 的 ppn 的低 9 位必须是 0,意味着大页的物理地址必须 2M 对齐,否则会 page fault。
这个练习让我们把 kvmmake 函数里的映射都改成尽量使用大页。要映射大页,虚拟页和物理页都要 2M 对齐。具体方法是,不论虚拟地址还是物理地址,都按 2M 对齐的方式找到一块最大的连续地址空间,然后取二者中的较小的大小,就得到了最多多少字节能使用大页。再按 2M 对齐计算哪些地址用大页。其他地址用小页。
如果不正确设置内核页表,会发生十分有趣的现象。我之前没注意到设置大页时,物理页也要 2M 对齐,否则使用页表进行地址转换的时候,会引发 page fault。关键在于,page fault 发生在哪里?我写的第一版代码运行后会卡在“xv6 kernel is booting”之后。我在每两条语句之间插入了 printk,这样我就知道具体是哪里卡住了。我注意到 kvminithart 函数调用 w_satp 之前的 printk 都打印出来了,后面的 printk 都没打印。我知道是我的代码引发了问题,但是很疑惑为什么会卡在写 satp 这条汇编语句上(甚至一度以为 RISC-V 在开启地址转换之后会检查页表是否正确,事实证明一条汇编语句做不了那么多事情)。后来发现原来是 printk 本身也被错误的页表影响了。printk 在启用地址转换之前,就已经可以通过物理地址访问硬件终端,从而打印内核消息了。启用地址转换之后,printk 访问的地址都是虚拟地址了。地址转换后运行的第一段代码就是 printk,所以 page fault 发生在 printk 内部!调试的工具本身被 bug 影响了。