ARMv7 MMU 到底在干什么?
说实话,很多做嵌入式开发的朋友,一开始都觉得 MMU 是个「黑盒子」。不就是把虚拟地址转成物理地址吗?嗯,这话没错,但太笼统了。我做了这么多年 ARM 平台优化,可以负责任地告诉你:不理解 MMU,你就没法真正掌控系统的性能和稳定性。
ARMv7 的 MMU,说白了就是 CPU 和物理内存之间的一个「翻译官 + 保安队长」。它干三件事:
- 地址转换:把程序看到的虚拟地址,映射到真实的物理地址
- 权限控制:决定谁能读、谁能写、谁能执行
- 属性管理:告诉 Cache 和写缓冲该怎么处理这段内存
你想想看,如果没有 MMU,所有程序都直接操作物理地址,一个野指针就能把整个系统搞崩。我在一个工控项目里就遇到过——同事关掉了 MMU 做裸机调试,结果一个数组越界直接覆盖了中断向量表,查了三天才找到原因。
地址转换:两级页表怎么查?
ARMv7 用的是两级页表结构。一级叫 L1 页表,二级叫 L2 页表。为什么搞两级?直接一级映射 4GB 不行吗?行,但太浪费内存了。一级页表要 16KB,二级页表按需分配,这才是嵌入式该有的做法。
转换流程大致是这样:
- CPU 发出一个虚拟地址(32位)
- MMU 从 CP15 寄存器里拿到一级页表基地址
- 用虚拟地址的 [31:20] 位索引 L1 页表,拿到一个描述符
- 如果描述符指向的是段(Section),直接得到物理地址
- 如果指向的是二级页表,再用 [19:12] 位索引 L2 页表
- 最终拿到 4KB 页面的物理地址
关键点:ARMv7 支持 4KB 小页、16KB 大页、64KB 段、1MB 段。我个人习惯,用户空间用 4KB 小页,内核空间用 1MB 段——这样既能精细控制,又能减少 TLB miss。
TLB 和 Cache:一对黄金搭档
地址转换是有代价的。每次查页表都要访问内存,那性能就完蛋了。所以 MMU 内部有个小硬件缓存,叫 TLB(Translation Lookaside Buffer)。它专门缓存最近用过的地址转换结果。
Cache 呢?它缓存的是实际的数据和指令。两者协同工作的逻辑是这样的:
- CPU 发出虚拟地址 → 先查 TLB
- TLB 命中 → 直接拿到物理地址 → 查 Cache
- Cache 命中 → 直接返回数据
- Cache 未命中 → 去物理内存取数据,同时更新 Cache
- TLB 未命中 → 硬件自动遍历页表,更新 TLB
这里有个坑。我曾经在一个视频处理项目里,发现帧率忽高忽低。排查到最后,是 TLB 抖动导致的——进程切换太频繁,TLB 被刷了个干净,每次都要重新查页表。后来我用了 TLB 锁定(TLB Lockdown),把关键代码段的映射锁在 TLB 里,问题就解决了。
我的建议:在实时性要求高的场景,考虑使用 TLB 锁定。ARMv7 的 TLB 支持锁定 1-8 个条目,足够放最关键的几段代码或数据了。
页表描述符:不只是地址
ARMv7 的页表描述符里,除了物理地址,还塞了一大堆属性位。这些位直接决定了内存的行为。我列个表,你一看就明白:
| 属性位 | 作用 | 我踩过的坑 |
|---|---|---|
| XN | 禁止执行 | 忘了设 XN,栈区被注入代码执行 |
| C/B | Cache 和写缓冲策略 | DMA 缓冲区设了 Cache,数据不一致 |
| AP | 访问权限(读/写) | 用户态能写内核数据,直接 panic |
| NS | 安全世界/非安全世界 | TrustZone 项目里配反了,死机 |
| SH | 共享属性(多核用) | 双核通信没设共享,数据互相看不见 |
注意:Cache 和 MMU 是独立的控制位。你可以开 MMU 但关 Cache,也可以开 Cache 但关 MMU。但在 ARMv7 里,MMU 关闭时,Cache 是透传的——也就是说,所有访问都直接走内存。这个特性在低功耗模式下很有用。
一张图看懂 MMU 全流程
下面这张 SVG 图,把 MMU、TLB、Cache 和内存的关系画清楚了。我建议你把它存下来,以后调试的时候对照着看。
Cache 策略:写回 vs 写通
ARMv7 的 Cache 支持两种写策略:
- 写回(Write-Back):数据先写 Cache,等 Cache Line 被替换时才写回内存。性能好,但要注意一致性。
- 写通(Write-Through):数据同时写 Cache 和内存。简单,但慢。
我个人习惯,代码段用写回,DMA 缓冲区用写通或者干脆关 Cache。为什么?因为 DMA 是直接访问物理内存的,如果 Cache 里有脏数据没写回去,DMA 读到的就是旧数据。我曾经在一个网络驱动里,因为没做 Cache 一致性维护,收包总是丢前几个字节——查了两天,最后发现是 Cache 里的数据比内存新,DMA 读走了旧数据。
避坑指南:如果你用 DMA,记得在启动 DMA 前调用 clean_dcache_range(),在 DMA 完成后调用 invalidate_dcache_range()。ARMv7 的 CP15 指令可以直接操作 Cache Line,不用整片刷。
域(Domain)和权限检查
ARMv7 的 MMU 还有一个「域」的概念。每个页表条目都属于 16 个域之一。CP15 的寄存器可以单独控制每个域的访问权限:
| 域权限 | 含义 | 典型用途 |
|---|---|---|
| 00 | 无访问(故障) | 隔离不安全的外设 |
| 01 | 客户(检查 AP) | 普通用户空间 |
| 10 | 保留 | — |
| 11 | 管理者(不检查 AP) | 内核空间 |
这个设计很巧妙。你想想看,内核空间设成「管理者」域,MMU 就不检查 AP 位了,直接放行。用户空间设成「客户」域,AP 位决定具体权限。这样既保证了内核的灵活,又保证了用户态的安全。
嗯,这一章的内容就到这里。MMU 的基础打牢了,后面讲页表配置、TLB 优化、Cache 一致性维护的时候,你才能跟得上。记住一句话:MMU 不是负担,是你掌控系统内存的武器。