2. RISC-V 页表机制:Sv32/Sv39/Sv48 页表格式、多级页表遍历过程、PTE 结构解析
页表,说白了就是虚拟地址到物理地址的「翻译官」。CPU 发出的虚拟地址,得靠它才能找到真正的物理内存位置。RISC-V 的页表设计很有意思,它不像 x86 那样固定四级,而是给了你 Sv32、Sv39、Sv48 三种选择。选哪个,取决于你的物理地址空间有多大。
我个人习惯把页表机制比作「俄罗斯套娃」——一级套一级,直到找到真正的物理页。嗯,咱们今天就一层层把它拆开看。
2.1 三种页表模式:Sv32 / Sv39 / Sv48
RISC-V 通过 satp 寄存器里的 MODE 字段来选择页表模式。这三种模式的核心区别,说白了就是「能管多大的地址空间」和「需要几级页表」。
| 模式 | 虚拟地址宽度 | 物理地址宽度 | 页表级数 | 最大虚拟空间 |
|---|---|---|---|---|
| Sv32 | 32 位 | 34 位 | 2 级 | 4 GiB |
| Sv39 | 39 位 | 56 位 | 3 级 | 512 GiB |
| Sv48 | 48 位 | 56 位 | 4 级 | 256 TiB |
2.2 PTE 结构解析:页表项里到底藏了什么?
页表项(PTE)是页表的基本单元。每个 PTE 占 8 字节(64 位),里面包含了物理页号(PPN)和一堆控制位。我刚开始看 RISC-V 手册时,觉得这些位挺乱的,后来画了个图才理清楚。
咱们以 Sv39 为例,PTE 的布局是这样的:
63 54 53 28 27 19 18 10 9 8 7 6 5 4 3 2 1 0
+----------+------------+------------+----------+-----+-+-+-+-+-+-+-+-+
| Reserved | PPN[2] | PPN[1] | PPN[0] | RSW |D|A|G|U|X|W|R|V|
+----------+------------+------------+----------+-----+-+-+-+-+-+-+-+-+
关键位说明:
- V(Valid):有效位。为 0 时,访问这个 PTE 会触发页错误。我踩过这个坑——页表没初始化好,V 位是 0,CPU 一跑就崩。
- R/W/X:读/写/执行权限。三者全为 0 时,表示指向下一级页表(非叶子节点)。
- U(User):用户模式可访问。为 0 时只有 S 模式能访问。
- A(Accessed):访问位。硬件在访问页表时会自动置 1。
- D(Dirty):脏位。页面被写入时置 1。这个位对 TLB 性能调优很重要,后面章节会细讲。
- RSW:留给操作系统用的两位。我习惯用它来标记页面的 NUMA 节点信息。
2.3 多级页表遍历过程:硬件是怎么找到物理地址的?
多级页表遍历,说白了就是「逐级索引,直到叶子」。我画了一张 Sv39 的遍历流程图,你看完就明白了。
遍历过程其实就三步:
- 取 VPN 索引:从虚拟地址中提取 VPN[2]、VPN[1]、VPN[0] 三个 9 位索引。
- 逐级查表:从
satp.ppn指向的根页表开始,用 VPN[2] 索引找到 L2 PTE。如果这个 PTE 的 R/W/X 全为 0,说明它指向下一级页表,继续用 VPN[1] 索引查 L1 页表。以此类推。 - 取物理地址:当遇到一个 R/W/X 不全为 0 的叶子 PTE 时,取出它的 PPN,加上虚拟地址的页内偏移,就得到了最终的物理地址。
2.4 Sv32 和 Sv48 的特殊之处
Sv32 和 Sv48 跟 Sv39 的核心逻辑一样,只是级数和地址宽度不同:
- Sv32:2 级页表,VPN 分成 VPN[1](10 位)和 VPN[0](10 位),页内偏移 12 位。物理地址 34 位,PPN 占 22 位。
- Sv48:4 级页表,VPN 分成 VPN[3] 到 VPN[0] 各 9 位。物理地址 56 位,PPN 占 44 位。
我记得有一次做 Sv48 的验证,发现页表遍历总是出错。查了半天,原来是 satp 寄存器的 MODE 字段没设对——设成了 Sv39,但虚拟地址高 9 位不是符号扩展,导致 VPN[3] 索引算错了。嗯,这种低级错误,犯过一次就再也不会忘了。
2.5 大页(Huge Page)的支持
RISC-V 的页表机制天然支持大页。当某个非叶子 PTE 的 R/W/X 不全为 0 时,它就是一个「大页 PTE」。比如在 Sv39 中:
- L2 级 PTE 如果权限位有效,就是一个 1 GiB 的大页。
- L1 级 PTE 如果权限位有效,就是一个 2 MiB 的大页。
2.6 页表遍历的性能开销
多级页表遍历最大的问题就是慢。一次遍历需要多次内存访问:Sv32 要 2 次,Sv39 要 3 次,Sv48 要 4 次。如果每次遍历都走一遍,性能就崩了。
这就是 TLB 存在的意义——缓存最近用过的页表项,避免重复遍历。我做过一个测试:没有 TLB 时,一次内存访问平均要 100 多个周期;有了 TLB,命中时只需要 1-2 个周期。
所以,理解页表机制,是优化 TLB 的第一步。后面几章,我会带你看看 TLB 是怎么设计的,以及怎么让它命中率更高。