2. DMA基础回顾

在讨论 IOMMU 与 SMMU 之前,必须先把 DMA 的底层机制重新梳理清楚。因为 IOMMU/SMMU 并不是独立存在的“魔法开关”,它介入的正是 DMA 的地址生成、访问权限与缓存一致性路径。只有理解设备如何发起一次 DMA、CPU 如何描述缓冲区、驱动如何映射地址,才能准确判断后续配置问题究竟出在设备、总线还是 IOMMU 页表上。

一、DMA工作原理:从CPU搬运到设备直访

在没有 DMA 的系统中,外设数据搬运通常由 CPU 逐字节或逐字读写完成,CPU 被大量占用。DMA(Direct Memory Access)的核心思想是:由 DMA 控制器或设备自身作为总线主设备(Bus Master),直接读写系统内存,CPU 只负责初始化传输参数并在完成后处理中断。

一次典型 DMA 传输包含四个阶段:

  1. 准备阶段:驱动分配内存缓冲区,得到 CPU 虚拟地址,并转换为设备可访问的总线地址。
  2. 编程阶段:驱动把源地址、目的地址、长度、方向、传输模式写入 DMA 控制器或设备寄存器。
  3. 传输阶段:设备发起总线读写,数据在内存与外设之间流动,CPU 可并行执行其他任务。
  4. 完成阶段:传输结束产生中断,驱动检查状态、同步缓存并释放资源。
关键点:DMA 传输中“地址”不是 CPU 虚拟地址,而是设备视角的总线地址。IOMMU/SMMU 正是在这个地址转换环节发挥作用。

二、传输模式:单次、块、散列与循环

不同外设对 DMA 的需求差异很大,常见传输模式包括:

  • 单次传输(Single Transfer):一次搬运一个数据单元,适合低速控制类外设。
  • 块传输(Block Transfer):连续搬运一整块数据,效率高,但要求缓冲区物理连续。
  • 散列/聚集传输(Scatter-Gather):通过描述符链表把多个不连续物理页组合成一次逻辑传输,是现代网卡、NVMe 的主流方式。
  • 循环传输(Circular):缓冲区首尾相接,适合音频、串口等持续数据流场景。

散列传输是理解描述符机制的前提,也是 IOMMU 最常介入的场景:设备看到的是一串“设备地址”,而这些地址需要被 IOMMU 映射到真实物理页。

三、描述符机制:把不连续内存串成一次传输

描述符(Descriptor)是驱动与硬件之间的“传输任务单”。每个描述符通常包含缓冲区地址、长度、方向、状态和控制位。硬件按顺序读取描述符并执行传输,完成后回写状态。

struct dma_desc {
    u64 buf_addr;   // 设备可见的总线地址
    u32 length;     // 数据长度
    u32 flags;      // 方向、中断、链尾等
    u32 status;     // 硬件回写完成状态
};

描述符本身也存放在内存中,因此它同样需要被设备访问。如果描述符地址没有正确映射,设备会读到全 0 或错误地址,表现为“传输不启动”或“数据错乱”。在 IOMMU 开启后,描述符中的 buf_addr 必须是 IOVA(I/O 虚拟地址),而不是物理地址。

四、缓存一致性与地址映射

DMA 绕过 CPU 直接访问内存,会带来两个经典问题:

  • 缓存一致性:CPU 可能把数据缓存在 cache 中,设备直接读内存时读到旧数据;设备写入内存后,CPU 又可能从 cache 读到旧值。
  • 地址映射:设备使用总线地址,CPU 使用虚拟地址,两者之间需要转换与权限检查。

解决一致性通常有两种方式:一是硬件保证一致(Coherent DMA),二是软件显式同步(Streaming DMA),使用 dma_sync_single_for_cpu/device 等 API。地址映射则依赖 dma_map_single、dma_map_sg 等接口,在 IOMMU 存在时由 IOMMU 驱动建立 IOVA 到 PA 的页表。

调试提示:若设备 DMA 读到的数据与 CPU 写入不一致,先检查是否遗漏 dma_sync_* 调用,再检查 IOMMU 映射是否覆盖了正确范围。

五、设备驱动中的DMA API与典型流程

Linux 内核提供了一套统一的 DMA API,屏蔽架构差异。典型流程如下:

/* 1. 分配一致性内存 */
cpu_addr = dma_alloc_coherent(dev, size, &dma_handle, GFP_KERNEL);

/* 2. 流式映射(单缓冲区) */
dma_addr = dma_map_single(dev, cpu_addr, size, DMA_TO_DEVICE);
if (dma_mapping_error(dev, dma_addr)) { /* 错误处理 */ }

/* 3. 同步(非一致性场景) */
dma_sync_single_for_device(dev, dma_addr, size, DMA_TO_DEVICE);

/* 4. 传输完成后解除映射 */
dma_unmap_single(dev, dma_addr, size, DMA_TO_DEVICE);

对于散列传输,使用 dma_map_sg 映射描述符表,硬件按 sg 表执行。注意:dma_alloc_coherent 返回的地址在 IOMMU 开启时同样是 IOVA,驱动不能假设它等于物理地址。

API用途IOMMU 开启后的地址
dma_alloc_coherent分配一致性内存返回 IOVA,映射已建立
dma_map_single流式映射单缓冲区返回 IOVA,需 unmap
dma_map_sg散列映射多段缓冲区返回 IOVA 数组
dma_sync_single_for_deviceCPU 到设备方向同步不改变地址,仅刷 cache

六、案例与常见误区复盘

案例:某网卡驱动在开启 IOMMU 后收包失败,日志显示描述符状态始终为 0。排查发现驱动把 dma_alloc_coherent 返回的 dma_handle 直接当作物理地址写入了描述符,而硬件实际需要的是 IOVA。修正后传输恢复正常。

常见误区:

  • 把 virt_to_phys 的结果直接给设备使用,忽略 IOMMU 映射。
  • 忘记 dma_unmap_single,导致 IOVA 泄漏,长时间运行后映射失败。
  • 在非一致性平台上省略 dma_sync_*,造成偶发数据错误。
  • 认为 dma_alloc_coherent 一定返回物理连续内存,实际上它只保证设备视角连续。
复盘结论:DMA 问题的根因往往不在“传输本身”,而在地址与一致性这两个边界条件。IOMMU/SMMU 配置问题,绝大多数可以回溯到本节所述的地址映射与描述符机制。
DMA 传输路径与 IOMMU 介入点 CPU 虚拟地址 VA Cache 一致性同步点 设备驱动 dma_map_single dma_alloc_coherent IOMMU / SMMU IOVA → PA 转换 权限检查 页表映射 DMA 设备 网卡 / NVMe / GPU 发起总线读写 使用 IOVA 系统内存 物理页 PA 描述符 / 数据缓冲 编程 DMA 寄存器 设备地址 IOVA 直接读写内存 页表转换 CPU/缓存路径 IOMMU 转换 设备发起 DMA 物理内存访问

总结来看,DMA 基础可以归纳为一条主线:驱动准备缓冲区 → 建立设备可见地址 → 描述符描述传输 → 设备直接访问内存 → 完成后同步与回收。IOMMU/SMMU 在这条主线上插入地址转换与权限控制,因此后续所有配置问题,都应回到这条主线逐段排查。