4. DMA API详解
在块设备驱动中,DMA(Direct Memory Access)是提升数据传输吞吐量的核心技术。Linux 内核提供了一套标准化的 DMA API,用于管理设备与内存之间的直接数据传输。本章将深入解析 dma_alloc_coherent、dma_map_single、dma_map_sg、dma_unmap_sg 等核心 API 的使用方法、适用场景以及内存屏障在 DMA 传输中的关键作用。
核心概念: DMA API 的核心目标是解决“设备可见地址”与“CPU 虚拟地址”之间的映射问题,并保证数据传输过程中的缓存一致性(Cache Coherency)。
4.1 DMA 地址与流式映射模型
理解 DMA API 之前,必须先区分两种基本的映射模型:一致性映射(Coherent Mapping) 和 流式映射(Streaming Mapping)。前者适用于长时间、频繁访问的缓冲区,后者适用于短时、一次性的数据传输。
| 特性 | 一致性映射(Coherent) | 流式映射(Streaming) |
|---|---|---|
| 典型 API | dma_alloc_coherent |
dma_map_single / dma_map_sg |
| 缓存一致性 | 硬件自动维护(或禁用缓存) | 软件通过 sync 操作维护 |
| 分配开销 | 较高(可能分配大块连续内存) | 较低(映射已有缓冲区) |
| 适用场景 | DMA 描述符环、控制结构 | 网络数据包、块设备 I/O 请求 |
| 生命周期 | 驱动加载到卸载 | 单次 I/O 操作 |
选择指南: 对于块设备驱动的 I/O 请求缓冲区,优先使用流式映射(
dma_map_sg),因为它能高效处理分散/聚集(SG)列表,且不浪费宝贵的连续物理内存。
4.2 dma_alloc_coherent:一致性内存分配
dma_alloc_coherent 用于分配一段在 CPU 和设备视角下均保持缓存一致性的内存区域。该函数返回 CPU 虚拟地址,并通过第三个参数返回 DMA 总线地址。
// 原型
void *dma_alloc_coherent(struct device *dev, size_t size,
dma_addr_t *dma_handle, gfp_t flag);
// 示例:分配 64KB 一致性缓冲区
struct device *dev = &pdev->dev;
dma_addr_t dma_handle;
void *cpu_addr;
cpu_addr = dma_alloc_coherent(dev, 64 * 1024, &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 区域或特殊内存池,分配大块内存时容易失败。建议仅在描述符环、状态寄存器等关键控制结构中使用。
4.3 dma_map_single:单缓冲区流式映射
当驱动需要将一块已有的内核缓冲区(如 kmalloc 或栈上分配的缓冲区)用于 DMA 传输时,使用 dma_map_single。该函数返回设备可用的 DMA 地址,并负责处理必要的缓存刷新操作。
// 原型
dma_addr_t dma_map_single(struct device *dev, void *cpu_addr,
size_t size, enum dma_data_direction dir);
// 示例:映射一个 4KB 的读缓冲区
void *buf = kmalloc(4096, GFP_KERNEL);
dma_addr_t dma_addr;
dma_addr = dma_map_single(dev, buf, 4096, DMA_FROM_DEVICE);
if (dma_mapping_error(dev, dma_addr)) {
dev_err(dev, "dma_map_single failed\n");
kfree(buf);
return -EIO;
}
// 启动 DMA 读操作:设备从 dma_addr 写入数据
// ... 等待 DMA 完成 ...
// 同步数据给 CPU(关键步骤)
dma_sync_single_for_cpu(dev, dma_addr, 4096, DMA_FROM_DEVICE);
// 现在可以安全读取 buf 中的数据
process_data(buf);
// 传输结束后解除映射
dma_unmap_single(dev, dma_addr, 4096, DMA_FROM_DEVICE);
kfree(buf);
方向枚举:
DMA_TO_DEVICE:CPU 写入,设备读取(如写磁盘请求)。DMA_FROM_DEVICE:设备写入,CPU 读取(如读磁盘完成)。DMA_BIDIRECTIONAL:双向传输,开销最大,谨慎使用。
4.4 dma_map_sg 与 dma_unmap_sg:分散/聚集映射
块设备驱动中,一个 I/O 请求通常对应多个不连续的物理内存片段(bio_vec)。dma_map_sg 是处理这种分散/聚集(SG)列表的标准接口。它不仅能映射多个片段,还能合并物理上连续的片段,减少 DMA 描述符数量。
// 原型
int dma_map_sg(struct device *dev, struct scatterlist *sg,
int nents, enum dma_data_direction dir);
// 示例:映射块设备请求的 SG 列表
struct scatterlist *sglist;
int nents, i, mapped;
// 假设 sglist 已由块层填充,nents 为原始片段数
mapped = dma_map_sg(dev, sglist, nents, DMA_TO_DEVICE);
if (mapped == 0) {
dev_err(dev, "dma_map_sg failed\n");
return -ENOMEM;
}
// 遍历映射后的 SG 列表(注意使用 sg_dma_address 和 sg_dma_len)
for_each_sg(sglist, sg, mapped, i) {
dma_addr_t dma_addr = sg_dma_address(sg);
unsigned int dma_len = sg_dma_len(sg);
// 将 dma_addr 和 dma_len 填入硬件 DMA 描述符
hw_desc[i].addr = cpu_to_le64(dma_addr);
hw_desc[i].len = cpu_to_le32(dma_len);
}
// 启动 DMA 传输...
// 传输完成后解除映射
dma_unmap_sg(dev, sglist, nents, DMA_TO_DEVICE);
| API | 输入参数 | 返回值 | 关键点 |
|---|---|---|---|
dma_map_sg |
原始 SG 列表、片段数、方向 | 映射后的有效片段数(可能小于 nents) | 使用 sg_dma_address() 获取 DMA 地址 |
dma_unmap_sg |
原始 SG 列表、原始 nents、方向 | void | 必须传入原始 nents,而非映射后的数量 |
dma_sync_sg_for_cpu |
SG 列表、nents、方向 | void | DMA_FROM_DEVICE 后调用,使 CPU 可见最新数据 |
dma_sync_sg_for_device |
SG 列表、nents、方向 | void | DMA_TO_DEVICE 前调用,刷新 CPU 缓存到内存 |
4.5 内存屏障在 DMA 传输中的作用
内存屏障(Memory Barrier)用于保证 CPU 和 DMA 控制器对内存访问的顺序一致性。在以下场景中必须使用屏障:
- 描述符写入后: 在将 DMA 描述符写入内存并通知硬件开始传输之前,必须确保描述符内容对设备可见。
- 状态读取前: 在读取设备更新的状态字段(如完成标志)之前,必须确保 CPU 看到的是最新写入。
- 缓存同步后: 在调用
dma_sync_single_for_cpu之后,通常需要一个读屏障来保证数据可见性。
// 典型 DMA 启动序列(含屏障)
// 1. 填充描述符到一致性内存
desc->addr = cpu_to_le64(dma_addr);
desc->len = cpu_to_le32(len);
desc->flags = cpu_to_le32(DESC_VALID);
// 2. 写屏障:确保描述符写入完成后再通知硬件
wmb(); // 写内存屏障
// 3. 通知硬件(写设备寄存器)
writel(1, host->regs + DOORBELL);
// --- 中断处理中 ---
// 4. 读屏障:确保读取状态前,设备已完成写入
rmb(); // 读内存屏障
// 5. 检查完成状态
if (desc->flags & cpu_to_le32(DESC_DONE)) {
// 处理完成的数据
}
常见错误: 遗漏
wmb() 可能导致硬件读到过时的描述符内容;遗漏 rmb() 可能导致 CPU 看到陈旧的完成标志。在 ARM/ARM64 架构上,dma_alloc_coherent 返回的内存通常不需要额外屏障,但流式映射区域必须谨慎处理。
4.6 综合案例:块设备请求的 DMA 传输流程
以下代码片段展示了在块设备驱动中,如何将 dma_map_sg 与内存屏障结合,完成一次完整的读请求处理:
static int myblock_submit_bio(struct bio *bio)
{
struct myblock_dev *dev = bio->bi_disk->private_data;
struct scatterlist sglist[BIO_MAX_PAGES];
struct bio_vec bvec;
struct bvec_iter iter;
int i = 0, mapped;
// 1. 将 bio 的片段填入 SG 列表
bio_for_each_segment(bvec, bio, iter) {
sg_set_page(&sglist[i], bvec.bv_page,
bvec.bv_len, bvec.bv_offset);
i++;
}
// 2. 映射 SG 列表到 DMA 地址
mapped = dma_map_sg(dev->dma_dev, sglist, i,
bio_data_dir(bio) == WRITE ?
DMA_TO_DEVICE : DMA_FROM_DEVICE);
if (mapped == 0) {
bio_io_error(bio);
return BLK_STS_IOERR;
}
// 3. 填充硬件描述符(使用 sg_dma_address / sg_dma_len)
for_each_sg(sglist, sg, mapped, i) {
dev->hw_desc[i].addr = cpu_to_le64(sg_dma_address(sg));
dev->hw_desc[i].len = cpu_to_le32(sg_dma_len(sg));
}
// 4. 写屏障 + 通知硬件
wmb();
writel(mapped, dev->regs + DMA_START);
// 5. 等待中断(简化处理)
wait_for_completion(&dev->dma_done);
// 6. 如果是读操作,同步数据给 CPU
if (bio_data_dir(bio) == READ) {
dma_sync_sg_for_cpu(dev->dma_dev, sglist, i, DMA_FROM_DEVICE);
rmb(); // 确保同步后的数据可见
}
// 7. 解除映射并结束 I/O
dma_unmap_sg(dev->dma_dev, sglist, i,
bio_data_dir(bio) == WRITE ?
DMA_TO_DEVICE : DMA_FROM_DEVICE);
bio_endio(bio);
return BLK_STS_OK;
}
性能要点:
- 尽量在 I/O 提交路径中批量映射 SG 列表,避免频繁调用
dma_map_single。 - 对于高性能 NVMe 或 SSD 控制器,考虑使用
dma_map_sg_attrs传递DMA_ATTR_SKIP_CPU_SYNC属性,由驱动手动控制同步时机。 - 内存屏障的开销在大多数架构上很小,但过度使用(如每个描述符都加屏障)会降低性能。通常只在描述符链首尾和状态检查点使用。
4.7 常见陷阱与调试建议
| 陷阱 | 现象 | 解决方案 |
|---|---|---|
遗漏 dma_sync_* 调用 |
读数据时看到旧值或全零 | 在 DMA_FROM_DEVICE 后必须调用 dma_sync_sg_for_cpu |
| 方向参数错误 | 数据损坏或 DMA 超时 | 严格区分 DMA_TO_DEVICE 和 DMA_FROM_DEVICE |
使用 dma_unmap_sg 时传入映射后的数量 |
内存泄漏或 DMA 错误 | 始终传入原始 nents,而非 mapped |
在中断上下文中调用 dma_alloc_coherent |
调度时原子违规 | 使用 GFP_ATOMIC 标志,或预分配缓冲区 |
忽略 dma_mapping_error 检查 |
使用无效 DMA 地址导致硬件异常 | 每次 dma_map_* 后必须检查返回值 |
调试工具: 启用内核配置
CONFIG_DMA_API_DEBUG 和 CONFIG_DMA_API_DEBUG_SG,可以在运行时检测常见的 DMA API 误用,如重复映射、方向错误、未同步等。