第三章 ARMv9内存管理单元(MMU):页表、粒度与TLB实战

内存管理单元,说白了就是CPU的地址翻译官。ARMv9的MMU比之前复杂了不少,但核心逻辑没变——把虚拟地址变成物理地址。我刚开始接触ARMv9时,第一反应是“这不就是页表嘛”,结果一深入才发现,细节多得让人头皮发麻。

这一章,咱们就把它拆开揉碎了讲。你想想看,一个系统跑着几十个进程,每个进程都以为自己独占4GB空间,这背后全靠MMU撑着。

3.1 页表结构:4级 vs 3级

ARMv9支持两种页表层级:4级和3级。选哪个,取决于你的地址空间大小和粒度需求。

层级 页表级数 最大地址空间 典型场景
4级页表 L0 → L1 → L2 → L3 48位虚拟地址 通用OS(Linux/Android)
3级页表 L1 → L2 → L3 39位虚拟地址 嵌入式RTOS、裸机系统

我个人习惯,只要地址空间不超过512GB,就用3级页表。少一级查表,TLB miss时少一次内存访问,性能上能省几个cycle。

经验之谈: 我在一个车载项目中,为了省那点页表占用,硬是把4级改成了3级。结果发现地址空间不够用,又改回去。嗯,别学我,先算清楚再动手。

3.2 转换粒度:4K / 16K / 64K

粒度就是页的大小。ARMv9支持三种:4K、16K、64K。选哪个,直接影响TLB覆盖率和内存碎片。

  • 4K页:最通用,Linux默认。粒度细,内存浪费少,但页表项多,TLB压力大。
  • 16K页:折中方案。我在一些多媒体设备上用过,TLB miss率比4K低不少。
  • 64K页:大页,适合大内存场景。TLB覆盖范围大,但内部碎片也大。

为什么会这样?你想想看,TLB的条目数是固定的。页越大,单条TLB能映射的内存就越多。4K页下,一条TLB只能管4KB;64K页下,一条能管64KB。差距16倍。

注意: 粒度不是随便选的。ARMv9要求所有级联页表使用相同粒度。你不能L2用4K,L3用64K。我曾经见过一个团队因为这个配置错误,系统跑起来直接挂掉。

3.3 TLB管理:刷得对才能跑得快

TLB是MMU的缓存,存着最近用过的地址转换结果。问题来了——页表改了,TLB里的旧条目怎么办?

答案是:刷掉。ARMv9提供了几种TLB维护指令:

  • TLBI ALLE1:刷掉所有EL1的TLB条目。简单粗暴,但开销大。
  • TLBI VAE1:按虚拟地址刷。只刷特定地址,粒度更细。
  • TLBI ASIDE1:按地址空间ID(ASID)刷。切换进程时用这个,不用全刷。

我建议,进程切换时优先用ASID。全刷TLB的成本太高,尤其是大页场景。我在一个网络设备上做过测试,全刷TLB导致延迟抖动从10μs飙到50μs,换成ASID后降回15μs。

核心原则: 能局部刷就别全局刷,能按ASID刷就别按地址刷。TLB维护是性能敏感路径,别偷懒。

3.4 内存属性与域

ARMv9的MMU不只是做地址转换,它还管内存属性。每个页表项里都带着属性位,告诉CPU这块内存怎么用。

属性 作用 典型值
AttrIndx 指向MAIR中的属性配置 0~7
NS 非安全空间标记 0/1
AP 访问权限(读/写/只读) 00/01/10/11
SH 共享属性(不可共享/内部共享/外部共享) 00/01/10
AF 访问标志位 0/1

域(Domain)这个概念在ARMv9里被弱化了,但依然存在。每个页表项属于一个域,域控制着对该页表的访问权限。说白了,就是给页表再加一层保护。

我遇到过一个问题:一个外设DMA写内存,CPU读同一块内存,结果数据不一致。查了半天,发现是内存属性里把共享位设成了“不可共享”。改成“内部共享”后,问题解决。嗯,这种坑,踩过一次就记住了。

避坑指南: 设置内存属性时,先想清楚这个内存是给谁用的。CPU独占?CPU+外设?多核共享?属性设错了,轻则性能下降,重则数据错乱。

3.5 知识结构图:MMU核心逻辑

下面这张图,把MMU的整个流程串起来了。从虚拟地址到物理地址,中间经过页表查询、TLB命中检查、属性校验,最后才拿到物理地址。

ARMv9 MMU 地址转换流程 ARMv9 MMU 地址转换流程 虚拟地址 (VA) TLB 查询 TLB 命中 → 物理地址 TLB 未命中 → 查页表 4级/3级页表遍历 属性校验 (AP/SH/Attr) 物理地址 (PA) 关键点 TLB命中直接出物理地址 未命中才走页表遍历

这张图里,TLB命中是绿色路径,未命中是橙色路径。你想想看,如果TLB命中率高,大部分访问走绿色路径,性能自然好。这就是为什么我前面强调TLB管理的重要性。

3.6 实战建议:配置MMU的步骤

如果你要从零开始配MMU,我建议按这个顺序来:

  1. 确定地址空间大小:选3级还是4级页表。
  2. 选粒度:4K/16K/64K,看你的内存分布。
  3. 配置MAIR:设置内存属性寄存器,定义AttrIndx对应的缓存策略。
  4. 建立页表:从L0/L1开始,逐级填充。
  5. 设置TTBR:把页表基地址写到TTBR0_EL1/TTBR1_EL1。
  6. 使能MMU:写SCTLR_EL1的M位,开启MMU。
  7. 验证:读一个虚拟地址,看是否映射到正确的物理地址。
警告: 使能MMU前,确保页表已经完整建立。否则,CPU一开MMU就取指令,页表不全直接触发异常。我见过一个新手,页表只建了一半就开MMU,系统直接卡死。