ARMv7 MMU 到底在干什么?

说实话,很多做嵌入式开发的朋友,一开始都觉得 MMU 是个「黑盒子」。不就是把虚拟地址转成物理地址吗?嗯,这话没错,但太笼统了。我做了这么多年 ARM 平台优化,可以负责任地告诉你:不理解 MMU,你就没法真正掌控系统的性能和稳定性。

ARMv7 的 MMU,说白了就是 CPU 和物理内存之间的一个「翻译官 + 保安队长」。它干三件事:

  • 地址转换:把程序看到的虚拟地址,映射到真实的物理地址
  • 权限控制:决定谁能读、谁能写、谁能执行
  • 属性管理:告诉 Cache 和写缓冲该怎么处理这段内存

你想想看,如果没有 MMU,所有程序都直接操作物理地址,一个野指针就能把整个系统搞崩。我在一个工控项目里就遇到过——同事关掉了 MMU 做裸机调试,结果一个数组越界直接覆盖了中断向量表,查了三天才找到原因。

地址转换:两级页表怎么查?

ARMv7 用的是两级页表结构。一级叫 L1 页表,二级叫 L2 页表。为什么搞两级?直接一级映射 4GB 不行吗?行,但太浪费内存了。一级页表要 16KB,二级页表按需分配,这才是嵌入式该有的做法。

转换流程大致是这样:

  1. CPU 发出一个虚拟地址(32位)
  2. MMU 从 CP15 寄存器里拿到一级页表基地址
  3. 用虚拟地址的 [31:20] 位索引 L1 页表,拿到一个描述符
  4. 如果描述符指向的是段(Section),直接得到物理地址
  5. 如果指向的是二级页表,再用 [19:12] 位索引 L2 页表
  6. 最终拿到 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 和内存的关系画清楚了。我建议你把它存下来,以后调试的时候对照着看。

ARMv7 MMU 地址转换与 TLB/Cache 协同流程 CPU 核心 TLB 命中 Cache 命中 返回数据 未命中 硬件页表遍历 物理内存 更新 TLB 未命中 Cache Line 填充 关键说明: 1. TLB 命中后直接得到物理地址,无需访问页表 2. Cache 命中后直接返回数据,无需访问物理内存 3. TLB 未命中时,硬件自动遍历两级页表,并更新 TLB 4. Cache 未命中时,从物理内存加载整个 Cache Line(通常 32 或 64 字节)

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 不是负担,是你掌控系统内存的武器。