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 场景中必须关注的关键字段。

关键点:MMU 只服务于 CPU 侧的地址转换。设备发起 DMA 时,若没有 IOMMU/SMMU,设备使用的是物理地址;若启用 SMMU,则设备也可以使用 IOVA(I/O 虚拟地址),由 SMMU 完成类似 MMU 的翻译。

二、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() 获得设备可见地址,同时实现地址隔离、受限访问和更大的连续地址空间。

CPU 视角与设备视角的地址转换路径 CPU 发出虚拟地址 VA TTBR0_EL1 / TTBR1_EL1 MMU 多级页表遍历 TLB 缓存 VA→PA 页表 PGD→PUD→PMD→PTE 权限 / 属性 / PFN DRAM 物理地址 PA VA 查表 PA TLB 命中:跳过页表遍历 设备 DMA 引擎 发出 IOVA / 总线地址 Stream ID 标识设备 SMMU / IOMMU IOVA → PA 翻译 权限 / 隔离 / 属性检查 SMMU 页表 Stream Table Context Descriptor DRAM 物理地址 PA IOVA 查表 PA 缓存一致性关注点 • CPU 数据缓存与 DRAM 之间:cache clean / invalidate • CPU 缓存与设备可见内存之间:dma_sync_single_for_device / for_cpu • 一致性设备(coherent)由硬件保证;非一致性设备需驱动显式同步 CPU 路径 设备路径 物理内存

四、案例与场景:网卡 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
调试建议:在 ARM64 平台上可通过 /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 故障的基础。