4. MMU与内存管理:页表结构、TLB操作、域访问控制与缓存策略

MMU这东西,说白了就是给CPU戴上一副“虚拟眼镜”。它让每个进程都以为自己独占整个4GB地址空间,实际上物理内存可能只有512MB。我早年做手机Bootloader时,第一次打开MMU就把系统搞挂了——原因很简单,页表配置错了。从那以后,我对MMU的每一个比特都格外小心。

ARMv7的MMU比ARM9/ARM11复杂得多,但也灵活得多。咱们从最核心的页表结构开始聊。

4.1 一级页表结构

ARMv7的一级页表(L1)有4096个表项,每个表项32位。它覆盖整个4GB地址空间,每个表项管理1MB的虚拟内存块。

关键点:一级页表必须放在物理地址16KB对齐的位置。TTBR0和TTBR1寄存器指向的就是这个基地址。

一级页表项的类型决定了这1MB区域怎么映射:

表项类型 描述 粒度
Section 直接映射为1MB的段 1MB
Supersection 映射16MB的大段(需要LPAE) 16MB
Page Table 指向二级页表,做细粒度映射 4KB/64KB
Fault 未映射,访问会产生缺页异常 —

我个人习惯:内核空间用Section映射,因为内核代码段、数据段通常很大,1MB粒度够用,还能省掉二级页表的内存开销。用户空间则用二级页表,方便按页管理。

4.2 二级页表结构

二级页表(L2)把一级页表里标记为“Page Table”的1MB区域进一步细分。ARMv7支持两种L2表项粒度:

  • Large Page(64KB):每个L2表项管理64KB,一张L2表有16个表项
  • Small Page(4KB):每个L2表项管理4KB,一张L2表有256个表项

二级页表本身占用4KB,必须4KB对齐。每个L2表项包含物理地址、访问权限、域、缓存属性等信息。

我的经验:嵌入式系统里,如果内存紧张,优先用Section映射。我在一个IoT项目里,光靠Section映射就省下了32KB的页表内存——这对只有256KB SRAM的芯片来说,简直是救命。

4.3 页表遍历与TLB操作

CPU访问虚拟地址时,MMU会做页表遍历(Page Table Walk)。这个过程分两步:

  1. 从TTBRx拿到一级页表基址,用虚拟地址的[31:20]位索引L1表项
  2. 如果L1表项指向L2页表,再用虚拟地址的[19:12]位索引L2表项

每次遍历都要访问内存,太慢了。所以ARMv7内置了TLB(Translation Lookaside Buffer),缓存最近用过的地址转换结果。

注意:TLB不是自动一致的。你修改了页表之后,必须手动使失效的TLB条目。否则CPU可能还在用旧的映射——我见过有人因为这个bug,调试了整整两天。

TLB操作主要通过CP15协处理器完成:

/* 使失效所有TLB条目 */
MRC p15, 0, r0, c8, c7, 0    /* TLBIALL */

/* 使失效指定ASID的TLB条目 */
MRC p15, 0, r0, c8, c3, 0    /* TLBIMVA,r0包含虚拟地址和ASID */

/* 使失效指定MVA的TLB条目 */
MRC p15, 0, r0, c8, c1, 0    /* TLBIMVAA,r0包含虚拟地址 */

我建议:每次切换进程时,一定要刷TLB。如果用了ASID(Address Space ID),可以只刷当前ASID的条目,避免全局刷新的性能损失。

4.4 域访问控制

ARMv7的域访问控制是个很巧妙的设计。它把内存空间分成16个域(Domain),每个域可以独立设置访问权限。

域控制寄存器(DACR)有16个2位的域字段:

值 权限 说明
00 No Access 任何访问都产生Domain Fault
01 Client 按页表里的权限检查
10 Reserved 不要用
11 Manager 不检查权限,直接通过

我在做安全启动时,就用域控制来隔离关键代码。把BootROM所在的域设为Manager,其他域设为Client。这样即使应用代码跑飞了,也碰不到BootROM区域。

实用技巧:把内核空间映射到Domain 0并设为Manager,用户空间映射到Domain 1并设为Client。这样内核访问自己的空间时,MMU直接放行,省掉权限检查的开销。

4.5 内存属性与缓存策略

ARMv7的内存属性由页表项里的TEX(Type Extension)、C(Cacheable)、B(Bufferable)三个位控制。这三个位组合起来,决定内存区域的缓存行为。

常用的组合:

TEX C B 内存类型 典型用途
000 0 0 Strongly Ordered 设备寄存器、共享内存
000 0 1 Device 外设内存
000 1 0 Normal, Write-Through 帧缓冲区
000 1 1 Normal, Write-Back 普通内存(代码、数据)
001 1 1 Normal, Write-Back, No Allocate 流式数据

你想想看,为什么设备寄存器要用Strongly Ordered?因为CPU不能对设备寄存器做乱序访问或合并写操作。我曾经在一个网卡驱动里,不小心把DMA描述符所在的内存设成了Write-Back,结果数据一直不刷新到内存,网卡收不到包——调试了三天才发现是缓存策略的问题。

避坑指南:多核系统里,不同核共享的数据结构必须用Strongly Ordered或Device类型。否则一个核写的数据可能只留在自己的L1缓存里,另一个核读到的还是旧值。我曾经在AMP系统里踩过这个坑,两个核通过共享内存通信,数据总是对不上——最后发现是缓存一致性问题。

4.6 页表配置实战

下面是一个实际的一级页表初始化代码片段,我习惯在启动汇编阶段就把它配好:

/* 假设TTBR0指向0x80000000,页表在0x70000000 */
ldr r0, =0x70000000      /* 页表基址 */
ldr r1, =0x80000000      /* 物理地址起始 */

/* 映射前256MB为Section,设备内存用Strongly Ordered */
mov r2, #256
1:
  /* 构建Section描述符 */
  orr r3, r1, #0xC02     /* Domain=0, Section, AP=11 */
  /* 设置TEX=000, C=0, B=0 -> Strongly Ordered */
  bic r3, r3, #0x38      /* 清除TEX位 */
  str r3, [r0], #4       /* 写入页表,地址递增 */
  add r1, r1, #0x100000  /* 物理地址加1MB */
  subs r2, r2, #1
  bne 1b

/* 映射后256MB为Normal, Write-Back */
/* ... 类似逻辑,设置TEX=000, C=1, B=1 ... */

/* 使失效TLB */
MRC p15, 0, r0, c8, c7, 0
DSB
ISB

这段代码把前256MB物理内存映射为Strongly Ordered(适合设备寄存器),后256MB映射为Normal Write-Back(适合代码和数据)。实际项目中,你还需要根据具体外设的地址范围做调整。

4.7 知识结构总览

下面这张图总结了MMU与内存管理的核心脉络:

ARMv7 MMU与内存管理知识结构 ARMv7 MMU与内存管理核心结构 MMU核心组件 页表结构 TLB操作 域访问控制 缓存策略 一级页表(L1) 二级页表(L2) Section/Supersection TLB遍历 TLB失效操作 ASID管理 DACR寄存器 16个域管理 Client/Manager权限 TEX/C/B位 Write-Back/Through Strongly Ordered/Device 核心原则:虚拟地址 → 页表遍历 → 域检查 → 权限检查 → 缓存策略 → 物理地址

这张图把MMU的五个核心模块串起来了。你从中心出发,沿着箭头往下看,每个分支都对应一个关键知识点。实际调试时,遇到内存访问异常,就按这个结构排查:先看页表映射对不对,再看域权限够不够,最后检查缓存策略是否合适。

我的调试习惯:遇到MMU相关的异常,我第一件事就是读DFSR(Data Fault Status Register)和IFSR(Instruction Fault Status Register)。这两个寄存器会告诉你是什么类型的错误——是Translation Fault、Domain Fault还是Permission Fault。对症下药,比瞎猜快得多。