2. DMA 基础与 Linux 内核 DMA API

在 Linux 内核中,设备驱动通常不直接操作物理地址,而是通过 DMA API 让设备与内存之间搬运数据。理解一致性 DMA、流式 DMA、DMA 掩码、DMA 方向以及 DMA 属性,是后续配置 SMMU 和排查 DMA 映射问题的前提。本节从内核视角梳理这些概念,并给出常见 API 的语义与使用边界。

一、核心原理:为什么需要 DMA API

CPU 访问内存时经过 MMU,看到的是虚拟地址;而外设 DMA 引擎看到的是总线地址(DMA 地址)。在无 IOMMU/SMMU 的系统中,总线地址通常等于物理地址;在启用 SMMU 后,总线地址需要经过 SMMU 转换为物理地址。DMA API 的职责,就是为驱动屏蔽这些差异,提供“分配缓冲区、建立映射、取得设备可用的 DMA 地址、同步缓存”的统一接口。

内核把 DMA 映射分为两类:一致性 DMA 和 流式 DMA。前者保证 CPU 与设备看到的内存内容始终一致,适合长期存在、频繁访问的控制结构;后者用于一次性或阶段性传输,需要显式同步,性能更好但使用约束更多。

Linux DMA 映射模型:CPU 视角与设备视角 CPU / 驱动 虚拟地址 (VA) kmalloc / dma_alloc cache 一致性由 API 维护 DMA API 层 dma_alloc_coherent dma_map_single / sg dma_sync_* / dma_unmap 设备 / DMA 引擎 总线地址 (DMA addr) 受 dma_mask 限制 方向决定读写语义 一致性 DMA dma_alloc_coherent() 返回 CPU VA + DMA 地址 无需手动 sync,适合描述符环 代价:可能不可缓存或占用受限 流式 DMA dma_map_single() / dma_map_sg() 映射已有缓冲区,返回 DMA 地址 传输前后需 dma_sync_* 同步 用完必须 dma_unmap_* 解除映射

二、一致性 DMA 与流式 DMA 的语义差异

一致性 DMA 通过 dma_alloc_coherent() 分配,返回两个地址:CPU 使用的虚拟地址和设备使用的 DMA 地址。该缓冲区在 CPU 与设备之间保持一致性,驱动不需要在每次访问前后调用同步函数。它适合长期存在、双方频繁读写的小型结构,例如网卡描述符环、命令队列、状态寄存器镜像。其代价是可能无法使用写合并缓存,分配尺寸和数量也受平台限制。

流式 DMA 通过 dma_map_single() 或 dma_map_sg() 对已有缓冲区建立映射,返回设备可用的 DMA 地址。映射期间,CPU 不应随意访问该缓冲区;若必须访问,需按方向调用 dma_sync_single_for_cpu() 或 dma_sync_single_for_device()。传输完成后必须调用 dma_unmap_single() 或 dma_unmap_sg() 解除映射,否则会造成地址空间泄漏或缓存状态错误。

关键区别:一致性 DMA 是“分配即一致”,流式 DMA 是“映射后需同步”。前者生命周期长、访问频繁;后者生命周期短、传输批量大。

三、DMA 掩码、方向与属性

DMA 掩码(dma_mask 与 coherent_dma_mask)描述设备能够寻址的总线地址范围。例如 32 位设备只能访问 4GB 以下地址,驱动必须正确设置掩码,否则 DMA API 可能返回设备无法访问的高地址,导致传输失败。常见做法是在 probe 中设置:

if (dma_set_mask_and_coherent(dev, DMA_BIT_MASK(64)))
        return -EIO;
/* 若设备仅支持 32 位 */
if (dma_set_mask_and_coherent(dev, DMA_BIT_MASK(32)))
        return -EIO;

DMA 方向由 enum dma_data_direction 表示,直接影响缓存同步策略:

  • DMA_TO_DEVICE:内存到设备,映射后需确保 CPU 写入对设备可见。
  • DMA_FROM_DEVICE:设备到内存,映射前需使 CPU 缓存失效,避免读到旧数据。
  • DMA_BIDIRECTIONAL:双向,同步开销最大,应尽量避免。
  • DMA_NONE:仅用于调试,表示无方向。

DMA 属性用于描述设备与内存之间的连接约束,例如是否支持缓存一致、是否需要写合并、是否受 SMMU 限制等。在设备树中常通过 dma-coherent、iommus、dma-ranges 等属性表达。驱动侧则通过 device_get_dma_attr() 或相关接口读取,用于决定映射策略。

四、常见 API 语义与使用清单

API 用途 是否需同步 是否需解除
dma_alloc_coherent() 分配一致性缓冲区 否 dma_free_coherent()
dma_map_single() 映射单个连续缓冲区 是 dma_unmap_single()
dma_map_sg() 映射分散/聚集列表 是 dma_unmap_sg()
dma_sync_single_for_cpu() 设备写完后交还 CPU 访问 — —
dma_sync_single_for_device() CPU 写完后交给设备读取 — —

五、典型场景与误区复盘

以网卡驱动为例:描述符环通常用 dma_alloc_coherent() 分配,因为 CPU 和设备都会频繁读写;数据包缓冲区则用 dma_map_single() 做流式映射,发送方向为 DMA_TO_DEVICE,接收方向为 DMA_FROM_DEVICE。发送完成后立即 dma_unmap_single(),避免长期占用映射资源。

常见误区:一是忘记设置 dma_mask,导致设备拿到无法访问的高地址;二是流式映射后 CPU 直接读写缓冲区却不调用 dma_sync_*,在非一致性架构上读到旧数据;三是映射后忘记 dma_unmap_*,造成 DMA 地址泄漏;四是把 DMA_BIDIRECTIONAL 当作默认方向,带来不必要的缓存同步开销。

排查 DMA 问题时,可先确认 /sys/kernel/debug/dma-api/ 下的映射统计,再检查设备树中的 dma-coherent、iommus 与 dma-ranges 是否与硬件一致。若启用 SMMU,还需确认流表项和域配置是否正确,否则 DMA API 返回的地址可能无法被设备正常访问。