MMU 与页表机制深度解析:ARMv8-A 架构下的内存管理

MMU,说白了就是 CPU 里的一个地址翻译器。你写的程序用的是虚拟地址,但物理内存只认物理地址。MMU 就是中间那个“翻译官”。在车载 SoC 里,这个翻译官尤其重要——它不光管地址转换,还管权限、缓存策略、执行权限。我做过好几个项目,内存映射配错了,轻则性能暴跌,重则系统直接挂掉。

ARMv8-A 架构下的 MMU,跟之前 ARMv7 比,变化很大。最核心的就是页表层级从两级变成了四级(L0-L3)。嗯,这里要注意,不是所有场景都需要四级,但理解四级结构是基本功。

MMU 核心工作流程

先看一张图,把整体流程理清楚:

ARMv8-A MMU 地址翻译流程 虚拟地址 VA TLB 查找(快表) 命中? 直接输出物理地址 PA 页表遍历(L0→L3) L0 页表(512GB 块) L1 页表(1GB 块) L2 页表(2MB 块) TLB 命中 直接出 PA 延迟 ≈ 1 周期 TLB 未命中 延迟 ≈ 几十周期

流程其实不复杂。CPU 发出虚拟地址,先查 TLB(快表)。TLB 命中就直接出物理地址,一个周期搞定。没命中就得走页表遍历,从 L0 一级一级往下查,直到找到对应的页表项。我在项目里遇到过 TLB 频繁未命中导致性能雪崩的情况,后来通过配置大页(2MB/1GB)才解决。

四级页表结构详解

ARMv8-A 的页表层级,从 L0 到 L3,每一级管的地址范围不一样:

层级 页表项数量 每项映射大小 典型用途
L0 512 512 GB 覆盖整个地址空间
L1 512 1 GB 大块内存区域映射
L2 512 2 MB 中等粒度映射
L3 512 4 KB 细粒度映射(默认页)

你想想看,为什么是 512 项?因为 ARMv8-A 的页表项大小是 8 字节,512 项正好占一个 4KB 页面。这个设计很巧妙——页表本身也放在内存里,用 4KB 页面管理,方便缓存。

关键点:页表遍历不是必须走完四级。如果 L1 页表项标记为“块映射”(Block Descriptor),就直接跳到最后,不再查 L2/L3。这就是大页(1GB/2MB)加速的原理。

页表项格式与属性控制

每个页表项(PTE)的 64 位里,包含了地址和属性。我挑几个最关键的属性说:

  • Lower Attr(bit[9:2]):访问权限、执行权限、安全状态
  • Upper Attr(bit[63:50]):缓存策略、共享属性
  • Output Address(bit[47:12]):物理页帧号

说白了,页表项就是一张“属性清单”。你配好它,MMU 就按这个清单执行。

缓存策略配置

车载 SoC 里,缓存策略配错了,后果很严重。我见过一个案例:DMA 和 CPU 共享内存,缓存策略没配成 non-cacheable,结果 DMA 写的数据 CPU 读不到,因为 CPU 读的是缓存里的旧数据。

ARMv8-A 的缓存策略通过 MAIR(Memory Attribute Indirection Register)和页表项里的 AttrIdx 字段配合实现:

AttrIdx MAIR 配置 缓存策略 适用场景
0 0x00 Non-cacheable DMA 共享内存、设备寄存器
1 0x44 Write-through 视频帧缓冲(读多写少)
2 0xFF Write-back 普通数据内存(性能最优)
3 0x04 Write-back no-allocate 流式数据(避免缓存污染)

我的习惯:在 BSP 初始化阶段,我会把 MAIR 的 8 个槽位全部配好,然后在页表里直接引用。这样后期调整缓存策略时,只需要改页表项的 AttrIdx,不用动 MAIR。

权限与执行控制

车载系统对安全要求极高。ARMv8-A 的页表提供了两级权限控制:

  • AP[2:1](Access Permission):控制读/写权限
  • PXN(Privileged Execute Never):禁止特权级执行
  • UXN(User Execute Never):禁止用户级执行

举个例子,内核代码段应该配成:AP=0b01(特权读写,用户只读),PXN=0,UXN=1。这样用户程序不能执行内核代码,但内核自己可以执行。

我曾经踩过的坑:在某个项目中,我把设备寄存器的页表配成了可执行(UXN=0)。结果一个野指针跳转到寄存器地址,CPU 试图执行那里的“指令”,直接触发异常。从那以后,所有非代码段我都强制设 UXN=1。

页表配置实战示例

下面是一个简化的页表初始化代码,展示如何配置 L2 页表项:

// 配置 L2 页表项(2MB 块映射)
void init_l2_page_table(uint64_t *l2_base, uint64_t phys_base, uint64_t attr_idx) {
    for (int i = 0; i < 512; i++) {
        uint64_t desc = phys_base + (i << 21);  // 物理地址
        desc |= 0x401;  // Block descriptor + AF
        desc |= (attr_idx << 2);  // AttrIdx
        desc |= (1 << 10);  // Access flag
        desc |= (3 << 8);   // AP: 特权读写, 用户只读
        desc |= (1 << 54);  // UXN: 禁止用户执行
        l2_base[i] = desc;
    }
}

这段代码里,0x401 是块描述符的标志位。AF(Access Flag)必须置 1,否则 MMU 会在第一次访问时触发异常去置位它,影响性能。

TLB 管理与维护

页表改完了,TLB 里的缓存还是旧的。这时候需要做 TLB 维护:

  • TLBI VMALLE1:无效化所有 TLB 条目(开销大,慎用)
  • TLBI VAE1IS:按虚拟地址无效化(精确,推荐)
  • DSB + ISB:确保维护指令执行完毕

我建议:在动态映射/取消映射时,尽量用按地址无效化,别动不动就全清。全清 TLB 会导致后续所有内存访问都 miss,性能瞬间掉一大截。

小结

ARMv8-A 的 MMU 和页表机制,核心就三件事:地址翻译、属性控制、权限管理。四级页表结构给了你足够的灵活性——大块内存用 L1/L2 块映射,小块内存用 L3 页映射。缓存策略和权限控制是车载系统的生命线,配错了就是事故。