3. MMU 与地址空间基础
在 Linux 内核中,CPU 访问内存时使用的地址并不是内存条上的真实物理位置,而是经过 MMU(Memory Management Unit,内存管理单元)转换后的虚拟地址。理解虚拟地址、物理地址、页表、TLB 以及缓存一致性,是掌握 DMA 映射与 SMMU 配置的前提。本节从 CPU 视角与设备视角两条线出发,建立地址转换的整体认知。
一、核心原理:虚拟地址到物理地址的转换链路
CPU 执行指令时发出的是虚拟地址(VA)。MMU 依据当前进程的页表基址寄存器(如 ARM64 的 TTBR0_EL1/TTBR1_EL1)逐级查表,将 VA 翻译为物理地址(PA),再通过总线访问 DRAM。页表通常采用多级结构,以 ARM64 4KB 页、48 位 VA 为例,分为 4 级:PGD → PUD → PMD → PTE,每级索引 9 位,页内偏移 12 位。
页表项(PTE)不仅保存物理页帧号(PFN),还携带权限位(AP)、可执行位(UXN/PXN)、可共享性(SH)、内存属性(如 Normal、Device-nGnRnE)以及有效位。这些属性直接决定该页是否可缓存、是否允许设备访问,是 DMA 场景中必须关注的关键字段。
二、TLB 与缓存一致性
为避免每次访问都遍历多级页表,MMU 内部维护 TLB(Translation Lookaside Buffer),缓存最近使用的 VA→PA 映射。TLB 命中时转换几乎无开销;未命中则触发 page table walk。当内核修改页表(如建立或撤销映射)后,必须执行 TLB 失效操作,否则旧映射可能继续生效。
缓存一致性包含两个层面:一是 CPU 数据缓存与内存之间的一致性,二是 CPU 缓存与设备可见内存之间的一致性。对于非一致性(non-coherent)设备,驱动在 DMA 传输前后需要执行 cache clean/invalidate 操作,确保设备读到的是最新数据、CPU 读到的是设备写入的数据。ARM64 上常用 dma_map_single()、dma_sync_single_for_device()、dma_sync_single_for_cpu() 等 API 来封装这些操作。
三、设备视角的地址转换需求
设备(如网卡、NVMe、GPU)通过 DMA 引擎直接读写内存,它“看到”的地址空间与 CPU 不同。传统模式下,设备使用物理地址,驱动通过 virt_to_phys() 或 DMA API 获取总线地址。这种方式存在两个问题:一是设备只能访问物理连续内存,碎片化场景下难以满足;二是设备可以访问任意物理内存,缺乏隔离与保护。
SMMU(System MMU,ARM 架构下的 IOMMU)引入后,设备发出的地址先经过 SMMU 翻译。SMMU 使用 Stream ID 区分不同设备,查表得到物理地址,并施加权限与属性检查。这样设备可以使用 IOVA,驱动通过 dma_alloc_coherent() 或 dma_map_sg() 获得设备可见地址,同时实现地址隔离、受限访问和更大的连续地址空间。
四、案例与场景:网卡 DMA 收包
以网卡收包为例,驱动首先通过 dma_alloc_coherent() 分配一块一致性内存作为描述符环,得到 CPU 虚拟地址和 DMA 地址。网卡将收到的数据包写入该内存,并更新描述符状态。驱动在读取描述符前,若设备为非一致性,需要调用 dma_sync_single_for_cpu() 使 CPU 缓存失效,确保读到设备写入的最新内容。
/* 简化示例:分配一致性 DMA 内存 */
dma_addr_t dma_handle;
void *cpu_addr;
cpu_addr = dma_alloc_coherent(dev, size, &dma_handle, GFP_KERNEL);
if (!cpu_addr)
return -ENOMEM;
/* 将 DMA 地址写入设备寄存器 */
writel(lower_32_bits(dma_handle), dev_base + RX_DESC_LO);
writel(upper_32_bits(dma_handle), dev_base + RX_DESC_HI);
/* 非一致性设备:同步缓存 */
dma_sync_single_for_cpu(dev, dma_handle, size, DMA_FROM_DEVICE);
/* 此时 CPU 可安全读取 cpu_addr 指向的数据 */
若平台启用了 SMMU,dma_handle 实际是 IOVA,设备发出的地址会先经过 SMMU 翻译。驱动无需关心底层物理页是否连续,SMMU 页表可以将离散的物理页映射为连续的 IOVA,从而简化设备编程模型。
五、常见误区与复盘
| 误区 | 正确理解 |
|---|---|
| 虚拟地址可以直接给设备使用 | 设备不认识 CPU 虚拟地址,必须使用 DMA 地址(物理地址或 IOVA) |
| 有了 TLB 就不需要页表失效 | 修改页表后必须执行 TLB invalidate,否则旧映射仍可能命中 |
| 所有设备都自动缓存一致 | 非一致性设备需要驱动显式执行 cache clean/invalidate 或使用 DMA API |
| SMMU 只是性能优化 | SMMU 还提供设备隔离、权限控制和地址空间扩展,是安全与功能特性 |
| dma_alloc_coherent 返回的地址就是物理地址 | 返回的是 CPU 虚拟地址,DMA 地址需通过 dma_handle 获取,可能为 IOVA |
/sys/kernel/debug/iommu/ 查看 SMMU 映射状态;使用 dmesg | grep -i smmu 确认 SMMU 是否启用及 Stream ID 匹配情况。若 DMA 传输异常,优先检查 DMA 地址是否正确、缓存同步是否遗漏、SMMU 页表是否建立。
总结来看,CPU 侧通过 MMU 完成 VA→PA 转换,设备侧通过 SMMU 完成 IOVA→PA 转换,两者共享同一套物理内存但使用不同的地址空间视图。TLB 加速 CPU 转换,缓存一致性保证数据可见性,而 DMA API 则封装了地址映射与同步细节。理解这条完整链路,是后续配置 SMMU、排查 DMA 故障的基础。