3. 内存管理单元(MMU)基础:页表结构、TLB与缓存操作

好,咱们进入第三章。MMU,说白了就是CPU的地址翻译官。你写的程序里用的虚拟地址,怎么变成物理内存上的真实地址?全靠它。我早年做第一个Cortex-A项目时,上来就把MMU关了跑裸机,结果一开MMU系统直接挂——后来才发现是页表配置错了。嗯,这章咱们就把这块啃透。

3.1 页表结构:4KB与64KB粒度

ARMv8的页表,用的是多级页表。说白了就是查表找地址,一级一级往下翻。粒度有两种常用:4KB和64KB。我个人习惯,大部分场景用4KB,内存紧张时考虑64KB。

核心概念: 页表就是一张“虚拟地址 → 物理地址”的映射表。ARMv8支持4级页表(4KB粒度)或3级页表(64KB粒度)。

3.1.1 4KB粒度页表结构

4KB粒度下,页表分4级:

  • Level 0: 覆盖整个地址空间,每个表项指向下一级表
  • Level 1: 每个表项覆盖1GB区域
  • Level 2: 每个表项覆盖2MB区域
  • Level 3: 每个表项覆盖4KB区域——这就是最终页

你想想看,查一次地址要经过4次内存访问,效率是不是有点低?所以ARM引入了TLB来缓存最近用过的翻译结果。这个后面会讲。

3.1.2 64KB粒度页表结构

64KB粒度下,页表只有3级:

  • Level 0: 覆盖整个地址空间
  • Level 1: 每个表项覆盖64GB区域
  • Level 2: 每个表项覆盖64KB区域——最终页

我在项目中遇到过,用64KB粒度时页表本身占的内存更少,但内部碎片会多一些。适合大块连续内存的场景。

特性 4KB粒度 64KB粒度
页表级数 4级 3级
最小页大小 4KB 64KB
页表内存占用 较大 较小
内部碎片 少 多
适用场景 通用、细粒度控制 大块连续内存、减少页表层级

3.2 转换表描述符

页表里的每一项,叫描述符。它不光存地址,还带了一堆属性。我刚开始看ARM手册时,被这些位域搞得头晕。后来总结了一下,其实就三类:

  • 无效描述符: 这一项没映射,访问就触发缺页异常
  • 表描述符: 指向下一级页表(非叶子节点)
  • 块描述符/页描述符: 指向物理内存块(叶子节点)
我的小技巧: 调试时先检查描述符的bit 0(有效位)。如果为0,说明页表项根本没配。我曾经花了两天查一个内存访问异常,最后发现是描述符的valid位没置1。

描述符里还有几个关键字段:

  • AttrIndx[2:0]: 指向MAIR(内存属性寄存器)的索引,决定这块内存的缓存策略
  • NS: 非安全空间位,安全世界和非安全世界切换时用
  • AP[2:1]: 访问权限位,控制读/写权限
  • SH[1:0]: 共享属性,决定多核间的缓存一致性
  • AF: 访问标志位,硬件访问后自动置1

3.3 TLB与缓存操作

TLB,全称Translation Lookaside Buffer。说白了就是页表的硬件缓存。每次查页表都要走内存,太慢了,TLB把最近用过的翻译结果存下来,下次直接命中。

但TLB有个坑——它不会自动感知页表的变化。你改了页表,TLB里还是旧数据。这时候就需要手动操作TLB。

3.3.1 TLB失效操作

ARMv8提供了几条指令来管理TLB:

// 使所有TLB条目失效(EL1下)
TLBI VMALLE1

// 使特定虚拟地址的TLB条目失效
TLBI VAE1, x0   // x0里放虚拟地址

// 使特定ASID的TLB条目失效
TLBI ASIDE1, x0 // x0里放ASID

// 同步指令,确保TLB操作完成
DSB ISH
ISB
注意: TLB操作后一定要加DSB和ISB。我曾经在项目里漏了ISB,结果TLB失效没生效,新页表没被使用,系统跑飞了。嗯,那次排查花了我一整天。

3.3.2 缓存操作

ARMv8的缓存分L1、L2、L3。操作缓存时,要清楚你在哪个异常级别。常用的缓存操作指令:

  • DC CIVAC: 清理并使无效(Clean and Invalidate)——写回脏数据并失效
  • DC CVAC: 只清理(Clean)——写回脏数据
  • DC IVAC: 只使无效(Invalidate)——丢掉缓存数据
  • IC IALLU: 使所有指令缓存失效
// 清理并使无效一个缓存行(按虚拟地址)
DC CIVAC, x1

// 同步
DSB SY

// 使指令缓存失效
IC IALLU
DSB SY
ISB

3.4 内存属性与域控制

内存属性,说白了就是告诉MMU这块内存该怎么对待。ARMv8用MAIR(Memory Attribute Indirection Register)来定义属性。

3.4.1 MAIR寄存器

MAIR有8个槽位,每个槽位8位,定义一种内存属性。我常用的配置:

// 设置MAIR
// 槽位0:正常内存,写回策略,读分配
// 槽位1:设备内存,非缓存
// 槽位2:正常内存,直写策略

LDR x0, =0x00FF44EE  // 示例值,具体按需配置
MSR MAIR_EL1, x0
槽位 属性 典型用途
Attr0 正常内存,写回,读分配 普通数据区
Attr1 设备内存,非缓存 MMIO、外设寄存器
Attr2 正常内存,直写 帧缓冲区、共享内存
Attr3 正常内存,不可缓存 DMA缓冲区

3.4.2 域控制

域控制,在ARMv8里其实已经弱化了。早期ARM有域控制寄存器,用来做内存访问权限的粗粒度控制。到了v8,域控制基本被页表里的AP位和PXN/UXN位替代了。

不过有个概念要记住:XN(Execute Never)位。标记了不可执行的页面,防止代码注入攻击。我在做安全启动时,把关键数据区都设了XN,防止被恶意代码执行。

避坑指南: 设备内存(Device memory)一定要设成非缓存。我曾经把GPIO寄存器配成了写回策略,结果写寄存器时数据被缓存在L1里,外设根本没收到——嗯,那次排查让我学会了看MAIR配置。

3.5 知识体系总览

下面这张图,把MMU的核心模块串起来了。你看一遍,心里就有谱了。

ARMv8 MMU核心结构图 ARMv8 MMU核心结构 虚拟地址 (VA) TLB(翻译后备缓冲器) 未命中 命中 页表查询(4级/3级) 描述符解析 内存属性(MAIR) 物理地址 (PA) 关键操作 • TLB失效:TLBI • 缓存操作:DC/IC • 同步:DSB/ISB 描述符关键位 • Valid: bit 0 • AP: 访问权限 • XN: 不可执行

这张图从左到右,就是一次完整的内存访问流程。虚拟地址进来,先查TLB。命中就直接拿物理地址。没命中就走页表查询,一级一级解析描述符,最后拿到物理地址和内存属性。嗯,流程清楚了,后面写代码心里就有底了。