页表
页表的目的,在于将不连续的内存变成连续的内存地址空间。页表提供了一个内存地址的抽象,让原本可能并不连续的内存在页表的映射下变成了连续的内存。此外,页表还能够为一个页设置访问权限,并记录页面的访问读写情况。
总结来说,页表有着以下好处:
- 重新映射地址与内存页,提供内存地址抽象;
- 设置访问权限(包括读/写/执行/用户态可用/全局可用等);
- 记录页面访问/读写情况。
不过,我们注意到,如果我们要为每一个虚拟页建立映射,那么我们需要建立 \(2^{64-12}\) 个映射,这会耗费大量的内存,而且一个程序只会使用很少的一部分虚拟地址空间。多级页表就是用来解决这个问题的。多级页表会从先按照虚拟地址的高位,映射到下一级页表中,如果这个虚拟地址没有被使用,那么下一级页表就无需存在。这样,很多个映射被合并到一个更高层次的映射,内存被大大节约了。而且,由于使用了多级页表,每一张页表占用的空间也比原来少了很多(在后面的 SV39 下一张页表是一个页的大小),因此避免了需要找到连续大内存的问题。
硬件会将最高级的页表地址记录在一个特权寄存器上(在 RISCV 上是 satp
寄存器,全称是 supervisor address translation and protection register)。当硬件想要知道一个虚拟地址在何处时(不考虑 TLB),会依次读取各级页表,翻译出物理内存的位置。
SV39 多级页表机制
我们的内核使用的是 SV39 多级虚拟内存机制。SV39 采用三级页表映射,每一级控制 \(2^{9}\) 个表项,每个表项占用 8 bytes,所以一张页表(无论哪一级)占用了 4 KiB,也就是一个页的大小。下面是虚拟地址和物理地址的映射示意图:(其中 VPN
表示 virtual page number,PPN
表示 physical page number)
38 30 29 21 20 12 11 0
+-----------+-----------+------------+---------------+
| VPN[2] | VPN[1] | VPN[0] | page offset |
+-----------+-----------+------------+---------------+
55 30 29 21 20 12 11 0
+-----------------+-----------+------------+---------------+
| PPN[2] | PPN[1] | PPN[0] | page offset |
+-----------------+-----------+------------+---------------+
每一级页表控制 \(2^{9}\) 个表项,所以可以控制 9 bits 的地址空间。最后一级控制的是 4 KiB 的页面,所以我们一共可以控制 \(12 + 9 \times 3 = 39\) bits 的虚拟地址空间,SV39 的 39 就是这样来的。
关于页表项,请参见SV39 的页表项。
SV39 映射举例
假设我们现在有一个地址 0x7FFFFFFFFF
(也就是低 39 bits 都是 1,其他高位都是 0),我们来看看虚拟地址是如何映射的。
- 通过
satp
寄存器找到第一级页表; - 翻译 30-38 这几位,找到第二级页表,如果发现这个地址非法,那么停止翻译,进入异常处理流程;
- 翻译 21-29 这几位,找到第三级页表,如果发现地址非法,处理同上;
- 翻译 12-20 这几位,找到对应的物理的 12-55 位。
翻译完成的物理内存有两部分,一部分是刚刚翻译的 12-55 位,另一部分是页面上的偏移,即虚拟地址的 0-11 位。