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 与设备看到的内存内容始终一致,适合长期存在、频繁访问的控制结构;后者用于一次性或阶段性传输,需要显式同步,性能更好但使用约束更多。
二、一致性 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 返回的地址可能无法被设备正常访问。