2. Linux DMA API 概述:核心 API 介绍
在 PCIe 驱动开发中,DMA(直接内存访问)是设备与系统内存之间进行高速数据传输的核心机制。Linux 内核提供了一套标准化的 DMA API,用于管理设备与内存之间的数据一致性。本章将详细介绍三个最核心的 DMA API:dma_alloc_coherent、dma_map_single 和 dma_map_sg,并阐述它们的使用场景与差异。
2.1 DMA API 分类:一致性映射 vs 流式映射
Linux DMA API 从映射管理方式上分为两大类:一致性映射(Coherent Mapping)和流式映射(Streaming Mapping)。两者的核心区别在于对 Cache 一致性的处理策略不同。
| 特性 | 一致性映射(Coherent) | 流式映射(Streaming) |
|---|---|---|
| Cache 一致性 | 硬件/软件保证始终一致 | 需要驱动在访问前后显式同步 |
| 内存分配方式 | 专用分配(dma_alloc_coherent) | 映射已有内存(dma_map_single/sg) |
| 适用场景 | 长期共享的控制结构、描述符环 | 短时、大批量数据缓冲区 |
| 性能开销 | 较高(可能禁用 Cache 或使用特殊内存) | 较低(利用 Cache,减少同步次数) |
| API 典型函数 | dma_alloc_coherent / dma_free_coherent | dma_map_single / dma_unmap_single / dma_map_sg |
选择原则:对于驱动与设备之间频繁访问、生命周期长的数据结构(如 DMA 描述符环、状态寄存器),优先使用一致性映射;对于一次性的数据块传输(如网络包、块设备 I/O),优先使用流式映射以获得更好的 Cache 利用率和性能。
2.2 dma_alloc_coherent:一致性内存分配
dma_alloc_coherent 用于分配一段在 CPU 和设备视角下始终保持 Cache 一致性的 DMA 缓冲区。该函数返回两个地址:CPU 虚拟地址(供驱动访问)和 DMA 总线地址(供设备访问)。
// 函数原型
void *dma_alloc_coherent(struct device *dev, size_t size,
dma_addr_t *dma_handle, gfp_t flag);
// 典型用法示例
struct device *dev = &pdev->dev;
dma_addr_t dma_handle;
void *cpu_addr;
size_t size = 4096; // 分配 4KB 一致性缓冲区
cpu_addr = dma_alloc_coherent(dev, size, &dma_handle, GFP_KERNEL);
if (!cpu_addr) {
dev_err(dev, "Failed to allocate coherent DMA memory\n");
return -ENOMEM;
}
// 使用 cpu_addr 和 dma_handle 进行 DMA 操作
// ...
// 释放一致性内存
dma_free_coherent(dev, size, cpu_addr, dma_handle);
关键参数说明:
- dev:指向 struct device 的指针,用于标识 DMA 所属设备。
- size:分配的字节数,通常需要对齐到页大小。
- dma_handle:输出参数,返回设备可用的 DMA 总线地址。
- flag:内存分配标志,常用
GFP_KERNEL(进程上下文)或GFP_ATOMIC(中断上下文)。
注意事项:一致性映射的内存通常来自特殊内存区域(如 CMA 或 IOMMU 保留区),分配速度较慢且可能浪费 Cache 性能。不要在热路径上频繁分配/释放一致性内存,建议在驱动初始化时一次性分配,在卸载时释放。
2.3 dma_map_single:流式映射单个缓冲区
dma_map_single 用于将一段已有的内核内存映射为 DMA 可访问的流式缓冲区。映射后,驱动必须调用 dma_sync_single_for_cpu 或 dma_sync_single_for_device 来维护 Cache 一致性。
// 函数原型
dma_addr_t dma_map_single(struct device *dev, void *cpu_addr,
size_t size, enum dma_data_direction dir);
// 典型用法示例
void *buffer = kmalloc(2048, GFP_KERNEL);
if (!buffer)
return -ENOMEM;
// 将内核缓冲区映射为 DMA 地址
dma_addr_t dma_addr = dma_map_single(dev, buffer, 2048, DMA_TO_DEVICE);
if (dma_mapping_error(dev, dma_addr)) {
dev_err(dev, "dma_map_single failed\n");
kfree(buffer);
return -ENOMEM;
}
// 启动 DMA 传输(设备从 buffer 读取数据)
// ...
// 传输完成后,解除映射
dma_unmap_single(dev, dma_addr, 2048, DMA_TO_DEVICE);
kfree(buffer);
| 方向标志 | 含义 | 典型场景 |
|---|---|---|
DMA_TO_DEVICE |
数据从内存传输到设备 | 发送网络包、写入块设备 |
DMA_FROM_DEVICE |
数据从设备传输到内存 | 接收网络包、读取块设备 |
DMA_BIDIRECTIONAL |
双向数据传输 | 帧缓冲、共享状态区 |
DMA_NONE |
调试用途,无实际传输 | 初始化或错误路径 |
性能提示:流式映射不会复制数据,仅修改页表或 Cache 策略。映射开销远小于一致性分配,适合高频 I/O 路径。但必须严格遵循“映射 → 传输 → 同步 → 访问 → 解映射”的生命周期。
2.4 dma_map_sg:流式映射 Scatter/Gather 列表
当 DMA 传输涉及多个物理上不连续的缓冲区时,使用 dma_map_sg 可以将一个 struct scatterlist 数组映射为设备可访问的 DMA 地址列表。该函数会返回映射成功的段数,并填充每个 scatterlist 的 dma_address 和 dma_length 字段。
// 函数原型
int dma_map_sg(struct device *dev, struct scatterlist *sg,
int nents, enum dma_data_direction dir);
// 典型用法示例
struct scatterlist sg[3];
int i, nents = 3, mapped;
// 初始化 scatterlist(假设已有三个缓冲区)
sg_init_table(sg, nents);
sg_set_buf(&sg[0], buf0, len0);
sg_set_buf(&sg[1], buf1, len1);
sg_set_buf(&sg[2], buf2, len2);
// 映射整个 SG 列表
mapped = dma_map_sg(dev, sg, nents, DMA_TO_DEVICE);
if (mapped == 0) {
dev_err(dev, "dma_map_sg failed\n");
return -ENOMEM;
}
// 遍历映射后的 SG 列表,获取 DMA 地址和长度
for_each_sg(sg, s, mapped, i) {
dma_addr_t addr = sg_dma_address(s);
unsigned int len = sg_dma_len(s);
// 将 addr 和 len 写入设备描述符
}
// 传输完成后解除映射
dma_unmap_sg(dev, sg, nents, DMA_TO_DEVICE);
2.5 核心 API 对比总结
| API | 映射类型 | 内存来源 | Cache 一致性 | 典型用途 |
|---|---|---|---|---|
| dma_alloc_coherent | 一致性映射 | 专用分配 | 始终一致 | 描述符环、控制结构 |
| dma_map_single | 流式映射 | 已有内核内存 | 需显式同步 | 单缓冲区 I/O |
| dma_map_sg | 流式映射 | 已有内核内存 | 需显式同步 | Scatter/Gather 传输 |
实践建议:
- 初始化阶段分配一致性内存,运行时使用流式映射进行数据传输。
- 流式映射必须配对使用
dma_map_*和dma_unmap_*,避免资源泄漏。 - 使用
dma_mapping_error检查映射是否成功,尤其是在 IOMMU 使能的系统上。 - 对于高性能设备,优先使用
dma_map_sg减少 DMA 描述符数量,提高传输效率。