3. Linux DMA API 详解:核心接口与性能权衡

在 PCIe 设备驱动中,DMA 是数据搬运的核心机制。Linux 内核提供了一套层次清晰的 DMA API,用于管理设备与内存之间的数据传输。本章深入剖析 dma_alloc_coherent、dma_map_single、dma_map_sg 等核心 API 的语义、适用场景及性能特征,帮助你在驱动开发中做出正确的选择。

核心原则: DMA API 的设计目标是隐藏体系结构差异(如 IOMMU、SWIOTLB),同时为驱动开发者提供一致的内存访问接口。理解这些 API 背后的内存模型,是优化延迟与吞吐的前提。

3.1 DMA 地址与 CPU 地址的映射模型

在深入 API 之前,必须理解两个关键地址空间:CPU 虚拟地址(驱动使用)和 DMA 总线地址(设备使用)。DMA API 负责在这两者之间建立正确的映射关系。

DMA 地址映射模型 CPU 虚拟地址 (驱动指针) DMA 映射层 (IOMMU / SWIOTLB) DMA 总线地址 (设备寄存器) 关键说明: • dma_addr_t 是设备可见的地址,与 CPU 虚拟地址无关。 • IOMMU 可提供地址翻译和保护;SWIOTLB 在无硬件 IOMMU 时做反弹缓冲。 • 驱动必须始终使用 dma_addr_t 来编程设备 DMA 寄存器,而非 CPU 地址。

3.2 一致性 DMA 映射:dma_alloc_coherent

dma_alloc_coherent 分配一段同时被 CPU 和设备访问的内存,且保证缓存一致性(Cache Coherent)。这意味着 CPU 和设备看到的始终是最新数据,无需手动刷新缓存。

函数原型与参数

dma_addr_t dma_alloc_coherent(struct device *dev, size_t size,
                               dma_addr_t *dma_handle, gfp_t flag);
参数说明
dev指向 struct device 的指针,标识 DMA 所属设备
size分配大小(字节),通常对齐到页大小
dma_handle输出参数,返回设备可见的 DMA 总线地址
flagGFP 标志(如 GFP_KERNEL、GFP_ATOMIC)
返回值CPU 虚拟地址(内核空间指针),失败返回 NULL

典型使用场景

  • 控制结构体:描述符环、命令队列、状态寄存器映射。
  • 小数据块:设备与 CPU 频繁交互的共享状态区。
  • 实时性要求高:避免缓存刷新带来的延迟抖动。
性能陷阱: dma_alloc_coherent 分配的内存通常是非缓存(Uncached)或写合并(Write-Combining)的,CPU 写入延迟较高。对于大数据流(如网络包、存储块),应优先考虑流式 DMA 映射。

释放接口

void dma_free_coherent(struct device *dev, size_t size,
                        void *cpu_addr, dma_addr_t dma_handle);

3.3 流式 DMA 映射:dma_map_single

dma_map_single 用于将已存在的内核缓冲区映射到设备可访问的 DMA 地址。它不保证缓存一致性,需要驱动显式管理同步。

函数原型

dma_addr_t dma_map_single(struct device *dev, void *cpu_addr,
                           size_t size, enum dma_data_direction dir);
方向枚举含义
DMA_TO_DEVICECPU 写入,设备读取(如发送数据)
DMA_FROM_DEVICE设备写入,CPU 读取(如接收数据)
DMA_BIDIRECTIONAL双向传输(较少使用,性能较差)
DMA_NONE仅用于调试

同步操作

在流式映射中,CPU 和设备不能同时访问同一块内存。必须通过以下 API 保证数据一致性:

// 设备访问前,CPU 完成写入后调用
void dma_sync_single_for_device(struct device *dev,
                                 dma_addr_t dma_handle, size_t size,
                                 enum dma_data_direction dir);

// CPU 读取前,设备完成写入后调用
void dma_sync_single_for_cpu(struct device *dev,
                              dma_addr_t dma_handle, size_t size,
                              enum dma_data_direction dir);
性能要点: 流式映射允许使用常规可缓存内存,CPU 写入延迟低。但同步操作(dma_sync_*)会触发缓存刷新或失效,开销与缓冲区大小成正比。对于大数据块,应尽量减少同步次数。

3.4 分散/聚集 DMA:dma_map_sg

现代 PCIe 设备通常支持分散/聚集(Scatter-Gather)传输,即一次 DMA 操作可以访问多个不连续的物理内存片段。dma_map_sg 将一组缓冲区映射为设备可用的 SG 列表。

函数原型与使用流程

int dma_map_sg(struct device *dev, struct scatterlist *sg,
                int nents, enum dma_data_direction dir);
步骤操作
1. 准备 SG 列表填充 struct scatterlist 数组,设置每个片段的 page 和 offset
2. 调用 dma_map_sg返回映射成功的片段数(可能少于 nents,因合并)
3. 遍历 SG 列表使用 sg_dma_address(sg) 和 sg_dma_len(sg) 获取设备地址和长度
4. 编程设备将 SG 条目写入设备的描述符表
5. 传输完成后调用 dma_unmap_sg 解除映射

SG 列表遍历示例

struct scatterlist *sg;
int i, mapped;

mapped = dma_map_sg(dev, sglist, nents, DMA_TO_DEVICE);
if (mapped == 0) {
    // 错误处理
}

for_each_sg(sglist, sg, mapped, i) {
    dma_addr_t addr = sg_dma_address(sg);
    unsigned int len = sg_dma_len(sg);
    // 将 addr 和 len 写入设备描述符
}

// 传输完成后
dma_unmap_sg(dev, sglist, nents, DMA_TO_DEVICE);
dma_map_sg 工作流程 CPU 内存 片段 0 (4KB) 片段 1 (8KB) 片段 2 (2KB) 片段 3 (6KB) dma_map_sg (可能合并) SG 条目 0: addr0, len=12KB SG 条目 1: addr1, len=8KB (合并后条目数 ≤ 原始 nents) 设备描述符表 Desc 0: addr0, len=12KB Desc 1: addr1, len=8KB PCIe 设备直接读取

3.5 API 对比与选择指南

特性dma_alloc_coherentdma_map_singledma_map_sg
内存来源专用分配已有缓冲区多个不连续片段
缓存一致性硬件保证需手动同步需手动同步
CPU 访问延迟高(非缓存)低(可缓存)低(可缓存)
适用数据量小(控制结构)中到大大(可合并)
映射开销分配时一次每次传输每次传输
典型场景描述符环、状态网络包、存储 I/O高性能存储、视频
性能优化建议:
  • 控制结构使用 dma_alloc_coherent,避免同步开销。
  • 数据路径优先使用流式映射,利用可缓存内存降低 CPU 延迟。
  • 对于大数据传输,使用 dma_map_sg 减少 DMA 启动次数,提升吞吐。
  • 在热路径中预分配并复用 DMA 映射,避免频繁 map/unmap。

3.6 常见错误与调试

错误模式后果解决方法
忘记调用 dma_sync_*数据不一致、CRC 错误严格按方向调用同步 API
使用 CPU 地址编程设备设备访问错误内存始终使用 dma_handle 或 sg_dma_address
映射后修改缓冲区内容设备看到脏数据同步前完成所有写入
dma_map_sg 后使用原始 nents访问无效地址使用返回值 mapped 遍历
未检查返回值静默数据损坏始终检查 dma_mapping_error
// 检查映射是否成功的标准模式
dma_addr_t dma_handle = dma_map_single(dev, buf, size, dir);
if (dma_mapping_error(dev, dma_handle)) {
    dev_err(dev, "DMA mapping failed\n");
    goto err_out;
}
调试技巧: 开启内核配置 CONFIG_DMA_API_DEBUG 和 CONFIG_DMA_API_DEBUG_SG,可以检测常见的 DMA API 误用,如双重映射、方向错误、泄漏等。