1. 内存管理单元(MMU)概述

内存管理单元(Memory Management Unit, MMU)是嵌入式处理器中负责虚拟地址到物理地址转换的硬件模块。它位于CPU核心与物理内存之间,是操作系统实现进程隔离、内存保护和虚拟内存的基础设施。在嵌入式Linux系统中,MMU的配置直接决定了系统的稳定性、安全性和DMA缓冲区的设计策略。

1.1 MMU的核心作用

MMU在嵌入式系统中的核心作用可以归纳为以下四个方面:

作用 描述 嵌入式场景价值
地址转换 将CPU发出的虚拟地址转换为物理地址 允许进程使用连续的虚拟地址空间,即使物理内存碎片化
内存保护 检查访问权限,防止进程越界访问 防止用户态程序破坏内核或其它进程的数据
缓存控制 管理页表项中的缓存属性(Cacheable/Non-cacheable) DMA缓冲区必须配置为Non-cacheable或使用Cache协同
虚拟化支持 提供第二级地址转换(Stage-2 translation) 支持虚拟化场景下的Guest OS隔离
关键认知: 在嵌入式系统中,MMU不是可选项而是必选项。即使运行裸机程序,现代ARM Cortex-A系列处理器在启动后也必须完成MMU初始化,否则无法使用L1/L2 Cache,性能会大幅下降。

1.2 虚拟地址与物理地址

理解虚拟地址(Virtual Address, VA)与物理地址(Physical Address, PA)的关系是掌握MMU的第一步。CPU执行指令时使用的地址是虚拟地址,而实际访问DRAM或外设寄存器时使用的是物理地址。MMU通过页表(Page Table)完成两者之间的映射。

地址空间对比: 在32位ARM Linux系统中,虚拟地址空间为4GB(0x00000000 ~ 0xFFFFFFFF),其中用户空间占3GB(0x00000000 ~ 0xBFFFFFFF),内核空间占1GB(0xC0000000 ~ 0xFFFFFFFF)。物理地址空间则取决于实际硬件配置,通常远小于4GB。
虚拟地址到物理地址转换流程 虚拟地址 → 物理地址转换流程 CPU发出VA MMU查询TLB 页表遍历 获取PA TLB Miss 说明: TLB(Translation Lookaside Buffer)是MMU内部的页表缓存。TLB命中时直接返回物理地址,无需访问内存中的页表。 TLB Miss时,MMU硬件自动遍历页表(Page Table Walk),将结果填入TLB后继续执行。

1.3 内存保护机制

MMU的内存保护机制通过页表项中的权限位(Permission Bits)实现。每个页表项都包含以下关键保护属性:

  • 访问权限(AP):区分内核态(Privileged)和用户态(Unprivileged)的读写权限
  • 域(Domain):ARM架构特有的保护域机制,支持16个域,每个域可配置为No Access/Client/Manager
  • 可执行(XN/EXEC):禁止在数据页上执行代码(NX位),防止缓冲区溢出攻击
  • 共享属性(Shareable):控制多核处理器间的缓存一致性行为
保护属性 ARMv7-A页表项位域 典型配置 安全意义
AP[2:0] 页表描述符[10:9] + [6] 内核RW, 用户RO 防止用户态篡改内核数据
Domain 一级描述符[8:5] 内核域=0, 用户域=1 隔离不同安全级别的内存区域
XN 二级描述符[0] 数据段XN=1, 代码段XN=0 阻止栈/堆上的代码执行
Shareable 页表项[18:17] DMA缓冲区Non-shareable 避免不必要的Cache同步开销
常见陷阱: 在配置DMA缓冲区时,必须将对应的页表项设置为Non-cacheable(或使用特定的Cache协同策略)。如果DMA缓冲区被映射为Cacheable,CPU和DMA控制器看到的数据可能不一致——CPU读到的是Cache中的旧数据,而DMA已经将新数据写入物理内存。这是嵌入式开发中最常见的内存一致性问题之一。

1.4 MMU与系统性能的权衡

MMU的引入虽然带来了内存保护和虚拟化的好处,但也引入了额外的性能开销。嵌入式系统专家需要理解以下权衡:

  • TLB Miss开销:一次TLB Miss可能触发多次内存访问(Page Table Walk),在ARM Cortex-A9上约需30~50个CPU周期
  • 页表大小:4KB小页粒度提供精细保护,但页表占用内存大;1MB/2MB大页减少TLB压力,但内存浪费增加
  • 上下文切换:切换进程时需刷新TLB(或使用ASID),增加切换延迟
MMU性能权衡架构图 MMU性能权衡架构 MMU核心 ✅ 优势 进程隔离 · 内存保护 ⚠️ 代价 TLB Miss · 页表开销 关键权衡参数: • 页粒度:4KB小页 → 精细保护,但TLB覆盖范围小 • 大页(1MB/2MB)→ TLB覆盖范围大,但内存碎片增加 • 嵌入式建议:关键代码段使用大页,DMA缓冲区使用Non-cacheable映射

1.5 嵌入式系统中的MMU初始化流程

在嵌入式Linux启动过程中,MMU的初始化是一个分阶段的过程:

  1. Boot ROM阶段:MMU关闭,CPU直接使用物理地址执行启动代码
  2. Bootloader阶段(如U-Boot):可临时开启MMU以加速加载,但通常保持关闭
  3. 内核早期启动(head.S):创建恒等映射(Identity Mapping),开启MMU
  4. 内核内存管理初始化:建立完整的页表,包括内核空间和用户空间映射
  5. 设备驱动加载:通过ioremap()、dma_alloc_coherent()等API创建特定映射
实战要点: 在编写DMA驱动程序时,必须明确当前使用的地址空间。CPU访问DMA缓冲区使用虚拟地址,DMA控制器使用物理地址。使用dma_alloc_coherent()可以自动创建Non-cacheable映射并返回CPU虚拟地址和DMA物理地址。如果使用dma_map_single(),则需要在DMA传输前后手动处理Cache一致性(dma_sync_single_for_device / dma_sync_single_for_cpu)。
/* 典型DMA缓冲区分配代码示例 */
#include <linux/dma-mapping.h>

/* 分配一致性DMA缓冲区(Non-cacheable) */
dma_addr_t dma_handle;
void *cpu_addr = dma_alloc_coherent(dev, size, &dma_handle, GFP_KERNEL);

/* cpu_addr: CPU使用的虚拟地址(Non-cacheable映射) */
/* dma_handle: DMA控制器使用的物理地址 */

/* 使用完毕后释放 */
dma_free_coherent(dev, size, cpu_addr, dma_handle);