3. ARMv8-A MMU 架构详解:AArch64 与 AArch32 模式差异、4级页表、粒度的选择

ARMv8-A 是个分水岭。说实话,我第一次看到它的 MMU 手册时,心里也咯噔了一下——怎么比 v7 复杂这么多?但用久了你会发现,它的设计其实很优雅。64 位和 32 位两套模式,各有各的脾气。

3.1 AArch64 与 AArch32:两套 MMU 的基因差异

ARMv8-A 最核心的变化,就是引入了 AArch64 执行状态。这意味着 MMU 也得跟着改。我习惯把这两套 MMU 比作「父子两代工程师」——底层逻辑相通,但实现方式完全不同。

对比项 AArch64 MMU AArch32 MMU
虚拟地址宽度 48 位(可选 52 位) 32 位(LPAE 下 40 位)
页表级数 4 级(L0~L3) 2 级或 3 级(L1~L3)
页表基址寄存器 TTBR0_ELx / TTBR1_ELx TTBR0 / TTBR1(CP15)
转换粒度 4KB / 16KB / 64KB 4KB / 64KB(LPAE)
地址空间划分 固定 2 个区域(低/高) 可配置 2~3 个区域
异常等级 EL0~EL3 独立 MMU 上下文 PL0~PL2 共享 MMU 配置

关键点:AArch64 的 MMU 是「按异常等级隔离」的。每个 EL 都有自己的 TTBR 和 MAIR。这在虚拟化场景下简直是救命的设计——我曾在 EL2 跑 Hypervisor,EL1 跑 Linux,EL0 跑 App,页表完全隔离,互不干扰。

3.2 4 级页表:从虚拟地址到物理地址的「导航系统」

AArch64 的 4 级页表,说白了就是一张多级地图。虚拟地址 48 位,被切成了 5 段:L0 索引、L1 索引、L2 索引、L3 索引、页内偏移。每一级索引都指向下一级页表的基址。

为什么会设计成 4 级?我个人的理解是:为了在「页表大小」和「地址覆盖范围」之间找平衡。如果只用 2 级,单张页表会大到不现实;如果用 5 级,查表延迟又太高。4 级是个折中。

ARMv8-A 4级页表转换流程 L0 索引 (9 bit) L1 索引 (9 bit) L2 索引 (9 bit) L3 索引 (9 bit) 偏移 (12 bit) L0 页表 L1 页表 L2 页表 L3 页表 物理地址 = 页基址 + 偏移 每级页表 512 个表项

避坑指南:我曾经在调试一个 4K 粒度的系统时,发现 L0 页表总是报错。查了半天才发现——L0 页表只在 48 位地址空间下存在。如果你的虚拟地址只用 39 位,L0 会被跳过,直接从 L1 开始。手册里叫它「起始级别」,很多人会忽略这个细节。

3.3 粒度的选择:4KB vs 16KB vs 64KB

粒度,就是页的大小。ARMv8-A 支持三种粒度:4KB、16KB、64KB。你可能会问:为什么不能只用一种?嗯,每种粒度都有自己的「舒适区」。

粒度 页表级数 覆盖范围 典型场景
4KB 4 级(L0~L3) 48 位地址空间 通用 Linux / Android
16KB 4 级(L0~L3) 48 位地址空间 存储类应用 / 大页需求
64KB 3 级(L1~L3) 48 位地址空间 高性能计算 / 服务器

我个人习惯在嵌入式 Linux 上用 4KB 粒度。为什么?因为 Linux 内核的内存管理就是基于 4KB 页设计的。换用 16KB 或 64KB,虽然能减少 TLB miss,但内核的 buddy allocator、page cache 都得跟着改——工作量不小。

注意:粒度不是随便选的。它会影响页表的级数和 TLB 的覆盖范围。64KB 粒度下,L0 页表被省掉了,但每个页表项覆盖的区域更大。如果你的内存碎片严重,64KB 粒度反而会浪费空间——我见过一个项目,用 64KB 粒度后,内存利用率从 85% 掉到了 60%。

3.4 模式差异带来的实际影响

说完了理论,聊聊实际开发中会遇到的问题。

  • 页表格式不同:AArch64 的页表项是 64 位的,AArch32 是 32 位的。这意味着你在写汇编操作页表时,寄存器宽度、偏移计算都得重新适配。
  • MAIR 寄存器:AArch64 用 MAIR_ELx 配置内存属性,AArch32 用 PRRR / NMRR。我刚开始切到 64 位时,找了半天才发现 MAIR 的编码方式和 PRRR 完全不同。
  • TLB 维护指令:AArch64 的 TLBI 指令更细粒度——可以按 ASID、按 VA、按 EL 来刷。AArch32 的 TLBI 就粗糙多了,经常得全刷。

经验之谈:如果你在做从 AArch32 到 AArch64 的迁移,我建议先把 MMU 初始化代码重写一遍。别想着「改改寄存器名字就行」——页表布局、异常等级、内存属性,全都不一样。我当年就吃过这个亏,调试了整整一周。

3.5 实战:如何选择粒度和页表级数

选粒度,其实是在问自己三个问题:

  1. 你的内存有多大? 小于 1GB,4KB 粒度就够了。大于 4GB,考虑 16KB 或 64KB。
  2. 你的应用对 TLB miss 敏感吗? 实时系统、网络转发,TLB miss 是性能杀手。大粒度能覆盖更多地址空间。
  3. 你的 OS 支持吗? 不是所有 OS 都支持 16KB/64KB。Linux 从 4.0 才开始支持 64KB 页。

举个例子。我之前做一个网络设备,内存 8GB,跑 Linux。一开始用 4KB 粒度,TLB miss 率在 3% 左右。换成 64KB 后,TLB miss 降到了 0.5%,吞吐量提升了 12%。代价是内核内存浪费了 8%。值不值?看你的业务场景。

小技巧:在 ARMv8-A 上,你可以通过 ID_AA64MMFR0_EL1 寄存器查看硬件支持的粒度。别假设所有核都支持 64KB——有些低端芯片只支持 4KB 和 16KB。

好了,这一章的内容就到这。记住:MMU 的配置没有银弹。理解 AArch64 和 AArch32 的差异,掌握 4 级页表的流转,选对粒度——这三件事做好了,你的系统就稳了一半。