3. 内存管理单元(MMU):页表结构、转换粒度与TLB管理
内存管理单元,说白了就是CPU里的一个地址翻译官。它负责把程序看到的虚拟地址,转换成物理内存上的真实地址。ARMv9的MMU相比之前版本,最大的变化就是引入了4级页表,以及更灵活的转换粒度。我刚开始接触ARMv9时,第一反应是:页表层数变多了,性能会不会下降?后来才发现,这背后是为了支持更大的地址空间和更精细的内存控制。
3.1 4级页表结构
ARMv9的页表查找过程,就像一本层层递进的目录。从顶级页表开始,每一级都指向下一级的基地址,直到最后一级找到真正的物理页框。
我个人习惯把4级页表想象成这样一个流程:
- TTBRx_ELx 寄存器指向顶级页表(Level 0)
- 虚拟地址的高位作为索引,查Level 0表项
- 表项指向Level 1页表的基地址
- 继续用虚拟地址的下一段索引查Level 1
- 重复直到Level 3,拿到物理地址
这里有个关键点:不是每次都需要走完4级。如果某个级别的表项标记为“块映射”(Block),那就可以提前结束查找。比如在Level 1直接映射一个1GB的大块,就不用再往下查了。
核心概念: 4级页表支持的最大虚拟地址空间是48位(未来可扩展到52位)。每一级页表项的大小是8字节,一个4KB页面的页表可以存放512个表项。
我在项目中遇到过一个问题:某次调试时发现页表项总是解析错误,查了半天才发现是TTBR寄存器指向的基地址没有按页对齐。嗯,这个坑踩过一次就再也不会忘了。
3.2 转换粒度
ARMv9支持三种转换粒度:4KB、16KB和64KB。你可能会问:为什么要有这么多选择?说白了,这是空间和性能的权衡。
| 粒度 | 页表级数 | 最大地址空间 | 典型场景 |
|---|---|---|---|
| 4KB | 4级(L0-L3) | 48位 | 通用操作系统,内存碎片少 |
| 16KB | 4级(L0-L3) | 48位 | 数据库、大页应用 |
| 64KB | 3级(L0-L2) | 48位 | 高性能计算,TLB命中率高 |
我个人建议:如果你在做嵌入式Linux或Android系统,默认用4KB粒度就好。但如果你在做高性能计算或者数据库加速,试试64KB粒度,TLB的命中率会有明显提升。
避坑指南: 我曾经在一个项目中,为了追求性能把粒度改成64KB,结果发现某些外设DMA只支持4KB对齐的传输。改回来之后,性能虽然降了一点,但系统稳定多了。所以,粒度选择一定要考虑整个系统的兼容性。
3.3 TLB管理与维护
TLB(Translation Lookaside Buffer)是MMU的缓存,专门存放最近用过的页表项。它的命中率直接决定了内存访问的性能。
ARMv9的TLB管理有几个关键点:
- TLB条目格式: 每个条目包含虚拟地址、物理地址、属性(权限、缓存策略等)
- TLB失效操作: 使用
TLBI指令进行失效 - 失效粒度: 可以按ASID、VMID、虚拟地址范围或全部失效
我记得有一次调试一个多核系统,发现某个核的TLB没有及时刷新,导致读取了过期的页表项。后来加上了 DSB 和 ISB 屏障指令,问题才解决。
重要提醒: TLB失效操作后,必须执行数据同步屏障(DSB)和指令同步屏障(ISB),才能保证其他核看到最新的页表。否则,你可能会遇到“幽灵般”的崩溃——程序跑着跑着就挂了,但代码逻辑完全正确。
3.4 页表查找流程(SVG图)
下面这张图展示了ARMv9 4级页表的完整查找流程。从虚拟地址出发,经过4级页表,最终得到物理地址。
3.5 TLB维护指令速查
在实际开发中,TLB维护是绕不开的操作。下面是我整理的常用TLBI指令:
| 指令 | 作用 | 使用场景 |
|---|---|---|
TLBI ALLE1 |
失效EL1所有TLB条目 | 切换进程时 |
TLBI ASIDE1 |
按ASID失效 | 单个进程退出时 |
TLBI VAE1 |
按虚拟地址失效 | 修改特定页表项时 |
TLBI ALLE2 |
失效EL2所有TLB条目 | 虚拟机切换时 |
个人经验: 我习惯在每次修改页表后,先执行 DSB ISH 确保修改可见,再执行 TLBI VAE1 失效对应地址,最后加一个 ISB 保证后续指令看到新页表。这个三步走流程,帮我避免了很多诡异的bug。
3.6 页表属性与权限控制
每个页表项不只是存地址,还包含丰富的属性。ARMv9的页表项格式如下:
位域说明:
[63:48] - 物理地址高位(扩展地址)
[47:12] - 物理地址低位(页框号)
[11:10] - 访问权限(AP)
[9] - 非安全位(NS)
[8] - 可共享位(SH)
[7:6] - 内存属性(AttrIndx)
[5] - 访问标志(AF)
[4] - 脏位(DBM)
[3:2] - 保留
[1] - 可写位(S2AP)
[0] - 有效位(Valid)
这里要注意的是访问标志(AF)。如果AF位为0,第一次访问该页时会触发一个异常,由操作系统软件置位。这个机制可以用来实现“懒加载”——只有真正用到某页时,才分配物理内存。
我曾经踩过的坑: 在实现一个自定义内存管理器时,忘了设置AF位,结果每次访问都触发异常,性能直接崩了。后来在页表初始化时统一把AF置1,才恢复正常。所以,除非你明确需要懒加载,否则建议把AF位默认置1。
3.7 多核系统中的TLB一致性
在多核系统中,每个核都有自己的TLB。当一个核修改了页表,其他核的TLB可能还存着旧数据。这时候就需要广播TLB失效。
ARMv9提供了两种方式:
- 广播失效: 使用
TLBI VAE1IS等带IS后缀的指令,会向所有核发送失效请求 - 本地失效: 使用不带
IS的指令,只影响当前核
我个人建议:在修改共享页表时,一定要用广播失效。否则,其他核可能读到过期的页表项,导致数据不一致。我曾经在一个项目中偷懒用了本地失效,结果多核并发时出现了随机崩溃,排查了两天才找到原因。
嗯,MMU这部分内容确实有点绕,但它是整个内存管理的基石。理解了4级页表的查找流程、粒度选择、TLB维护,你就能在ARMv9平台上写出更高效、更稳定的内存管理代码。