2. RISC-V Sv39 虚拟地址格式:39位虚拟地址的划分与三级页表索引原理

在 xv6 内核中,RISC-V 架构采用 Sv39 分页方案管理 39 位虚拟地址空间。理解虚拟地址的位划分是掌握多级页表遍历与 TLB 刷新的基础。本章将详细解析 Sv39 虚拟地址的格式、三级页表索引的生成原理,以及硬件如何利用这些索引完成地址翻译。

2.1 Sv39 虚拟地址的位划分

Sv39 将 39 位虚拟地址(VA[38:0])划分为四个逻辑部分,用于三级页表遍历和页内寻址。高位 VA[63:39] 必须与 VA[38] 相同(符号扩展),否则硬件会触发页错误。

位域 位范围 宽度 名称 用途
VPN[2] VA[38:30] 9 位 一级页号 索引顶级页表(L2 页表)
VPN[1] VA[29:21] 9 位 二级页号 索引二级页表(L1 页表)
VPN[0] VA[20:12] 9 位 三级页号 索引三级页表(L0 页表)
Offset VA[11:0] 12 位 页内偏移 在物理页框内寻址字节
关键点:每个 VPN 字段均为 9 位,意味着每级页表最多包含 29 = 512 个页表项(PTE)。每个 PTE 大小为 8 字节,因此每级页表恰好占用一个 4 KiB 物理页。

2.2 三级页表索引原理

硬件(MMU)在翻译虚拟地址时,从顶级页表(L2)开始,逐级向下索引,直到找到最终的物理页框号(PPN)。索引过程完全由 VPN 字段驱动:

  1. L2 索引: 从 satp 寄存器读取根页表物理地址,加上 VPN[2] × 8 得到 L2 PTE 地址。读取 PTE,检查有效位(V)和读写权限。
  2. L1 索引: 从 L2 PTE 中提取下一级页表物理地址,加上 VPN[1] × 8 得到 L1 PTE 地址。读取并检查。
  3. L0 索引: 从 L1 PTE 中提取下一级页表物理地址,加上 VPN[0] × 8 得到 L0 PTE 地址。读取并检查。
  4. 物理地址合成: 从 L0 PTE 中提取物理页框号(PPN),拼接页内偏移(Offset)得到最终物理地址。
硬件遍历流程: 整个索引过程由 RISC-V 的 MMU 硬件自动完成,无需软件干预。软件(如 xv6 的 walk 函数)仅在建立或修改页表时模拟此过程。
Sv39 虚拟地址位划分与三级页表索引结构 Sv39 虚拟地址位划分与三级页表索引结构 39 位虚拟地址 (VA[38:0]) VPN[2] (9位) VA[38:30] VPN[1] (9位) VA[29:21] VPN[0] (9位) VA[20:12] Offset (12位) VA[11:0] 索引 索引 索引 页内偏移 L2 页表 (顶级) 512 个 PTE 由 satp 寄存器指向 VPN[2] 选择 PTE L1 页表 (二级) 512 个 PTE 由 L2 PTE 指向 VPN[1] 选择 PTE L0 页表 (三级) 512 个 PTE 由 L1 PTE 指向 VPN[0] 选择 PTE 物理页框 PPN 由 L0 PTE 提供 + Offset 得物理地址 说明: 每个 VPN 字段索引对应级别的页表,最终 L0 PTE 提供物理页框号(PPN),与 Offset 拼接得到 56 位物理地址。

2.3 页表项(PTE)格式与权限控制

每个页表项(PTE)为 64 位,包含物理页框号(PPN)和权限标志位。xv6 利用这些标志位实现用户/内核隔离、读写执行权限控制。

位域 位范围 标志 描述
V 0 Valid 页表项有效
R 1 Readable 页面可读
W 2 Writable 页面可写
X 3 Executable 页面可执行
U 4 User 用户模式可访问
G 5 Global 全局映射(不刷 TLB)
A 6 Accessed 页面已被访问
D 7 Dirty 页面已被写入
RSW 8-9 Reserved for SW 操作系统保留位
PPN 10-53 Physical Page Number 物理页框号(44 位)
Reserved 54-63 — 保留位,必须为 0
注意: 如果 PTE 的 R、W、X 均为 0,则该 PTE 指向下一级页表(非叶子节点)。xv6 的 walk 函数通过检查 (PTE_R | PTE_W | PTE_X) 判断当前 PTE 是否为叶子节点。

2.4 xv6 中的页表遍历实现

xv6 内核提供 walk 函数模拟硬件页表遍历过程,用于在软件中查找或创建虚拟地址对应的 PTE。以下是核心代码片段:

// 在页表 pagetable 中查找虚拟地址 va 对应的 PTE
// 如果 alloc 非零,则在缺失的中间页表层级分配新页
pte_t *
walk(pagetable_t pagetable, uint64 va, int alloc)
{
  if(va >= MAXVA)
    panic("walk");

  for(int level = 2; level > 0; level--) {
    pte_t *pte = &pagetable[PX(level, va)];
    if(*pte & PTE_V) {
      pagetable = (pagetable_t)PTE2PA(*pte);
    } else {
      if(!alloc || (pagetable = (pagetable_t)kalloc()) == 0)
        return 0;
      memset(pagetable, 0, PGSIZE);
      *pte = PA2PTE(pagetable) | PTE_V;
    }
  }
  return &pagetable[PX(0, va)];
}
代码解析:
  • PX(level, va) 宏提取对应级别的 VPN 索引:PX(2, va) 提取 VPN[2],PX(1, va) 提取 VPN[1],PX(0, va) 提取 VPN[0]。
  • 循环从 level=2 开始,逐级向下,直到 level=0 返回叶子 PTE 的指针。
  • 如果中间页表缺失且 alloc=1,则调用 kalloc() 分配新物理页并设置 PTE。

2.5 地址翻译完整示例

假设虚拟地址 0x7F3A4B0C,其二进制位划分如下:

  • VPN[2] = 0x1F8 (VA[38:30])
  • VPN[1] = 0x1D2 (VA[29:21])
  • VPN[0] = 0x0B0 (VA[20:12])
  • Offset = 0xC (VA[11:0])

硬件遍历过程:

  1. 从 satp 读取根页表物理地址 PA_root。
  2. 计算 L2 PTE 地址:PA_root + 0x1F8 * 8,读取 PTE,得到 L1 页表物理地址 PA_L1。
  3. 计算 L1 PTE 地址:PA_L1 + 0x1D2 * 8,读取 PTE,得到 L0 页表物理地址 PA_L0。
  4. 计算 L0 PTE 地址:PA_L0 + 0x0B0 * 8,读取 PTE,得到物理页框号 PPN。
  5. 最终物理地址 = PPN << 12 | 0xC。