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的查找过程,其实是一个多级索引的过程。虚拟地址被拆分成多个部分,每一级索引对应一级页表。每一级页表项指向下一级页表的基地址,直到最后一级页表项指向物理页面。

ARMv9 4级页表查找流程 虚拟地址 VA[63:0] L0索引 L1索引 L2索引 L3索引 + 偏移 L0页表 L1页表 L2页表 L3页表 物理地址 PA 每级页表项指向下一级基址

3.2 页表查找过程详解

页表查找,也叫页表遍历。当CPU执行一条指令,访问一个虚拟地址时,MMU会按以下步骤操作:

  1. TLB查找:先看TLB里有没有这个虚拟地址对应的转换。如果有,直接命中,返回物理地址。这一步很快,通常1-2个时钟周期。
  2. 页表遍历:TLB未命中,MMU自动从内存中读取页表。从TTBRx_ELx寄存器拿到页表基地址,然后逐级查找。
  3. 属性检查:找到页表项后,检查访问权限、内存类型、安全状态等。如果权限不匹配,触发异常。
  4. 更新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解决缓存一致性的机制,主要有:

  1. 硬件一致性:ACE/CHI总线协议,保证CPU和DMA之间的缓存一致性。硬件自动监听和更新。
  2. 软件维护:通过DC(数据缓存)和IC(指令缓存)指令,手动刷新缓存。比如修改页表后,必须执行DSB和ISB指令。
  3. 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的典型流程:

  1. 设置MAIR_EL1,定义内存属性(比如Normal内存、Device内存)。
  2. 设置TCR_EL1,配置页表粒度和地址范围。
  3. 设置TTBR0_EL1,指向页表基地址。
  4. 执行DSB和ISB,确保配置生效。
  5. 设置SCTLR_EL1的M位,使能MMU。
  6. 再次执行DSB和ISB。

注意:使能MMU时,必须确保当前执行的代码地址是恒等映射的,否则使能后PC指向的虚拟地址无法转换,直接死机。我见过有人在这里栽跟头,调试了半天才发现是映射没做好。

嗯,MMU的基础就这些。理解页表查找过程、TLB的作用和缓存一致性维护,是后续学习虚拟化内存管理的前提。你想想看,如果没有MMU,虚拟化根本不可能实现——每个虚拟机都需要独立的地址空间,MMU就是那个隔离的基石。