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。
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),增加切换延迟
1.5 嵌入式系统中的MMU初始化流程
在嵌入式Linux启动过程中,MMU的初始化是一个分阶段的过程:
- Boot ROM阶段:MMU关闭,CPU直接使用物理地址执行启动代码
- Bootloader阶段(如U-Boot):可临时开启MMU以加速加载,但通常保持关闭
- 内核早期启动(head.S):创建恒等映射(Identity Mapping),开启MMU
- 内核内存管理初始化:建立完整的页表,包括内核空间和用户空间映射
- 设备驱动加载:通过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);