3. 页表层级设计:单级页表、多级页表(2级/3级/4级)、ARMv8-A页表架构

页表是MMU将虚拟地址转换为物理地址的核心数据结构。页表层级的设计直接决定了地址空间大小、内存占用、TLB命中率和上下文切换开销。本章从单级页表出发,逐步深入到多级页表的设计原理,最后以ARMv8-A的4级页表架构作为实战案例进行剖析。

3.1 单级页表:简单但低效

单级页表是最直接的地址转换方案:整个虚拟地址空间由一张线性页表覆盖。每个虚拟页对应一个页表项(PTE),PTE中存放物理页帧号(PPN)和权限属性。

单级页表核心参数:
虚拟地址位数:32位 | 页大小:4KB | 页表项大小:4字节
页表项数量:2^20 = 1,048,576 | 页表总大小:4MB
单级页表地址转换结构 虚拟页号(VPN) 20位 偏移 12位 单级页表 (4MB) PTE[0] ... PTE[VPN] ... PTE[2^20-1] 每个PTE: 物理页帧号(PPN) + 属性 索引: VPN → PTE[VPN] 物理地址 = PPN + 偏移 缺点:即使进程只使用少量内存,也必须为整个4GB虚拟地址空间分配4MB连续物理内存
单级页表致命缺陷:对于32位系统(4GB虚拟空间,4KB页),页表固定占用4MB连续物理内存。若系统运行100个进程,仅页表就需要400MB内存,且大部分PTE无效。对于64位系统,单级页表大小将膨胀到不可接受的程度。

3.2 多级页表:按需分配与稀疏空间支持

多级页表将线性页表拆分为层级结构,每一级负责虚拟地址的一部分位段。核心思想是:只分配实际使用的页表层级,未使用的上层页表项可以置空,从而大幅减少内存占用。

3.2.1 两级页表(32位经典设计)

以32位x86架构为例,虚拟地址被拆分为10位页目录索引 + 10位页表索引 + 12位页内偏移。

层级位段位数表项数每表大小
页目录(PD)VPN[31:22]1010244KB
页表(PT)VPN[21:12]1010244KB
页内偏移VA[11:0]12——
两级页表地址转换流程 PD索引 10位 PT索引 10位 偏移 12位 页目录(PD) 1024项 × 4字节 = 4KB PD[PD索引] → 页表基址 页表(PT) 1024项 × 4字节 = 4KB PT[PT索引] → 物理页帧号 物理页帧 基址 + 偏移 → 物理地址 内存节省原理:若进程只使用一个4MB连续区域,仅需1个页目录项+1个页表=8KB 相比单级页表4MB,节省512倍内存。未使用的页目录项直接标记为无效

3.2.2 三级页表(32位PAE/36位物理地址)

当物理地址扩展到36位(PAE)时,两级页表无法容纳更多物理页帧号,引入第三级——页目录指针表(PDPT)。

层级位段位数表项数每表大小
PDPTVPN[31:30]2432字节
页目录(PD)VPN[29:21]95124KB
页表(PT)VPN[20:12]95124KB
页内偏移VA[11:0]12——

3.2.3 四级页表(64位架构标准)

64位架构需要覆盖巨大的虚拟地址空间(当前通常使用48位或52位有效地址)。四级页表成为主流设计,每级索引通常使用9位(页大小4KB时)。

四级页表层级(48位虚拟地址,4KB页):
L0: PGD(页全局目录) — 索引位[47:39],9位,512项
L1: PUD(页上级目录) — 索引位[38:30],9位,512项
L2: PMD(页中间目录) — 索引位[29:21],9位,512项
L3: PTE(页表项) — 索引位[20:12],9位,512项
页内偏移 — 位[11:0],12位
四级页表层级结构(48位虚拟地址) L0:9位 L1:9位 L2:9位 L3:9位 偏移12位 L0: PGD 512项 → 指向L1表 L1: PUD 512项 → 指向L2表 L2: PMD 512项 → 指向L3表 L3: PTE 512项 → 物理页帧 四级页表关键特性 • 每级表大小固定:512项 × 8字节 = 4KB • 覆盖虚拟地址空间:2^48 = 256TB • 支持大页:L1块(1GB)、L2块(2MB)、L3页(4KB) • 未使用的中间级表不分配物理内存 • 典型进程页表总大小:4KB(PGD) + 4KB(PUD) + N×4KB(PMD) + M×4KB(PTE) • 硬件TLB缓存最近使用的转换,减少内存访问次数 • 每级PTE包含:物理地址高位 + 权限位 + 属性位 • 页表遍历需要4次内存访问(无TLB命中时) • ARMv8-A使用4级页表(48位)或5级页表(52位)

3.3 ARMv8-A页表架构详解

ARMv8-A架构支持AArch64(64位)和AArch32(32位)两种执行状态。在AArch64状态下,MMU使用4级页表(当使用48位虚拟地址时)或5级页表(当使用52位虚拟地址时)。

3.3.1 页表层级与粒度

粒度页大小L0索引位数L1索引位数L2索引位数L3索引位数覆盖地址空间
4KB4KB9位(VA[47:39])9位(VA[38:30])9位(VA[29:21])9位(VA[20:12])48位/256TB
16KB16KB—13位(VA[47:35])13位(VA[34:22])11位(VA[21:14])48位/256TB
64KB64KB——14位(VA[47:34])13位(VA[33:16])48位/256TB

3.3.2 页表描述符格式

ARMv8-A的每个页表项(描述符)为8字节(64位)。描述符类型由最低两位决定:

  • 00:无效描述符(表项未使用)
  • 01:表描述符(指向下一级页表)
  • 10:块描述符(直接映射到大块内存,仅L1/L2可用)
  • 11:页描述符(仅L3可用,映射4KB/16KB/64KB页)
ARMv8-A L3页描述符(64位)关键字段:
位[63:48]:物理地址高位(取决于物理地址宽度)
位[47:12]:物理页帧号(PPN)
位[11:10]:Access Flag(AF)
位[7:6]:属性索引(AttrIndx)
位[5]:非安全(NS)
位[4]:用户/特权(AP[1])
位[3]:读写/只读(AP[2])
位[2]:可共享(Shareable)
位[1:0]:类型(11 = 页描述符)

3.3.3 转换表基址寄存器(TTBR)

ARMv8-A使用两个转换表基址寄存器:

  • TTBR0_EL1:用户空间页表基址(虚拟地址高位为0)
  • TTBR1_EL1:内核空间页表基址(虚拟地址高位为1)

通过TCR(Translation Control Register)配置地址分割点,典型配置为:用户空间0x0000000000000000-0x0000FFFFFFFFFFFF(48位),内核空间0xFFFF000000000000-0xFFFFFFFFFFFFFFFF(48位)。

ARMv8-A 地址空间分割与页表选择 用户空间 (TTBR0) 0x0000 0000 0000 0000 - 0x0000 FFFF FFFF FFFF 内核空间 (TTBR1) 0xFFFF 0000 0000 0000 - 0xFFFF FFFF FFFF FFFF TCR.T0SZ/T1SZ 控制分割点 地址转换流程 VA[63] = 0 → 使用TTBR0_EL1 → 遍历4级页表 → 物理地址 VA[63] = 1 → 使用TTBR1_EL1 → 遍历4级页表 → 物理地址 ARMv8-A 页表设计要点 • 页表遍历由硬件自动完成(MMU内部状态机),无需软件干预 • TLB(Translation Lookaside Buffer)缓存最近使用的页表转换结果 • 支持TLB失效指令(TLBI)在上下文切换时选择性清除TLB条目

3.4 页表层级设计对比总结

特性单级页表两级页表三级页表四级页表(ARMv8-A)
适用架构早期32位MCUx86 32位x86 PAEARMv8-A/x86_64
虚拟地址宽度32位32位32位(36位物理)48位/52位
页表层级数1234(或5)
最小页表内存(空进程)4MB(固定)4KB(仅页目录)32B(PDPT)4KB(PGD)
典型页表内存(1GB进程)4MB~1MB~1MB~2MB
TLB缺失代价1次内存访问2次内存访问3次内存访问4次内存访问
大页支持不支持4MB大页2MB/4MB2MB/1GB
硬件遍历是是是是
实战建议:在ARMv8-A平台上设计DMA缓冲区时,优先使用2MB大页(PMD块描述符)映射。大页可以减少页表层级深度(只需L0→L1→L2三级遍历),降低TLB压力,同时保证DMA缓冲区物理连续。配置方法:在L2级页表中使用块描述符(类型=10),而非指向L3页表的表描述符。

3.5 页表层级与DMA缓冲区设计的关联

页表层级设计直接影响DMA缓冲区的分配和映射策略:

  • 物理连续性要求:DMA设备通常要求缓冲区物理地址连续。多级页表可以通过大页映射(2MB/1GB)确保大块物理连续区域。
  • 页表锁定:DMA缓冲区对应的页表项必须标记为"不可换出",防止DMA操作期间物理页被回收。
  • 一致性管理:ARMv8-A页表属性中的Shareable位和Cache属性位控制DMA缓冲区的缓存策略(非缓存/写通/写回)。
  • IOMMU/SMMU:系统MMU(SMMU)为DMA设备提供独立的地址转换,其页表结构与ARMv8-A MMU兼容,但使用独立的转换表基址寄存器。
常见陷阱:在多级页表系统中,DMA缓冲区如果跨越页表边界(如4KB页边界),可能导致DMA操作被拆分为多个不连续的物理区域。务必使用dma_alloc_coherent()或分配大页(2MB)来保证物理连续性。同时注意,DMA缓冲区必须对齐到缓存行大小(ARMv8-A通常为64字节),避免缓存一致性问题。