2. Linux DMA API 概述:核心接口与使用场景
Linux 内核提供了一套标准化的 DMA(Direct Memory Access)API,用于管理设备与内存之间的数据搬运。这些 API 屏蔽了不同架构(ARM、x86、RISC-V 等)的硬件差异,为驱动开发者提供统一的缓冲分配、映射和同步接口。本章聚焦三个最核心的 API:dma_alloc_coherent、dma_map_single 和 dma_pool,并介绍它们的适用场景、性能特征和典型用法。
2.1 DMA 地址空间与 API 分类
在深入具体 API 之前,需要理解两个关键地址:CPU 虚拟地址(驱动通过指针访问)和 DMA 总线地址(设备通过 DMA 引擎访问)。Linux DMA API 负责在这两个地址空间之间建立映射,并处理缓存一致性。
根据缓存一致性的处理方式,DMA API 分为两大类:
| 类别 | 代表 API | 一致性保证 | 典型场景 |
|---|---|---|---|
| 一致性 DMA | dma_alloc_coherent |
硬件保证 CPU 和设备始终看到相同数据 | 描述符环、控制寄存器、小数据量控制结构 |
| 流式 DMA | dma_map_single / dma_map_sg |
通过显式 dma_sync_* 维护一致性 |
网络数据包、块设备 I/O、大块数据传输 |
| DMA 池 | dma_pool |
基于一致性 DMA,预分配固定大小缓冲 | USB 传输描述符、网络小包、固定大小控制块 |
2.2 dma_alloc_coherent:一致性 DMA 缓冲
dma_alloc_coherent 分配一段物理连续的内存,并同时返回 CPU 虚拟地址和 DMA 总线地址。硬件保证这段内存的缓存一致性——CPU 和 DMA 设备可以同时访问而不需要软件同步。
函数原型与参数
#include <linux/dma-mapping.h>
void *dma_alloc_coherent(struct device *dev, size_t size,
dma_addr_t *dma_handle, gfp_t flag);
| 参数 | 说明 |
|---|---|
dev |
请求 DMA 的设备指针,用于关联 IOMMU 域 |
size |
分配的字节数,会被向上对齐到页大小 |
dma_handle |
输出参数,返回 DMA 总线地址 |
flag |
GFP 标志(如 GFP_KERNEL、GFP_ATOMIC) |
| 返回值 | CPU 虚拟地址(内核空间),失败返回 NULL |
典型用法示例
struct device *dev = &pdev->dev;
dma_addr_t dma_handle;
void *cpu_addr;
size_t buf_size = 4096; // 4KB
cpu_addr = dma_alloc_coherent(dev, buf_size, &dma_handle, GFP_KERNEL);
if (!cpu_addr) {
dev_err(dev, "Failed to allocate coherent DMA buffer\n");
return -ENOMEM;
}
// 驱动可以直接读写 cpu_addr,设备可以通过 dma_handle 访问
memset(cpu_addr, 0, buf_size);
writel(dma_handle, reg_base + DMA_ADDR_REG);
// 使用完毕后释放
dma_free_coherent(dev, buf_size, cpu_addr, dma_handle);
2.3 dma_map_single:流式 DMA 映射
dma_map_single 将一段已有的内核缓冲区(通常来自 kmalloc 或 __get_free_pages)映射为 DMA 可访问的总线地址。它不分配新内存,只建立映射并处理缓存同步。流式映射要求驱动在 CPU 和设备访问之间显式调用同步函数。
函数原型与方向
dma_addr_t dma_map_single(struct device *dev, void *cpu_addr,
size_t size, enum dma_data_direction dir);
dma_data_direction 枚举定义了数据传输方向,影响缓存同步策略:
| 方向 | 值 | 含义 | 缓存操作 |
|---|---|---|---|
DMA_TO_DEVICE |
1 | CPU 写入,设备读取 | 映射时刷出缓存行 |
DMA_FROM_DEVICE |
2 | 设备写入,CPU 读取 | 映射时无效化缓存行 |
DMA_BIDIRECTIONAL |
3 | 双向传输 | 映射时刷出并无效化 |
DMA_NONE |
0 | 调试用途 | 无操作 |
流式 DMA 的完整生命周期
- 分配缓冲区:使用
kmalloc或alloc_pages分配内存。 - 映射:调用
dma_map_single获取 DMA 地址。 - 提交给设备:将 DMA 地址写入设备寄存器,启动传输。
- 同步(可选):在 CPU 访问前调用
dma_sync_single_for_cpu,在设备访问前调用dma_sync_single_for_device。 - 解除映射:传输完成后调用
dma_unmap_single。
// 分配缓冲区
void *buf = kmalloc(buf_size, GFP_KERNEL);
if (!buf) return -ENOMEM;
// 映射为 DMA 地址
dma_addr_t dma_addr = dma_map_single(dev, buf, buf_size, DMA_TO_DEVICE);
if (dma_mapping_error(dev, dma_addr)) {
kfree(buf);
return -EIO;
}
// 启动 DMA 传输(伪代码)
writel(dma_addr, reg_base + DMA_SRC);
writel(buf_size, reg_base + DMA_LEN);
writel(1, reg_base + DMA_START);
// 等待传输完成...
// 解除映射
dma_unmap_single(dev, dma_addr, buf_size, DMA_TO_DEVICE);
kfree(buf);
2.4 dma_pool:DMA 缓冲池
dma_pool 用于管理固定大小的一致性 DMA 缓冲。它预分配一大块一致性内存,然后从中切分出多个固定大小的对象,避免频繁调用 dma_alloc_coherent 带来的开销和碎片。
核心 API 函数
| 函数 | 作用 |
|---|---|
dma_pool_create |
创建 DMA 缓冲池,指定对象大小、对齐和边界 |
dma_pool_alloc |
从池中分配一个对象,返回 CPU 地址和 DMA 地址 |
dma_pool_free |
将对象归还给池 |
dma_pool_destroy |
销毁池,释放所有预分配内存 |
典型用法:USB 传输描述符管理
struct dma_pool *td_pool;
struct td_descriptor {
__le32 flags;
__le32 buf_addr;
__le32 length;
// ... 其他字段
};
// 创建池:每个对象 64 字节,32 字节对齐,无边界限制
td_pool = dma_pool_create("usb_td", dev, sizeof(struct td_descriptor),
32, 0);
if (!td_pool) {
dev_err(dev, "Failed to create DMA pool\n");
return -ENOMEM;
}
// 分配一个描述符
struct td_descriptor *td;
dma_addr_t td_dma;
td = dma_pool_alloc(td_pool, GFP_ATOMIC, &td_dma);
if (!td) {
dev_err(dev, "Failed to allocate TD from pool\n");
return -ENOMEM;
}
// 使用 td 和 td_dma...
// 释放回池
dma_pool_free(td_pool, td, td_dma);
// 驱动卸载时销毁池
dma_pool_destroy(td_pool);
2.5 三种 API 的性能对比
下表从分配开销、缓存一致性和适用场景三个维度对比三种核心 DMA API:
| 特性 | dma_alloc_coherent | dma_map_single | dma_pool |
|---|---|---|---|
| 分配开销 | 高(物理连续、页对齐) | 低(仅建立映射) | 极低(预分配池) |
| 缓存一致性 | 硬件保证 | 软件显式同步 | 硬件保证(基于一致性) |
| CPU 访问延迟 | 较高(每次访问可能触发缓存操作) | 低(同步后直接访问) | 较高(同左) |
| 内存碎片 | 高(大块连续) | 无(使用已有内存) | 低(池化管理) |
| 典型大小 | 4KB ~ 2MB | 任意大小 | 32B ~ 4KB |
| 推荐场景 | 描述符环、控制寄存器 | 网络包、文件系统 I/O | USB TD、网络小包池 |
2.6 错误处理与调试
使用 DMA API 时,必须检查返回值并处理错误:
- dma_alloc_coherent:返回 NULL 表示分配失败,常见原因是内存不足或 GFP 标志不匹配。
- dma_map_single:使用
dma_mapping_error检查返回的 DMA 地址是否有效。 - dma_pool_alloc:返回 NULL 表示池中无可用对象,可考虑增大池容量或使用 GFP_ATOMIC。
// 检查 dma_map_single 是否成功
dma_addr_t dma_addr = dma_map_single(dev, buf, size, dir);
if (dma_mapping_error(dev, dma_addr)) {
dev_err(dev, "DMA mapping failed\n");
// 清理资源
kfree(buf);
return -EIO;
}
CONFIG_DMA_API_DEBUG 可以检测常见的 DMA API 误用,如映射后未解映射、使用错误的同步方向等。在开发阶段建议始终开启此选项。
2.7 本章小结
Linux DMA API 为驱动开发者提供了三种核心缓冲管理方式:
- dma_alloc_coherent:适用于小尺寸、高频访问的控制结构,硬件保证一致性但 CPU 访问延迟较高。
- dma_map_single:适用于大块数据传输,通过显式同步获得最佳性能,是网络和存储驱动的首选。
- dma_pool:适用于固定大小对象的频繁分配和释放,在 USB、网络子系统中广泛使用。