3. Linux DMA API 概览:dma_alloc_coherent、dma_map_single、dma_map_sg

在 PCIe 驱动开发中,DMA(直接内存访问)是设备与主机内存之间进行高速数据传输的核心机制。Linux 内核提供了一套标准化的 DMA API,用于管理设备与 CPU 之间的内存一致性、地址映射以及分散/聚集 I/O。本章聚焦三个最常用的 API:dma_alloc_coherent、dma_map_single 和 dma_map_sg,深入解析其工作原理、适用场景及调试要点。

3.1 DMA API 分类与选择依据

Linux DMA API 主要分为两类:一致性映射(Coherent Mapping) 和 流式映射(Streaming Mapping)。选择哪种映射方式,取决于数据传输的方向、频率以及对缓存一致性的要求。

API 函数 映射类型 适用场景 关键特性
dma_alloc_coherent 一致性映射 长期、频繁的 DMA 传输(如描述符环、控制寄存器) 保证 CPU 和设备看到一致的内存视图;无需手动缓存刷新
dma_map_single 流式映射 单次、短期的 DMA 传输(如网络数据包、块 I/O 请求) 需要显式指定数据传输方向;需要调用 dma_unmap_single 释放
dma_map_sg 流式映射(分散/聚集) 非连续物理内存的批量 DMA 传输(如高性能存储、网络) 将多个物理不连续的缓冲区映射为设备可访问的 DMA 地址列表
调试提示: 在调试 DMA 问题时,首先确认使用的是哪种映射类型。一致性映射的调试重点在于内存泄漏和地址对齐;流式映射的调试重点在于方向错误和缓存一致性问题。

3.2 dma_alloc_coherent:一致性 DMA 内存分配

dma_alloc_coherent 用于分配一段在 CPU 和设备之间保持缓存一致性的 DMA 缓冲区。该函数返回两个地址:一个给 CPU 使用的虚拟地址(vaddr),一个给设备使用的 DMA 总线地址(dma_handle)。

// 原型
void *dma_alloc_coherent(struct device *dev, size_t size,
                         dma_addr_t *dma_handle, gfp_t flag);

// 典型用法:分配描述符环
struct my_dev *priv;
priv->ring_vaddr = dma_alloc_coherent(&pdev->dev,
                                      RING_SIZE * sizeof(struct desc),
                                      &priv->ring_dma,
                                      GFP_KERNEL);
if (!priv->ring_vaddr) {
    dev_err(&pdev->dev, "Failed to allocate coherent DMA memory\n");
    return -ENOMEM;
}
// 使用完毕后释放
dma_free_coherent(&pdev->dev, RING_SIZE * sizeof(struct desc),
                  priv->ring_vaddr, priv->ring_dma);
常见陷阱:
  • size 参数必须与释放时一致,否则会导致内存管理错误。
  • 分配大块一致性内存(超过 2MB)可能失败,建议使用 dma_alloc_attrs 并设置 DMA_ATTR_NO_WARN 抑制警告。
  • 一致性内存通常来自低端内存区域(ZONE_DMA),在嵌入式系统中尤其稀缺。

3.3 dma_map_single:流式单次 DMA 映射

dma_map_single 用于将一段内核虚拟地址(通常是 kmalloc 或 __get_free_pages 分配的缓冲区)映射为设备可访问的 DMA 地址。与一致性映射不同,流式映射需要驱动程序显式管理缓存一致性。

// 原型
dma_addr_t dma_map_single(struct device *dev, void *cpu_addr,
                          size_t size, enum dma_data_direction dir);

// 典型用法:发送网络数据包
struct sk_buff *skb = ...;
dma_addr_t dma_addr = dma_map_single(&pdev->dev, skb->data,
                                     skb->len, DMA_TO_DEVICE);
if (dma_mapping_error(&pdev->dev, dma_addr)) {
    dev_err(&pdev->dev, "DMA mapping failed\n");
    return -EIO;
}
// 将 dma_addr 写入设备描述符,触发 DMA 传输
// 传输完成后解除映射
dma_unmap_single(&pdev->dev, dma_addr, skb->len, DMA_TO_DEVICE);
方向常量 含义 缓存操作
DMA_TO_DEVICE CPU 写入,设备读取 映射时刷新缓存(写回),确保设备看到最新数据
DMA_FROM_DEVICE 设备写入,CPU 读取 映射时无效化缓存,解除映射时无效化缓存(确保 CPU 读到设备写入的数据)
DMA_BIDIRECTIONAL 双向传输 映射和解除映射时都执行完整的缓存同步
DMA_NONE 调试用途 不执行任何缓存操作
调试要点: 流式映射最常见的错误是方向常量使用错误。例如,在设备写入数据后,如果使用 DMA_TO_DEVICE 解除映射,CPU 可能读到过时的缓存数据。务必使用 dma_mapping_error 检查映射是否成功。

3.4 dma_map_sg:分散/聚集 DMA 映射

对于高性能设备(如 NVMe SSD、万兆网卡),数据通常分布在多个物理不连续的内存页中。dma_map_sg 将一个 struct scatterlist 数组映射为设备可访问的 DMA 地址列表,设备可以通过一次 DMA 操作完成所有分散缓冲区的传输。

// 原型
int dma_map_sg(struct device *dev, struct scatterlist *sg,
               int nents, enum dma_data_direction dir);

// 典型用法:NVMe 驱动提交 I/O 请求
struct scatterlist *sg;
int i, nents;
nents = dma_map_sg(&pdev->dev, sg, num_sg, DMA_FROM_DEVICE);
if (nents == 0) {
    dev_err(&pdev->dev, "SG DMA mapping failed\n");
    return -ENOMEM;
}
// 遍历映射后的 SG 列表,将 DMA 地址填入设备描述符
for_each_sg(sg, sgl, nents, i) {
    dma_addr_t dma_addr = sg_dma_address(sgl);
    unsigned int len = sg_dma_len(sgl);
    // 将 dma_addr 和 len 写入设备描述符
}
// 传输完成后解除映射
dma_unmap_sg(&pdev->dev, sg, num_sg, DMA_FROM_DEVICE);
关键区别: dma_map_sg 的返回值 nents 可能小于传入的 num_sg,因为相邻的物理页可能被合并。必须使用 sg_dma_address 和 sg_dma_len 获取映射后的 DMA 地址和长度,而不是直接使用 sg_page 和 sg->length。

3.5 DMA 地址映射流程对比

下面的 SVG 图展示了三种 DMA API 在地址映射流程中的核心差异,帮助理解 CPU 虚拟地址、物理地址与设备 DMA 地址之间的关系。

三种 DMA API 地址映射流程对比 三种 DMA API 地址映射流程对比 dma_alloc_coherent 1. 分配物理连续内存 2. 建立 CPU 虚拟地址映射 3. 返回 DMA 总线地址 4. 缓存一致性由硬件保证 5. 无需手动缓存刷新 6. 适用于描述符环等长期缓冲区 返回: vaddr + dma_handle dma_map_single 1. 传入 CPU 虚拟地址 2. 转换为物理地址 3. 映射为 DMA 总线地址 4. 根据方向执行缓存操作 5. 传输完成后必须 unmap 6. 适用于单次短时传输 返回: dma_addr_t dma_map_sg 1. 传入 scatterlist 数组 2. 合并相邻物理页 3. 映射为 DMA 地址列表 4. 使用 sg_dma_address 获取 5. 传输完成后必须 unmap 6. 适用于非连续批量传输 返回: 映射后的 SG 条目数 核心差异总结 • 一致性映射(dma_alloc_coherent):分配即映射,生命周期长,适合控制路径。 • 流式映射(dma_map_single):临时映射,需要方向参数,适合数据路径。 • 分散/聚集映射(dma_map_sg):流式映射的批量版本,减少 DMA 中断次数。

3.6 调试与性能优化建议

  • 地址对齐检查: 大多数 PCIe 设备要求 DMA 缓冲区地址对齐到 4 字节、8 字节或缓存行(64 字节)。使用 dma_get_cache_alignment 获取平台缓存行大小。
  • 映射错误处理: 始终检查 dma_mapping_error 的返回值。映射失败时,设备无法访问内存,继续操作会导致数据损坏。
  • 缓存同步: 对于流式映射,如果需要在 DMA 传输过程中访问缓冲区(例如部分处理),使用 dma_sync_single_for_cpu 和 dma_sync_single_for_device 手动同步。
  • 性能计数器: 使用 /sys/kernel/debug/dma-api/ 下的调试接口监控 DMA 映射次数、错误次数和内存使用量。
调试陷阱: 在 SMP(对称多处理)系统中,dma_alloc_coherent 分配的内存可能来自 NUMA 节点远端,导致跨节点访问延迟增加。使用 dev_to_node 获取设备所在 NUMA 节点,并传递给 dma_alloc_coherent 的 flag 参数(如 GFP_KERNEL | __GFP_THISNODE)。

3.7 本章小结