3、内存管理单元基础:MMU架构、页表查找过程、TLB与缓存一致性
内存管理单元,简称MMU,是CPU内部一个关键硬件模块。说白了,它的职责就是完成虚拟地址到物理地址的转换。你写的程序里用的地址都是虚拟地址,真正访问DRAM时得用物理地址。MMU就是中间那个翻译官。
我刚开始接触ARMv8的时候,总觉得MMU就是个查表器,没什么复杂的。后来做虚拟化项目,才发现MMU的设计直接决定了系统性能和隔离能力。ARMv9在这方面继承并强化了v8的架构,理解它,是后续学习虚拟化和内存加密的基础。
3.1 MMU核心架构
ARMv9的MMU架构,核心由三部分组成:页表遍历单元、TLB和属性检查逻辑。页表遍历单元负责从内存中读取页表项;TLB是页表项的缓存;属性检查逻辑则判断访问权限、内存类型等。
关键点:ARMv9的MMU支持最多4级页表(L0到L3),页表粒度可以是4KB、16KB或64KB。我建议你默认使用4KB粒度,兼容性最好,也最常用。
MMU的查找过程,其实是一个多级索引的过程。虚拟地址被拆分成多个部分,每一级索引对应一级页表。每一级页表项指向下一级页表的基地址,直到最后一级页表项指向物理页面。
3.2 页表查找过程详解
页表查找,也叫页表遍历。当CPU执行一条指令,访问一个虚拟地址时,MMU会按以下步骤操作:
- TLB查找:先看TLB里有没有这个虚拟地址对应的转换。如果有,直接命中,返回物理地址。这一步很快,通常1-2个时钟周期。
- 页表遍历:TLB未命中,MMU自动从内存中读取页表。从TTBRx_ELx寄存器拿到页表基地址,然后逐级查找。
- 属性检查:找到页表项后,检查访问权限、内存类型、安全状态等。如果权限不匹配,触发异常。
- 更新TLB:将新的转换结果写入TLB,下次访问就快了。
我的经验:页表遍历是MMU最耗时的操作。一次遍历可能需要多次内存访问,如果页表不在缓存中,延迟可能高达几百个时钟周期。所以TLB命中率至关重要。
页表项的结构,ARMv9定义得很清晰。每个页表项64位,包含物理地址、访问标志、权限位、内存属性等。我习惯用表格来理解:
| 位域 | 名称 | 说明 |
|---|---|---|
| [63:48] | 物理地址高位 | 物理地址的高16位,与低位拼接 |
| [47:12] | 物理地址低位 | 4KB页框的物理地址 |
| [11] | nG | 非全局位,用于进程切换 |
| [10] | AF | 访问标志位,必须置1 |
| [7:6] | AP | 访问权限,控制读写 |
| [5] | NS | 非安全位,用于安全世界 |
| [4] | XN | 不可执行位 |
| [1:0] | 描述符类型 | 00=无效,01=页表,10=块,11=页 |
注意:AF位必须由软件在第一次访问时置1。如果AF为0,MMU会触发访问标志异常。我曾经在项目里忘了处理这个异常,导致系统反复进入异常处理,性能惨不忍睹。
3.3 TLB设计与操作
TLB是MMU的缓存,存储最近使用的虚拟地址到物理地址的映射。ARMv9的TLB分为多个层级:L1 TLB和L2 TLB。L1 TLB通常很小,但速度极快;L2 TLB更大,但稍慢。
TLB的条目,除了虚拟地址和物理地址,还包含ASID(地址空间标识符)和VMID(虚拟机标识符)。ASID用于区分不同进程,VMID用于区分不同虚拟机。这样,多个进程或虚拟机的TLB条目可以共存,不用每次切换都刷TLB。
TLB的操作,主要有三种:
- TLB失效:清除TLB中的特定条目。ARMv9提供了多种失效指令,比如TLBI VMALLE1IS,失效所有EL1的TLB条目。
- TLB锁定:将关键条目锁定在TLB中,防止被替换。这个功能在实时系统中很有用。
- TLB预取:软件可以主动将页表项加载到TLB中,减少后续访问的延迟。
避坑指南:我曾经在虚拟化环境中,因为TLB失效粒度不对,导致虚拟机切换后访问了错误的物理地址。记住,切换虚拟机时要用VMID相关的失效指令,只失效当前VM的TLB条目,别把其他VM的也清了。
3.4 缓存一致性问题
缓存一致性,是MMU和缓存之间的大问题。MMU修改了页表项,但TLB里还存着旧的映射。或者,CPU通过缓存访问了数据,但DMA设备直接写入了内存。这些都会导致不一致。
ARMv9解决缓存一致性的机制,主要有:
- 硬件一致性:ACE/CHI总线协议,保证CPU和DMA之间的缓存一致性。硬件自动监听和更新。
- 软件维护:通过DC(数据缓存)和IC(指令缓存)指令,手动刷新缓存。比如修改页表后,必须执行DSB和ISB指令。
- TLB维护:修改页表后,必须执行TLB失效指令,并配合DSB保证指令顺序。
我建议你记住一个原则:修改页表后,一定要执行完整的维护序列。否则,后果可能是数据错误、系统崩溃,甚至安全漏洞。
// 修改页表后的典型维护序列
// 假设我们修改了虚拟地址 0x1000 对应的页表项
// 1. 修改页表项(写内存)
*(uint64_t *)page_table_entry = new_value;
// 2. 确保页表写完成
DSB(ISH); // 数据同步屏障
// 3. 失效TLB中对应的条目
TLBI(VAE1IS, 0x1000); // 失效EL1的TLB条目
// 4. 确保TLB失效完成
DSB(ISH); // 数据同步屏障
// 5. 确保后续指令看到新的转换
ISB(); // 指令同步屏障
我的习惯:我通常把页表修改和缓存维护封装成一个函数,避免遗漏。每次修改页表,都调用这个函数,形成肌肉记忆。
3.5 实战中的MMU配置
配置MMU,需要设置几个关键寄存器:
- SCTLR_EL1:系统控制寄存器,使能MMU和缓存。
- TTBR0_EL1 / TTBR1_EL1:页表基地址寄存器,分别用于低地址和高地址空间。
- TCR_EL1:翻译控制寄存器,配置页表粒度、地址大小、缓存策略等。
- MAIR_EL1:内存属性寄存器,定义内存类型和缓存策略。
配置MMU的典型流程:
- 设置MAIR_EL1,定义内存属性(比如Normal内存、Device内存)。
- 设置TCR_EL1,配置页表粒度和地址范围。
- 设置TTBR0_EL1,指向页表基地址。
- 执行DSB和ISB,确保配置生效。
- 设置SCTLR_EL1的M位,使能MMU。
- 再次执行DSB和ISB。
注意:使能MMU时,必须确保当前执行的代码地址是恒等映射的,否则使能后PC指向的虚拟地址无法转换,直接死机。我见过有人在这里栽跟头,调试了半天才发现是映射没做好。
嗯,MMU的基础就这些。理解页表查找过程、TLB的作用和缓存一致性维护,是后续学习虚拟化内存管理的前提。你想想看,如果没有MMU,虚拟化根本不可能实现——每个虚拟机都需要独立的地址空间,MMU就是那个隔离的基石。