MIT 6.S081 pgtbl 前置篇(lab3):xv6 中的页表是什么东西 ?

发布时间:2026/7/23 8:55:36
MIT 6.S081 pgtbl 前置篇(lab3):xv6 中的页表是什么东西 ? 开篇我第一次看下面这段话的时候非常痛苦还贴了两张图片(我后面会给出)让我更痛苦了现在好多了RISC-V指令用户和内核指令使用的是虚拟地址而机器的RAM或物理内存是由物理地址索引的。RISC-V页表硬件通过将每个虚拟地址映射到物理地址来为这两种地址建立联系。XV6基于Sv39 RISC-V运行这意味着它只使用64位虚拟地址的低39位而高25位不使用。在这种Sv39配置中RISC-V页表在逻辑上是一个由2272^{27}227个页表条目Page Table Entries/PTE组成的数组每个PTE包含一个44位的物理页码Physical Page Number/PPN和一些标志。分页硬件通过使用虚拟地址39位中的前27位索引页表以找到该虚拟地址对应的一个PTE然后生成一个56位的物理地址其前44位来自PTE中的PPN其后12位来自原始虚拟地址。图3.1显示了这个过程页表的逻辑视图是一个简单的PTE数组参见图3.2进行更详细的了解。页表使操作系统能够以 4096 (2122^{12}212) 字节的对齐块的粒度控制虚拟地址到物理地址的转换这样的块称为页page。先搞懂最基础的单位页Page分页机制的核心思想是把内存切成固定大小的小块以块为单位做地址映射这个小块就叫「页」。xv6 里一页固定是 4096 字节4KB也就是2122^{12}212字节。对应到地址上一页内的地址偏移需要 12 位二进制才能完全表示 (0~4095)这 12 位就叫页内偏移地址翻译时这 12 位完全不变虚拟地址的低 12 位直接就是物理地址的低 12 位。可以把一页理解成一栋楼的「楼层」页内偏移就是「房间号」。翻译地址只需要找到对应的楼层房间号直接照搬就行。Sv39 到底是什么39 位虚拟地址怎么拆Sv39 的全称是 「RISC-V 39 位虚拟地址分页方案」虚拟地址虽然是 64 位寄存器存储但只有低 39 位是有效的高 25 位不用(实际要求做符号扩展)整个虚拟地址空间大小是239512GB2^{39} 512 GB239512GB对教学内核来说完全够用39 位有效地址扣掉低 12 位的页内偏移剩下27 位是虚拟页号VPN用来在页表里查找对应的物理页号。两种视图逻辑视图 vs 实际硬件视图逻辑视图线性页表为了方便理解概念可以把页表想象成一个超大的一维数组数组长度 2272^{27}227个条目PTE正好对应 27 位虚拟页号(VPN)虚拟页号(VPN)直接当数组下标就能查到对应的物理页号这个模型理解起来最简单但有个致命问题太浪费内存。每个 PTE 占 8 字节一整张线性页表就要227×81GB2^{27} × 8 1 GB227×81GB。一个进程就占 1GB 页表现实中根本不可能。实际硬件三级页表xv6 实际使用为了节省内存硬件把 27 位虚拟页号(VPN) 拆成了3 段每段 9 位对应三级页表层级位数对应虚拟地址段单级页表大小L2根页表 / 最高级9 位第 30 ~ 38 位292^929 512 个 PTE共 4KB正好占 1 页L1中间级9 位第 21 ~ 29 位同上512 个 PTE4KB 一页L0最低级 / 叶子级9 位第 12 ~ 20 位同上512 个 PTE4KB 一页每级页表正好 512 个条目 × 8 字节 4096 字节刚好放进一个物理页里。这不是巧合是专门设计的。比喻线性页表 一本写满所有地址的超级厚电话本大部分页面都是空的非常占地方。三级页表 省→市→街道三级目录。没人住的街道就不用建目录页大大节省空间。只有真正用到的地址区域才会分配对应的页表页。页表条目PTE里存了什么每个 PTE 是一个 64 位的整数核心分两部分高 44 位物理页号PPN物理地址总共 56 位扣掉低 12 位页内偏移剩下 44 位就是物理页号。翻译地址时把这里的 PPN 拿出来拼上虚拟地址的低 12 位偏移就得到了完整的 56 位物理地址。低 10 位标志位用来控制这个页的权限和状态xv6 常用的标志位有 6 个标志位含义PTE_V (第 0 位)有效位。为 1 表示这个 PTE 可用为 0 表示空条目PTE_R (第 1 位)可读权限PTE_W (第 2 位)可写权限PTE_X (第 3 位)可执行权限PTE_U (第 4 位)用户态可访问。为 0 的话只有内核态能访问这个页PTE_A / PTE_D访问位 / 脏位记录页面是否被读写过用于置换算法关键规则如果 PTE_V 是 0后面的 PPN 和其他标志位全部无效代表这个虚拟页没有映射到物理内存。完整的地址翻译过程硬件自动完成CPU 拿到一个虚拟地址后会自动通过页表完成翻译整个过程不需要软件干预具体分 4 步拿到根页表地址CPU 里有个专门的寄存器satp里面存着当前进程根页表L2 级的物理页号(PPN)。每个进程有自己独立的页表进程切换时内核会修改satp。查 L2 级页表取出虚拟地址的 L2 索引第 30~38 位共 9 位作为下标去根页表里找对应的 PTE。如果 PTE 无效V0触发缺页异常page fault交给内核处理如果有效从 PTE 中取出 L1 级页表的物理地址查 L1 级页表取出虚拟地址的 L1 索引第 21~29 位去 L1 页表里找下一级 PTE。同样判断有效性有效则取出 L0 级页表的物理地址。查 L0 级页表得到最终物理地址取出虚拟地址的 L0 索引第 12~20 位去 L0 页表里找到叶子 PTE。有效则取出 44 位物理页号 PPN把 PPN 左移 12 位拼上虚拟地址的低 12 位页内偏移得到最终的 56 位物理地址整个过程由 MMU内存管理单元硬件自动完成对程序完全透明。对应 xv6 内核源码的关键定义这些定义都在 kernel/riscv.h 里做实验时会频繁用到和上面的概念一一对应// kernel/riscv.h// 页大小与偏移#definePGSIZE4096// 一页4096字节#definePGSHIFT12// 页内偏移占12位// 提取每一级页表的索引9位#definePXMASK0x1FF// 9位掩码 0b111111111#definePXSHIFT(level)(PGSHIFT(9*(level)))#definePX(level,va)((((uint64)(va))PXSHIFT(level))PXMASK)// PTE 标志位#definePTE_V(1L0)// 有效#definePTE_R(1L1)// 可读#definePTE_W(1L2)// 可写#definePTE_X(1L3)// 可执行#definePTE_U(1L4)// 用户态可访问// 从 PTE 中提取物理页号 / 把物理页号打包成 PTE#definePTE2PA(pte)(((pte)10)12)#definePA2PTE(pa)((((uint64)pa)12)10)注意高 25 位不是随便写的Sv39 要求虚拟地址的第 38 位是符号位第 39~63 位必须和第 38 位保持一致符号扩展。如果随便填高位硬件会直接报地址错误。xv6 的用户地址空间从 0 开始内核地址空间从 0xffffffe000000000 开始就是为了符合符号扩展规则。页表本身存在物理内存里页表不是什么特殊的硬件结构就是普通的物理内存页内核在物理内存里分配页面来存放各级页表条目。satp里存的也是物理地址不是虚拟地址。三级页表是按需分配的不用的虚拟地址区域对应的中间级页表都不会分配这就是三级页表节省内存的核心原因。举个例子如果一个应用程序只使用了一个页面那么顶级页面目录将只使用条目0条目 1 到 511 都将被忽略因此内核不必为这511个条目所对应的中间页面目录分配页面也就更不必为这 511 个中间页目录分配底层页目录的页。 所以在这个例子中三级设计仅使用了三个页面共占用3×40963\times40963×4096个字节。