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 传输包含四个阶段:
- 准备阶段:驱动分配内存缓冲区,得到 CPU 虚拟地址,并转换为设备可访问的总线地址。
- 编程阶段:驱动把源地址、目的地址、长度、方向、传输模式写入 DMA 控制器或设备寄存器。
- 传输阶段:设备发起总线读写,数据在内存与外设之间流动,CPU 可并行执行其他任务。
- 完成阶段:传输结束产生中断,驱动检查状态、同步缓存并释放资源。
二、传输模式:单次、块、散列与循环
不同外设对 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 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_device | CPU 到设备方向同步 | 不改变地址,仅刷 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 在这条主线上插入地址转换与权限控制,因此后续所有配置问题,都应回到这条主线逐段排查。