4. Linux DMA API 概述:核心内存映射接口
Linux 内核提供了一套标准化的 DMA API,用于在驱动程序和硬件之间安全、高效地管理内存缓冲区。本章深入剖析三个最核心的 API:dma_alloc_coherent、dma_map_single 和 dma_map_sg,涵盖其语义、适用场景、参数含义及底层内存映射机制。
4.1 DMA API 设计原则与分类
DMA API 的设计遵循两条核心原则:设备无关性和缓存一致性管理。根据缓冲区使用模式,API 分为两类:
| 类别 | 核心 API | 缓存一致性 | 典型场景 |
|---|---|---|---|
| 一致性映射 (Coherent) | dma_alloc_coherent |
硬件与 CPU 始终一致,无需手动刷新 | 描述符环、控制寄存器、持续轮询的状态区 |
| 流式映射 (Streaming) | dma_map_single / dma_map_sg |
需要显式同步(dma_sync_single_for_cpu/device) |
网络数据包、块设备 I/O、一次性传输 |
选择指南: 如果设备需要频繁、随机地访问同一块内存(如 DMA 描述符),使用一致性映射;如果数据是流式传输(如网络帧),使用流式映射以获得更好的性能。
4.2 dma_alloc_coherent:一致性 DMA 缓冲区
dma_alloc_coherent 分配一段物理连续且缓存一致的内存,返回 CPU 虚拟地址和 DMA 总线地址。其核心机制是:分配时禁用该内存区域的 CPU 缓存行(或使用非缓存映射),从而避免缓存一致性问题。
// 原型
void *dma_alloc_coherent(struct device *dev, size_t size,
dma_addr_t *dma_handle, gfp_t flag);
// 示例:分配 64KB 一致性缓冲区
dma_addr_t dma_handle;
void *cpu_addr;
cpu_addr = dma_alloc_coherent(&pdev->dev, 64 * 1024,
&dma_handle, GFP_KERNEL);
if (!cpu_addr) {
dev_err(&pdev->dev, "Failed to allocate coherent DMA memory\n");
return -ENOMEM;
}
// 使用 cpu_addr 和 dma_handle 编程硬件寄存器
// ...
dma_free_coherent(&pdev->dev, 64 * 1024, cpu_addr, dma_handle);
关键参数:
dev:DMA 设备结构体,用于关联 IOMMU/总线。size:请求字节数,必须为页大小(PAGE_SIZE)的整数倍。dma_handle:输出参数,返回设备可用的 DMA 总线地址。flag:内存分配标志,通常为GFP_KERNEL(可睡眠)或GFP_ATOMIC(中断上下文)。
4.3 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);
// 示例:映射一个网络 skb 的数据区
struct sk_buff *skb = ...;
dma_addr_t dma_addr;
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 写入,设备读取(如网络发送) | 映射时刷新 CPU 脏缓存行到内存 |
DMA_FROM_DEVICE |
设备写入,CPU 读取(如网络接收) | 映射时无效化 CPU 缓存行,避免读到旧数据 |
DMA_BIDIRECTIONAL |
双向传输 | 映射时同时刷新和无效化 |
DMA_NONE |
调试用途 | 无操作 |
常见错误: 映射后,CPU 不应再直接访问原始虚拟地址,除非先调用
dma_sync_single_for_cpu。否则可能导致缓存一致性问题,产生数据损坏。
4.4 dma_map_sg:流式聚合散列表映射
dma_map_sg 用于将多个不连续的物理内存片段(scatterlist)映射为设备可访问的 DMA 地址列表。它返回映射成功的片段数,并可能合并相邻物理页以优化传输。
// 原型
int dma_map_sg(struct device *dev, struct scatterlist *sg,
int nents, enum dma_data_direction dir);
// 示例:映射一个包含 3 个片段的 SG 列表
struct scatterlist sg[3];
int i, mapped;
sg_init_table(sg, 3);
sg_set_buf(&sg[0], buf1, len1);
sg_set_buf(&sg[1], buf2, len2);
sg_set_buf(&sg[2], buf3, len3);
mapped = dma_map_sg(&pdev->dev, sg, 3, DMA_BIDIRECTIONAL);
if (mapped <= 0) {
dev_err(&pdev->dev, "SG mapping failed\n");
return -EIO;
}
// 遍历映射后的 SG 列表
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 引擎
}
// ...
dma_unmap_sg(&pdev->dev, sg, mapped, DMA_BIDIRECTIONAL);
4.5 核心 API 对比与选择
| 特性 | dma_alloc_coherent | dma_map_single | dma_map_sg |
|---|---|---|---|
| 内存所有权 | 分配并拥有 | 仅映射,不分配 | 仅映射,不分配 |
| 物理连续性 | 必须连续 | 取决于原始缓冲区 | 不要求连续 |
| 缓存一致性 | 硬件保证 | 需显式同步 | 需显式同步 |
| 分配开销 | 高(页分配 + 页表修改) | 低(仅映射) | 中(遍历 + 合并) |
| 典型用途 | 描述符环、控制结构 | 单缓冲区 I/O | 多片段 I/O(网络、存储) |
性能要点:
- 一致性映射在分配时开销大,但后续访问无同步开销,适合长期持有的缓冲区。
- 流式映射在每次传输前后需要同步,但分配灵活,适合高频、短生命周期的 I/O。
- 对于支持 IOMMU 的系统,dma_map_sg 可以避免物理连续性的限制,简化驱动设计。
4.6 错误处理与调试
所有 DMA 映射 API 都可能失败,必须检查返回值:
// 检查 dma_alloc_coherent 返回值
if (!cpu_addr) {
dev_err(dev, "Coherent allocation failed\n");
return -ENOMEM;
}
// 检查 dma_map_single 返回值
dma_addr_t addr = dma_map_single(dev, buf, size, dir);
if (dma_mapping_error(dev, addr)) {
dev_err(dev, "Single mapping failed\n");
return -EIO;
}
// 检查 dma_map_sg 返回值
int mapped = dma_map_sg(dev, sg, nents, dir);
if (mapped <= 0) {
dev_err(dev, "SG mapping failed\n");
return -EIO;
}
调试技巧: 开启内核配置
CONFIG_DMA_API_DEBUG 可以检测常见的 DMA API 误用,如未匹配的 map/unmap、错误的方向参数、双重释放等。在开发阶段建议始终开启。