2、ARMv8-A MMU架构:页表层级结构(4级页表)、翻译粒度(4KB/16KB/64KB)、地址空间划分(用户空间与内核空间)

好,我们接着往下聊。上一章我们把MMU的开关和基本控制逻辑理清了。这一章,咱们深入ARMv8-A的MMU核心——页表。说白了,页表就是一张“虚拟地址到物理地址”的映射表。但ARMv8-A的这张表,可不是一张简单的平面表,它是一个四级的树形结构。

2.1 四级页表:为什么搞这么复杂?

你可能会问:“为什么不用一张大表搞定所有映射?” 嗯,这个问题我当年刚接触ARMv8时也想过。原因很简单:节省内存。你想,一个48位的虚拟地址空间,如果直接用一张页表,那得占多大地方?4KB的页,需要2^36个页表项,每个条目8字节,那就是512GB!这显然不现实。

所以,ARMv8-A用了四级页表。从高到低分别是:PGD(页全局目录)、PUD(页上级目录)、PMD(页中间目录)、PTE(页表项)。每一级只索引地址的一部分,只有真正用到的虚拟地址范围,才会分配对应的页表。

我个人习惯把这种结构想象成一棵“懒加载”的树。你想想看,一个进程可能只用了一小段地址空间,那它只需要在树根上长出几根树枝,而不是把整棵大树都种出来。我在项目中遇到过一个大内存的嵌入式设备,内存只有256MB,但虚拟地址空间是48位的。如果不用四级页表,光是页表本身就能把内存吃光。

核心要点:四级页表的核心思想是“按需分配”。只有被映射的虚拟地址范围,才会在每一级上创建对应的页表。这极大节省了页表占用的物理内存。

2.2 翻译粒度:4KB、16KB、64KB

ARMv8-A支持三种页大小:4KB、16KB、64KB。这个“粒度”决定了页表最后一层(PTE)能映射多大的连续物理内存。

我直接给你一个对比表,这样更清楚:

粒度 页表级数 每级索引位数 最大地址空间 典型场景
4KB 4级 9+9+9+9+12 48位 通用Linux、Android
16KB 4级 11+11+11+11+14 48位 某些服务器场景
64KB 3级 13+13+13+16 48位 高性能计算、大页优化

注意看,64KB粒度时,页表级数变成了3级。为什么?因为64KB的页内偏移需要16位,剩下的32位地址只需要3级索引就够了。这能减少一次内存访问,提升TLB命中率。

我曾经在一个视频编解码项目中,把页粒度从4KB改成64KB,TLB缺失率直接降了30%。当然,代价是内存浪费更严重——你分配的最小单位是64KB,哪怕只用1字节也得占一整页。所以,选粒度就是选 trade-off

我的建议:如果你在做嵌入式实时系统,内存紧张,用4KB。如果你在做高性能计算,内存充裕,追求TLB命中率,用64KB。16KB是个折中方案,但用得不多。

2.3 地址空间划分:用户空间与内核空间

ARMv8-A把虚拟地址空间分成两半:用户空间(EL0)内核空间(EL1及以上)。怎么分的?看最高位。

对于48位地址空间:

  • 0x0000_0000_0000_0000 ~ 0x0000_FFFF_FFFF_FFFF:用户空间(低半部分)
  • 0xFFFF_0000_0000_0000 ~ 0xFFFF_FFFF_FFFF_FFFF:内核空间(高半部分)

中间那一段(0x0001_0000_0000_0000 ~ 0xFFFE_FFFF_FFFF_FFFF)是空洞,不允许访问。你想想看,这样设计的好处是什么?权限隔离。用户态程序只能访问低地址,内核态可以访问全部。一旦用户程序试图访问内核地址,MMU直接触发异常。

我记得有一次调试一个驱动,用户态程序传了一个指针进来,指向0xFFFF_0000_1234_5678。我一看就知道坏了——这明显是内核地址。果然,一访问就崩了。嗯,这里要注意:用户空间和内核空间的页表是独立的。每个进程有自己的用户页表,但所有进程共享同一个内核页表。这样切换进程时,只需要切换用户页表,内核部分不变。

避坑指南:我曾经在移植Linux时,不小心把内核空间的页表属性配错了,导致用户态能读到内核数据。那叫一个惨,系统安全形同虚设。所以,一定要检查TTBR0_EL1(用户页表基址)和TTBR1_EL1(内核页表基址)的配置,确保它们指向正确的页表。

2.4 页表项的结构

每个页表项(PTE)是8字节。我挑几个关键位说说:

  • bit 0: Valid —— 这个条目是否有效。无效的话,MMU直接报错。
  • bit 1: Table/Page —— 0表示指向下一级页表,1表示这是一个最终的页映射。
  • bit 2-4: AttrIndx —— 属性索引,指向MAIR寄存器定义的属性。
  • bit 5: NS —— 非安全空间位,用于TrustZone。
  • bit 6: AP —— 访问权限,控制读/写权限。
  • bit 7: SH —— 可共享性,用于多核一致性。
  • bit 8-9: AF —— 访问标志,硬件自动置位。
  • bit 10-11: nG —— 全局位,用于TLB标记。

你看,一个8字节的条目,塞了这么多信息。ARM的设计确实精妙。我刚开始看手册时,被这些位搞得头晕。后来我写了个小工具,把页表项打印出来,逐位解析,才真正搞明白。

这里给一个简单的页表项解析示例(伪代码):

// 假设 pte 是一个 64 位的页表项
if (pte & 0x1) {  // Valid 位
    if (pte & 0x2) {  // Table/Page 位
        // 这是一个页映射
        phys_addr = pte & 0x0000FFFFFFFFF000;  // 取物理页基址
        attr_index = (pte >> 2) & 0x7;          // 属性索引
        ap = (pte >> 6) & 0x3;                  // 访问权限
    } else {
        // 这是一个指向下一级页表的指针
        next_table = pte & 0x0000FFFFFFFFF000;
    }
}

嗯,代码很简单,但实际调试时,你往往需要结合硬件手册来确认每一位的含义。我建议你手边常备一份《ARM Architecture Reference Manual ARMv8》,遇到问题直接查表。

2.5 小结

这一章我们聊了ARMv8-A MMU的页表结构。四级页表解决了大地址空间下的内存浪费问题;三种粒度让你在性能和内存利用率之间做选择;用户/内核空间的划分提供了基本的隔离保护。下一章,我们看看RISC-V的MMU是怎么设计的,和ARM有什么异同。

记住,页表是MMU的灵魂。搞懂了页表,你就掌握了内存管理的钥匙。