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 地址之间的关系。
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)。