ARMv7-A 架构 MMU 概述:TTB、TLB 与页表层级
各位同学,咱们今天聊聊 ARMv7-A 的 MMU。说实话,这玩意儿是嵌入式系统里最绕、也最关键的部分之一。我早年做第一个 Cortex-A8 项目时,就被页表配置坑过——系统跑起来莫名其妙地卡顿,查了三天才发现是 TLB 颠簸问题。从那以后,我养成了一个习惯:先搞懂 MMU 的骨架,再动手写代码。
ARMv7-A 的 MMU,说白了就是一套地址翻译的硬件机制。它负责把 CPU 发出的虚拟地址,转换成物理地址,送到内存总线上去。你想想看,如果没有 MMU,所有程序都得直接操作物理地址,那多危险?一个野指针就能把整个系统搞崩。
2.1 转换表基址寄存器(TTB)
MMU 启动的第一步,是告诉硬件:你的页表放在哪里?这个信息就存在 TTB 里。ARMv7-A 有两个 TTB 寄存器:TTBR0 和 TTBR1。
关键区别:
- TTBR0:用于低端虚拟地址空间(通常 0x00000000 ~ 0xBFFFFFFF),每个进程可以有自己的页表。
- TTBR1:用于高端虚拟地址空间(通常 0xC0000000 ~ 0xFFFFFFFF),一般放操作系统内核页表。
我个人习惯的做法是:用户态用 TTBR0,内核态用 TTBR1。这样切换进程时,只需要改 TTBR0,内核页表纹丝不动。嗯,这里要注意——TTB 寄存器里存的不是物理地址的完整值,而是经过对齐的基地址。ARMv7-A 要求 TTB 基地址按 16KB 对齐,低 14 位必须为 0。
避坑指南:我曾经在一个项目中,因为 TTB 基地址没有正确对齐,导致 MMU 翻译出来的地址全是错的。系统一启动就进入 Data Abort 异常。排查了整整一天,最后用逻辑分析仪抓总线才发现问题。所以,写 TTB 之前,务必检查地址对齐。
2.2 页表层级结构:L1 与 L2
ARMv7-A 的页表是两级结构。为什么是两级?因为单级页表太占内存,多级又增加翻译延迟。两级是个折中——我当年在项目里实测过,两级页表在 4KB 页面下,内存开销和性能平衡得最好。
2.2.1 L1 页表(一级页表)
L1 页表是一个 16KB 的表格,包含 4096 个条目。每个条目对应 1MB 的虚拟地址空间。L1 条目有三种类型:
| 条目类型 | 描述 | 适用场景 |
|---|---|---|
| 段映射(Section) | 直接映射 1MB 物理内存 | 大块连续内存,如 DDR 区域 |
| 粗页表(Coarse Page Table) | 指向二级页表,支持 4KB/64KB 页面 | 需要细粒度控制的区域 |
| 细页表(Fine Page Table) | 指向二级页表,支持 1KB/4KB/64KB 页面 | 极少使用,ARMv7 已废弃 |
你想想看,如果整个系统都用段映射,那页表只有 16KB,翻译速度极快。但粒度太粗——1MB 的页面,你没法单独设置某个 4KB 页面的权限。所以实际项目中,我通常的做法是:内核代码段用段映射,用户进程用二级页表。
2.2.2 L2 页表(二级页表)
L2 页表由 L1 中的粗页表条目指向。每个粗页表条目指向一个 1KB 的二级页表,包含 256 个条目。每个 L2 条目对应 4KB 的物理页面。
注意:L2 页表的大小是 1KB,必须按 1KB 对齐。我曾经见过有人把 L2 页表放在一个结构体里,结果编译器自动填充了字节,导致对齐失败。解决办法是:用 __attribute__((aligned(1024))) 强制对齐。
L2 条目也有几种类型:
- 小页(Small Page):4KB 页面,最常用。
- 大页(Large Page):64KB 页面,用于大块数据。
- 子页(Subpage):1KB 页面,ARMv7 已废弃。
我个人建议:除非有特殊需求,否则统一用 4KB 小页。64KB 大页虽然能减少 TLB 压力,但内存碎片问题很头疼。我在一个视频编解码项目里试过 64KB 大页,结果内存利用率下降了 15%,最后还是换回了 4KB。
2.3 TLB:地址翻译的缓存
TLB(Translation Lookaside Buffer)是 MMU 内部的一个高速缓存,专门存放最近用过的页表条目。它的作用很简单:避免每次地址翻译都去内存里查页表。
ARMv7-A 的 TLB 分为:
- 指令 TLB(ITLB):缓存指令地址的翻译结果。
- 数据 TLB(DTLB):缓存数据地址的翻译结果。
- 统一 TLB(UTLB):指令和数据共用。
为什么会这样分?因为指令和数据访问模式不同。指令是顺序访问,数据是随机访问。分开缓存能减少冲突。我记得有一次做性能分析,发现某个算法在 TLB 命中率只有 85% 时,性能直接腰斩。后来通过调整页表布局,把热数据集中到连续的虚拟地址上,TLB 命中率提到了 98%,性能立马回来了。
实战技巧:如果你发现系统有间歇性的卡顿,可以检查一下 TLB 缺失率。ARM 的 PMU(性能监视单元)可以统计 TLB 缺失次数。我一般用 PMCCNTR 和 PMSELR 寄存器来读这些计数器。具体代码后面章节会讲。
2.4 地址翻译流程
咱们把整个流程串起来。当 CPU 发出一个虚拟地址时,MMU 会做以下事情:
- 检查 TLB:如果 TLB 命中,直接拿到物理地址,走人。
- TLB 未命中:MMU 根据虚拟地址的高 12 位,索引 L1 页表。
- 解析 L1 条目:如果是段映射,直接得到物理地址;如果是粗页表,拿到 L2 页表基址。
- 查 L2 页表:用虚拟地址的中间 8 位索引 L2 页表,得到 4KB 页面的物理基址。
- 组合物理地址:物理基址 + 虚拟地址的低 12 位偏移 = 最终物理地址。
- 更新 TLB:把这次翻译结果写进 TLB,下次就快了。
嗯,这里要注意:TLB 不是自动更新的。如果你修改了页表,必须手动 invalidate 对应的 TLB 条目。否则 MMU 可能还在用旧的翻译结果,那数据就全乱了。我习惯用 DSB + ISB 指令来保证 TLB 刷新的顺序性。
2.5 整体架构图
下面这张图展示了 ARMv7-A MMU 的核心结构。我把它画成了两级页表 + TLB 的协作流程,方便你理解数据是怎么流动的。
2.6 页表配置的权衡
在实际项目中,页表配置不是一成不变的。你需要根据应用场景做权衡:
| 场景 | 推荐配置 | 理由 |
|---|---|---|
| 实时系统(RTOS) | 段映射为主 | 减少 TLB 缺失,确定性高 |
| Linux 用户进程 | 4KB 小页 + 二级页表 | 支持虚拟内存、换页、写时复制 |
| 多媒体处理 | 64KB 大页 + 段映射混合 | 大块数据连续,减少 TLB 压力 |
| 安全隔离 | 细粒度二级页表 | 每个页面独立权限控制 |
我个人经验是:不要一开始就追求完美配置。先跑起来,然后用性能计数器看 TLB 缺失率、页表访问延迟。根据数据再调整。我曾经在一个网络设备项目里,把页表从全二级改成段映射 + 二级混合,TLB 缺失率从 12% 降到了 3%,吞吐量提升了 20%。
重要提醒:修改页表配置后,一定要做两件事:
- DSB:保证所有内存访问完成。
- ISB:刷新指令流水线,让 MMU 重新读取页表。
我见过有人只做了 DSB,忘了 ISB,结果 MMU 还在用旧的页表缓存,系统跑着跑着就挂了。这两个指令缺一不可。