第三章 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。
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倍。
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。
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读同一块内存,结果数据不一致。查了半天,发现是内存属性里把共享位设成了“不可共享”。改成“内部共享”后,问题解决。嗯,这种坑,踩过一次就记住了。
3.5 知识结构图:MMU核心逻辑
下面这张图,把MMU的整个流程串起来了。从虚拟地址到物理地址,中间经过页表查询、TLB命中检查、属性校验,最后才拿到物理地址。
这张图里,TLB命中是绿色路径,未命中是橙色路径。你想想看,如果TLB命中率高,大部分访问走绿色路径,性能自然好。这就是为什么我前面强调TLB管理的重要性。
3.6 实战建议:配置MMU的步骤
如果你要从零开始配MMU,我建议按这个顺序来:
- 确定地址空间大小:选3级还是4级页表。
- 选粒度:4K/16K/64K,看你的内存分布。
- 配置MAIR:设置内存属性寄存器,定义AttrIndx对应的缓存策略。
- 建立页表:从L0/L1开始,逐级填充。
- 设置TTBR:把页表基地址写到TTBR0_EL1/TTBR1_EL1。
- 使能MMU:写SCTLR_EL1的M位,开启MMU。
- 验证:读一个虚拟地址,看是否映射到正确的物理地址。