ARMv7-A MMU 架构详解:页表结构、转换等级、TLB 与缓存架构
ARMv7-A 的 MMU,说白了就是一套地址翻译的硬件流水线。我刚开始接触的时候,也被那套多级页表搞得头晕。但后来发现,只要抓住几个核心概念,这东西其实挺直观的。
地址转换的核心流程
CPU 发出的地址是虚拟地址(VA),经过 MMU 翻译成物理地址(PA),才能去访问内存。这个过程,我习惯把它拆成三步:
- TLB 查找:先看看最近有没有翻译过这个地址。有的话,直接拿结果,快得很。
- 页表遍历:TLB 没命中,硬件就自动去内存里查页表。ARMv7-A 支持一级或两级页表。
- 权限与属性检查:翻译出物理地址后,还要检查访问权限、缓存策略这些。
核心要点:整个转换过程对软件是透明的,除了页表基地址寄存器(TTBR0/TTBR1)需要你配好,其他都是硬件自动完成。
页表结构与转换等级
ARMv7-A 的页表分为一级页表(L1)和二级页表(L2)。我当年在调试一个视频解码驱动时,就因为搞混了 L1 和 L2 的粒度,导致内存访问异常,查了两天才找到原因。
一级页表(L1)
L1 表有 4096 个表项,每个表项 4 字节,总共 16KB。它覆盖整个 4GB 地址空间。每个表项可以指向:
- 段(Section):1MB 的块,直接映射。适合大块连续内存,比如外设寄存器。
- 粗页表(Coarse Page Table):指向一个 L2 页表,用于更细粒度的映射。
- 细页表(Fine Page Table):ARMv7-A 已经不推荐用了,了解即可。
二级页表(L2)
L2 表包含 256 或 1024 个表项,取决于 L1 表项的类型。它支持三种粒度:
| 粒度 | 大小 | 典型用途 |
|---|---|---|
| 大页(Large Page) | 64KB | 帧缓冲、大块数据缓冲区 |
| 小页(Small Page) | 4KB | 标准内存页,Linux 默认就用这个 |
| 极小页(Tiny Page) | 1KB | 极少用,浪费 TLB 空间 |
我的建议:除非有特殊需求,否则尽量用 4KB 小页。Linux 内核的内存管理就是基于 4KB 页设计的,兼容性最好。
TLB 架构
TLB 是 MMU 内部的高速缓存,专门存最近用过的 VA→PA 映射。ARMv7-A 的 TLB 通常分为:
- 指令 TLB(ITLB):缓存指令地址的翻译结果。
- 数据 TLB(DTLB):缓存数据地址的翻译结果。
- 统一 TLB(UTLB):有些核把指令和数据 TLB 合在一起。
我记得有一次,一个同事发现程序跑着跑着就卡死了。查了半天,原来是 TLB 没刷新,导致映射了过期的物理地址。从那以后,我每次切换页表或者修改页表项,都会记得做一次 TLB 失效操作。
避坑指南:我曾经在 SMP 系统上吃过亏。一个核改了页表,另一个核的 TLB 还是旧的。必须用广播指令(如 TLBIALL)来同步所有核的 TLB。
缓存架构
ARMv7-A 的缓存分为 L1 和 L2。L1 又分成指令缓存(I-Cache)和数据缓存(D-Cache)。
| 缓存级别 | 特点 | 典型大小 |
|---|---|---|
| L1 I-Cache | 只缓存指令,通常 VIPT(虚拟索引物理标签) | 16KB - 64KB |
| L1 D-Cache | 只缓存数据,PIPT(物理索引物理标签)或 VIPT | 16KB - 64KB |
| L2 Cache | 统一缓存指令和数据,通常 PIPT | 128KB - 2MB |
这里有个关键点:缓存策略。ARMv7-A 允许你为每个内存区域单独配置:
- Write-Back(回写):写操作先到缓存,等缓存行被替换时才写回内存。性能好,但要注意缓存一致性。
- Write-Through(透写):写操作同时写缓存和内存。简单,但性能差一些。
- Non-Cacheable(不缓存):直接访问内存。适合外设寄存器。
实战经验:我在做 DMA 驱动时,就遇到过缓存一致性问题。CPU 写了一段内存,DMA 去读,结果读到的是缓存里的旧数据。解决方案是:在 DMA 传输前,用 DCACHE_CLEAN 把缓存刷回内存。
MMU 与缓存的交互
MMU 和缓存不是孤立的。MMU 的页表项里,有两位专门控制缓存行为:
- TEX[2:0]:类型扩展位,定义内存类型(强序、设备、正常)。
- C:可缓存位。
- B:可缓冲位。
举个例子,如果你把外设寄存器映射成“设备内存”(Device Memory),MMU 会告诉缓存:别碰这块地址,每次都直接访问物理内存。这样就能避免外设读到脏数据。
页表项格式详解
一个典型的 L2 小页表项(4KB)长这样:
位 [31:12]:物理地址高位(PA[31:12])
位 [11:9] :AP[2:0](访问权限)
位 [8:6] :TEX[2:0](类型扩展)
位 [5] :APX(访问权限扩展)
位 [4] :S(可共享)
位 [3] :nG(非全局)
位 [2] :XN(不可执行)
位 [1] :C(可缓存)
位 [0] :B(可缓冲)
嗯,这里要注意:AP 和 APX 组合起来决定读写权限。比如 AP=0b11, APX=0 就是“完全可读写”。我习惯在初始化时,把内核空间设为“特权模式可读写,用户模式不可访问”,这样能防止用户程序乱搞。
小技巧:调试 MMU 问题时,可以用 mcr p15, 0, r0, c7, c8, 0 指令来手动失效 TLB。我经常在修改页表后,用这个指令来确保新映射生效。
总结一下
ARMv7-A 的 MMU 其实不复杂。你只要记住:
- 页表是树形结构,L1 管大块,L2 管小块。
- TLB 是加速器,记得失效它。
- 缓存策略在页表项里配,别搞混。
把这些搞清楚了,MMU 配置对你来说就是小菜一碟。