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 负责在这两者之间建立正确的映射关系。
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 总线地址 |
flag | GFP 标志(如 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_DEVICE | CPU 写入,设备读取(如发送数据) |
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);
3.5 API 对比与选择指南
| 特性 | dma_alloc_coherent | dma_map_single | dma_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 误用,如双重映射、方向错误、泄漏等。