4、内存管理单元(MMU):页表配置(4K/16K/64K页)、TLB管理策略、内存属性与域控制

好,咱们今天聊聊MMU。说实话,很多做嵌入式Linux或者RTOS的朋友,对MMU的态度就是“配好页表,开起来就完事了”。但在我这些年调过的三星芯片里,MMU配置的好坏,直接决定了系统是“丝般顺滑”还是“卡成PPT”。

MMU的核心工作,说白了就三件事:地址翻译、权限检查、属性控制。咱们一个一个拆开讲。

4.1 页表配置:4K / 16K / 64K 页的选择

页大小怎么选?这问题我当年刚接手一个Exynos项目时,也纠结过。ARM架构支持多种页粒度,最常见的就是4K、16K和64K。你想想看,这可不是随便选的,它直接影响TLB的命中率和内存的利用率。

核心原则:页越小,内存碎片越少,但页表越大,TLB覆盖范围越小。页越大,TLB覆盖范围大,但内部碎片多。

我个人习惯,在三星的Cortex-A系列上,一般这么选:

  • 4K页:通用场景,Linux默认。适合进程多、内存碎片敏感的场景。但页表项多,TLB压力大。
  • 16K页:折中方案。我在一个视频编解码项目里用过,效果不错。TLB命中率比4K高,内存浪费又比64K少。
  • 64K页:适合大块连续内存,比如多媒体buffer、GPU显存映射。TLB覆盖范围大,性能好。但小内存分配会浪费空间。

嗯,这里要注意,ARMv7-A和ARMv8-A的页表格式略有不同。以ARMv8-A为例,页表基地址寄存器是TTBR0_EL1和TTBR1_EL1。配置页表时,粒度由TCR_EL1中的TG0/TG1字段决定。

// 配置16KB页粒度示例(ARMv8-A)
TCR_EL1 = (TCR_EL1 & ~(3 << 14)) | (1 << 14);  // TG0 = 01, 16KB页
// 同时要调整TCR_EL1中的T0SZ,确保地址范围正确

避坑指南:我曾经在一个项目中,把页表粒度从4K改成64K,结果忘了调整T0SZ,导致高地址访问直接触发Translation Fault。排查了半天,最后发现是TCR配置没同步。记住,改了页大小,一定要重新计算地址偏移范围。

4.2 TLB管理策略

TLB,就是页表的硬件缓存。它的命中率,直接决定了你每次内存访问是“几拍搞定”还是“几十拍去查页表”。

三星的ARM处理器,TLB通常分为L1 TLB(指令和数据分离)和L2 TLB(统一)。L1 TLB很小,一般几十条;L2 TLB大一些,几百到上千条。

TLB管理,核心就两个操作:失效(Invalidate)锁定(Lockdown)

4.2.1 TLB失效策略

什么时候需要失效TLB?很简单,页表内容变了。比如你修改了页表项,或者切换了进程(ASID变了)。

失效粒度有三种:

  • 全部失效(TLBI ALL):简单粗暴,但性能差。所有TLB条目清空,后续访问全部miss。
  • 按ASID失效(TLBI ASID):只失效特定进程的TLB条目。进程切换时用这个,不影响其他进程。
  • 按虚拟地址失效(TLBI VA):只失效特定地址范围的条目。修改单个页表项时用,最精细。

我个人建议,能用VA级别就别用ALL。我在一个网络设备项目里,频繁切换进程,一开始用了TLBI ALL,结果性能惨不忍睹。改成按ASID失效后,吞吐量直接提升了15%。

// ARMv8-A TLB失效示例
// 全部失效(通常只在启动或SMP hotplug时用)
dsb ishst
tlbi alle1
dsb ish
isb

// 按ASID失效(进程切换时用)
dsb ishst
tlbi aside1, x0  // x0 = ASID值
dsb ish
isb

// 按虚拟地址失效(修改单页时用)
dsb ishst
tlbi vae1, x0    // x0 = 虚拟地址 + ASID
dsb ish
isb

注意:在多核系统中,TLB失效需要广播到所有核。ARM用TLB Invalidate Broadcast(TLBIB)机制。但如果你关掉了广播(比如在低功耗模式下),就得手动给每个核发IPI去执行TLBI。我踩过这个坑,一个核改了页表,另一个核还在用旧TLB,数据都写飞了。

4.2.2 TLB锁定策略

TLB锁定,就是把关键页表项“钉”在TLB里,不让它被替换出去。适合中断处理、实时任务等对延迟敏感的场景。

三星的ARM处理器,TLB锁定通常通过TLB Lockdown Register实现。但要注意,锁定的条目数有限,别锁太多,否则其他条目频繁miss反而得不偿失。

我的经验:在一个音频处理项目里,我把中断向量表和关键数据结构的页表项锁在了TLB里。中断响应时间从平均1.2us降到了0.8us。但只锁了4个条目,多了会影响普通任务的TLB命中率。

4.3 内存属性与域控制

内存属性,就是告诉MMU这块内存该怎么访问。域控制,则是权限管理的“大管家”。

4.3.1 内存属性

ARM的内存属性,主要分三类:

属性 说明 典型场景
Normal 普通内存,可缓存、可合并 DDR、堆栈、数据段
Device 设备内存,不可缓存,严格顺序 MMIO寄存器、FIFO
Strongly Ordered 强序内存,所有访问严格按序 中断控制器、同步寄存器

在页表项里,通过AttrIndx字段指向MAIR(Memory Attribute Indirection Register)中的属性编码。ARMv8-A有8个MAIR槽位,可以自定义组合。

// 配置MAIR示例
// MAIR0: 0x44 = Normal, Outer Write-Back, Inner Write-Back
// MAIR1: 0x04 = Device-nGnRE
MSR MAIR_EL1, 0x44
// 页表项中 AttrIndx = 0 时,使用MAIR0
// 页表项中 AttrIndx = 1 时,使用MAIR1

关键点:Device内存千万别设成可缓存的。我见过有人把UART寄存器配成Normal内存,结果写数据时被缓存了,串口半天没反应。调试时才发现,数据还在Cache里躺着呢。

4.3.2 域控制

域控制,是ARM MMU里一个很强大的权限管理机制。它把物理地址空间分成最多16个域(Domain),每个域可以独立配置访问权限。

域控制寄存器是DACR(Domain Access Control Register)。每个域占2位,共4种权限:

  • 00(No Access):任何访问都触发Domain Fault。
  • 01(Client):按页表项中的AP(Access Permission)位检查权限。
  • 10(Reserved):不要用,行为未定义。
  • 11(Manager):不检查权限,直接允许访问(但页表项中的XN、PXN等仍有效)。

我当年在调试一个安全启动方案时,就用域控制把Boot ROM所在的域设成了Manager,其他域设成Client。这样Boot ROM可以访问任何内存,但普通程序只能访问自己的域,实现了硬件级别的隔离。

避坑指南:我曾经把某个外设的域设成了Manager,想着省事。结果一个野指针写到了那个外设的寄存器,系统直接挂了。Manager模式虽然不检查AP,但千万别滥用。它应该只用于最核心、最可信的代码段。

4.4 实战建议

好了,总结一下我的个人经验:

  1. 页大小:别盲目用4K。如果你的应用有大块连续内存(比如多媒体、AI推理),试试64K页,TLB命中率提升很明显。
  2. TLB失效:能用VA级别就别用ALL。多核系统注意广播问题。
  3. TLB锁定:只锁最关键的几个条目,别贪多。
  4. 内存属性:Device内存一定不要缓存。Normal内存根据性能需求选Write-Back或Write-Through。
  5. 域控制:Manager模式是双刃剑,只在绝对信任的代码段使用。

嗯,MMU这块内容其实很深,今天咱们只讲了最核心的页表、TLB和属性控制。下一章我会聊聊Cache的一致性问题,那才是真正让人头疼的地方。到时候见。