2、MMU基础概念:虚拟地址与物理地址、页表与页表项、转换粒度(4K/16K/64K)、地址空间
好,咱们正式开始啃 MMU 这块硬骨头。很多做嵌入式开发的朋友,一听到 MMU 就觉得头大,觉得那是跑 Linux 才需要操心的东西。其实不然,裸机程序、RTOS 里,MMU 同样能帮你做很多事情。今天这一章,咱们就把 MMU 最核心的几个概念掰开揉碎了讲清楚。
2.1 虚拟地址与物理地址:一个“骗术”的开始
说白了,MMU 干的事就是一个“地址翻译”的活儿。CPU 执行指令时,拿到的地址是 虚拟地址(VA,Virtual Address),经过 MMU 硬件翻译之后,才变成真正去访问内存、外设寄存器的 物理地址(PA,Physical Address)。
你想想看,为什么要有这么一层“欺骗”?
- 隔离性:每个进程/任务都以为自己独占整个地址空间,互不干扰。A 进程的 0x1000 和 B 进程的 0x1000,翻译后可能是完全不同的物理地址。
- 安全性:用户态程序无法直接访问内核空间,MMU 在翻译时会检查权限。
- 简化链接:链接脚本里可以统一用虚拟地址,不用关心物理内存到底在哪儿。
核心公式: CPU 执行 → 虚拟地址 (VA) → MMU 查表 → 物理地址 (PA) → 访问内存
我在做早期一个 Cortex-A7 的裸机项目时,就犯过傻。直接往某个物理地址写数据,结果程序跑飞了。后来才发现,MMU 默认是开启的,我写的那个地址被映射到了别处。嗯,从那以后,我每次上电第一件事就是确认 MMU 的状态。
2.2 页表与页表项:翻译的“字典”
MMU 怎么知道 VA 对应哪个 PA?靠的就是 页表(Page Table)。页表是一张存放在内存里的表格,每一行就是一个 页表项(PTE,Page Table Entry)。
一个典型的页表项里,包含了这些关键信息:
| 字段 | 说明 |
|---|---|
| 物理地址基址 | 映射到的物理页框起始地址 |
| 访问权限 | 读、写、执行、用户/特权级 |
| 属性位 | 可缓存、可共享、非安全等 |
| 有效位 | 该映射是否有效 |
ARMv8 的页表是分级的,最常见的是 4 级页表(L0, L1, L2, L3)。为什么要分级?
- 节省内存:如果只用一级页表,4GB 地址空间用 4KB 粒度,需要 100 多万个页表项,每个 8 字节,光页表就占 8MB 内存。分级后,只有真正用到的地址范围才会创建页表。
- 支持大块映射:L1 页表项可以直接映射 1GB 的块,L2 可以映射 2MB 的块,L3 才是 4KB 的页。这样,像 DDR 这种大块连续内存,一个页表项就搞定了。
我的习惯: 在调试 MMU 问题时,我通常会先打印出当前使用的页表基地址寄存器(TTBR0_EL1),然后手动解析几个关键虚拟地址对应的页表项,看看物理地址对不对、权限对不对。这比光看代码靠谱多了。
2.3 转换粒度:4K / 16K / 64K
ARMv8 支持三种页面大小,也就是 转换粒度(Translation Granule):
- 4KB:最常用,兼容性好,Linux 默认就是它。粒度小,内存利用率高,但页表层级多(4级),TLB 压力大。
- 16KB:介于两者之间,某些场景下可以减少页表层级。
- 64KB:粒度大,页表层级少(2级或3级),TLB 可以覆盖更大的内存范围。适合大内存、高性能计算场景。
| 粒度 | 页表级数 | 最大映射范围(单级) | 典型场景 |
|---|---|---|---|
| 4KB | 4 (L0-L3) | L1: 1GB, L2: 2MB, L3: 4KB | 通用 Linux, RTOS |
| 16KB | 3 (L0-L2) | L1: 2GB, L2: 32MB, L3: 16KB | 存储服务器 |
| 64KB | 2 (L0-L1) | L0: 512GB, L1: 64KB | 高性能计算, GPU |
注意: 转换粒度是在系统启动时通过 TCR_EL1 寄存器配置的,一旦设定,整个系统的页表结构就固定了。你不能在运行中随意切换。我曾经在一个项目中,因为启动代码里 TCR 配置错了,导致 4KB 粒度的页表被当成了 64KB 来解析,结果 MMU 一开就挂,查了整整两天。
2.4 地址空间:32位与64位的差异
ARMv8 支持两种执行状态:AArch32 和 AArch64。它们的地址空间差别很大。
- AArch32:虚拟地址空间最大 4GB(32位)。通常分为用户空间(0x00000000 - 0xBFFFFFFF)和内核空间(0xC0000000 - 0xFFFFFFFF),这是 Linux 的经典 3:1 划分。
- AArch64:虚拟地址空间理论上是 2^64,但实际实现中,ARMv8 通常只使用低 48 位(256TB)或 52 位(4PB)。地址空间被分为两个区域:低地址区域(用户空间)和高地址区域(内核空间),中间有一大段“空洞”。
为什么要有空洞?说白了,是为了兼容性和未来扩展。你想想看,如果现在就把 64 位地址全用上,那页表得多大?硬件也扛不住。
下面这张图,是我自己画的一个 AArch64 地址空间布局,帮你直观理解:
在实际项目中,我建议你根据芯片的 物理内存大小 和 外设地址映射 来规划虚拟地址空间。比如,你的芯片只有 512MB DDR,那虚拟地址空间完全没必要映射到 256TB,只需要映射实际用到的部分就行,这样可以大大减小页表占用的内存。
避坑指南: 我曾经在一个项目里,因为把外设寄存器的物理地址映射到了用户空间,并且忘记设置“非缓存”属性,结果 CPU 读到的寄存器值永远是旧数据。后来加上 MT_DEVICE_nGnRnE 属性才解决。记住:外设寄存器一定要用设备内存属性,不能用普通内存属性。
好了,这一章的概念比较多,但都是后面配置 MMU 的基础。你只要把“虚拟地址 vs 物理地址”、“页表结构”、“粒度选择”和“地址空间布局”这四个点吃透,后面写代码就会顺畅很多。