MMU 与页表机制深度解析:ARMv8-A 架构下的内存管理
MMU,说白了就是 CPU 里的一个地址翻译器。你写的程序用的是虚拟地址,但物理内存只认物理地址。MMU 就是中间那个“翻译官”。在车载 SoC 里,这个翻译官尤其重要——它不光管地址转换,还管权限、缓存策略、执行权限。我做过好几个项目,内存映射配错了,轻则性能暴跌,重则系统直接挂掉。
ARMv8-A 架构下的 MMU,跟之前 ARMv7 比,变化很大。最核心的就是页表层级从两级变成了四级(L0-L3)。嗯,这里要注意,不是所有场景都需要四级,但理解四级结构是基本功。
MMU 核心工作流程
先看一张图,把整体流程理清楚:
流程其实不复杂。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 页映射。缓存策略和权限控制是车载系统的生命线,配错了就是事故。