4. DMA API 实战:缓存一致性编程核心接口
DMA(Direct Memory Access)是 Linux 驱动中实现高性能数据传输的核心机制。然而,DMA 与 CPU 缓存之间的数据不一致性是驱动开发中最常见的陷阱之一。本章通过三个实战维度——流式映射、一致性内存和同步操作——系统讲解 DMA API 的正确使用方法和调试要点。
核心认知:DMA 操作的本质是让外设直接读写系统内存,而 CPU 缓存可能持有这些内存区域的旧副本。DMA API 的作用就是在 CPU 缓存与设备之间建立正确的数据可见性屏障。
4.1 流式 DMA 映射:dma_map_single / dma_unmap_single
dma_map_single 和 dma_unmap_single 是流式 DMA 映射(Streaming DMA Mapping)的核心接口,适用于短时、一次性的 DMA 传输场景。它们不保证缓存一致性,而是通过显式的同步操作来维护数据一致性。
4.1.1 接口原型与参数说明
| 函数 | 原型 | 关键参数 |
|---|---|---|
dma_map_single |
dma_addr_t dma_map_single(struct device *dev, void *cpu_addr, size_t size, enum dma_data_direction dir) |
dev: 设备指针 cpu_addr: CPU 虚拟地址 size: 映射字节数 dir: 数据传输方向 |
dma_unmap_single |
void dma_unmap_single(struct device *dev, dma_addr_t dma_addr, size_t size, enum dma_data_direction dir) |
dma_addr: 由 map 返回的 DMA 总线地址 |
4.1.2 数据传输方向枚举
| 方向常量 | 含义 | 缓存操作要求 |
|---|---|---|
DMA_TO_DEVICE |
CPU → 设备(如发送数据) | map 前需将 CPU 缓存数据刷出(写回内存) |
DMA_FROM_DEVICE |
设备 → CPU(如接收数据) | unmap 前需使 CPU 缓存行失效(从内存重新读取) |
DMA_BIDIRECTIONAL |
双向传输 | 同时执行刷出和失效操作 |
DMA_NONE |
调试用途 | 无操作 |
4.1.3 标准使用流程
- 分配缓冲区:使用
kmalloc或__get_free_pages分配 CPU 可访问的缓冲区。 - 调用 dma_map_single:将 CPU 虚拟地址转换为 DMA 总线地址,同时根据方向执行缓存同步。
- 检查返回值:使用
dma_mapping_error验证映射是否成功。 - 启动 DMA:将 DMA 地址写入设备寄存器,触发传输。
- 等待传输完成:通过中断或轮询确认 DMA 结束。
- 调用 dma_unmap_single:解除映射,并根据方向执行缓存同步。
/* 流式 DMA 发送示例:CPU → 设备 */
void *cpu_buf;
dma_addr_t dma_buf;
struct device *dev = &pdev->dev;
/* 1. 分配缓冲区 */
cpu_buf = kmalloc(BUF_SIZE, GFP_KERNEL);
if (!cpu_buf)
return -ENOMEM;
/* 2. 填充数据 */
memcpy(cpu_buf, "Hello DMA", 10);
/* 3. 映射到 DMA 地址(自动刷出缓存) */
dma_buf = dma_map_single(dev, cpu_buf, BUF_SIZE, DMA_TO_DEVICE);
if (dma_mapping_error(dev, dma_buf)) {
kfree(cpu_buf);
return -EIO;
}
/* 4. 启动 DMA 传输(将 dma_buf 写入设备寄存器) */
writel(dma_buf, dev->regs + DMA_SRC_ADDR);
writel(BUF_SIZE, dev->regs + DMA_LEN);
writel(1, dev->regs + DMA_START);
/* 5. 等待中断... */
/* 6. 解除映射 */
dma_unmap_single(dev, dma_buf, BUF_SIZE, DMA_TO_DEVICE);
kfree(cpu_buf);
避坑指南:流式映射的缓冲区必须保证在 map 和 unmap 期间不被 CPU 访问。如果需要在 DMA 进行中读写缓冲区,必须使用
dma_sync_single_for_cpu/device 显式同步。
4.2 一致性 DMA 内存:dma_alloc_coherent
dma_alloc_coherent 分配的是缓存一致性内存(Coherent Memory),即 CPU 和设备看到的始终是同一份数据,无需手动同步。这种内存通常通过关闭 CPU 缓存或使用硬件一致性协议(如 IOMMU/SMMU)实现。
4.2.1 接口原型与特点
| 函数 | 原型 | 特点 |
|---|---|---|
dma_alloc_coherent |
void *dma_alloc_coherent(struct device *dev, size_t size, dma_addr_t *dma_handle, gfp_t flag) |
返回 CPU 虚拟地址,通过 dma_handle 返回 DMA 总线地址 |
dma_free_coherent |
void dma_free_coherent(struct device *dev, size_t size, void *cpu_addr, dma_addr_t dma_handle) |
释放一致性内存 |
4.2.2 适用场景
- 控制结构体:如描述符环(Descriptor Ring)、命令队列。
- 频繁小数据交换:状态寄存器、统计计数器。
- 实时性要求高:不允许同步操作带来的延迟抖动。
/* 分配一致性内存示例 */
struct my_desc {
u32 src_addr;
u32 dst_addr;
u32 len;
u32 flags;
};
struct my_desc *desc_cpu;
dma_addr_t desc_dma;
/* 分配一致性内存 */
desc_cpu = dma_alloc_coherent(dev, sizeof(struct my_desc) * NUM_DESC,
&desc_dma, GFP_KERNEL);
if (!desc_cpu)
return -ENOMEM;
/* 初始化描述符(CPU 直接写,设备立即可见) */
desc_cpu[0].src_addr = cpu_to_le32(src_phys);
desc_cpu[0].dst_addr = cpu_to_le32(dst_phys);
desc_cpu[0].len = cpu_to_le32(1024);
desc_cpu[0].flags = cpu_to_le32(DESC_VALID);
/* 将 DMA 地址写入设备 */
writel(desc_dma, dev->regs + DESC_BASE);
/* 使用完毕后释放 */
dma_free_coherent(dev, sizeof(struct my_desc) * NUM_DESC,
desc_cpu, desc_dma);
性能警告:一致性内存的每次 CPU 访问都可能触发完整的缓存行同步,导致性能低于流式映射。对于大数据块传输(如网络包、文件数据),应优先使用流式映射 + 显式同步。
4.3 DMA 同步操作:dma_sync_single_for_cpu/device
当使用流式映射且需要在 DMA 传输过程中由 CPU 访问缓冲区时,必须使用同步 API 来保证数据一致性。这两个函数是流式 DMA 编程中最容易被遗漏的接口。
4.3.1 接口原型
| 函数 | 作用 | 调用时机 |
|---|---|---|
dma_sync_single_for_cpu |
将 DMA 缓冲区所有权交还给 CPU,使设备写入的数据对 CPU 可见 | CPU 读取设备写入的数据之前 |
dma_sync_single_for_device |
将 DMA 缓冲区所有权交给设备,确保 CPU 写入的数据被刷出缓存 | CPU 写入数据后、启动 DMA 之前 |
4.3.2 典型使用模式
/* 接收路径:设备写入 → CPU 读取 */
/* 1. 启动 DMA 接收 */
dma_buf = dma_map_single(dev, cpu_buf, BUF_SIZE, DMA_FROM_DEVICE);
writel(dma_buf, dev->regs + DMA_ADDR);
writel(1, dev->regs + DMA_START);
/* 2. 等待中断,DMA 完成 */
/* 3. 将所有权交还给 CPU,使设备写入的数据可见 */
dma_sync_single_for_cpu(dev, dma_buf, BUF_SIZE, DMA_FROM_DEVICE);
/* 4. CPU 读取数据 */
process_data(cpu_buf, BUF_SIZE);
/* 5. 如果还要继续 DMA,将所有权交还给设备 */
dma_sync_single_for_device(dev, dma_buf, BUF_SIZE, DMA_FROM_DEVICE);
/* 6. 启动下一次 DMA */
writel(1, dev->regs + DMA_START);
关键原则:同步操作必须成对使用。在任意时刻,缓冲区只能有一个所有者——要么是 CPU,要么是设备。违反此原则会导致数据损坏或缓存一致性问题。
4.4 三种 DMA 内存模型对比
| 特性 | 流式映射(map/unmap) | 一致性内存(coherent) | 流式映射 + 同步(sync) |
|---|---|---|---|
| 缓存一致性 | 由 API 在 map/unmap 时维护 | 硬件保证始终一致 | 由开发者显式同步维护 |
| 适用数据量 | 大块数据(KB ~ MB) | 小块控制数据(B ~ KB) | 中等数据,需 CPU 参与 |
| 性能开销 | 低(仅 map/unmap 时同步) | 高(每次访问都同步) | 中等(按需同步) |
| 编程复杂度 | 中 | 低 | 高 |
| 典型场景 | 网络包收发、文件 I/O | 描述符环、寄存器镜像 | 零拷贝、部分更新 |
4.5 DMA 缓存一致性操作流程(SVG 架构图)
4.6 调试技巧与常见错误
4.6.1 调试手段
- 启用 DMA API 调试:内核配置
CONFIG_DMA_API_DEBUG,可检测 map/unmap 不匹配、方向错误等。 - 使用 IOMMU/IOVA 调试:通过
/sys/kernel/debug/dma-api/查看映射统计。 - 缓存行对齐检查:使用
dma_get_cache_alignment()获取平台缓存行大小,确保缓冲区起始地址和大小对齐。
4.6.2 常见错误清单
| 错误类型 | 现象 | 根因 | 解决方案 |
|---|---|---|---|
| 遗漏 unmap | DMA 地址泄漏,IOMMU 页表耗尽 | map 后未在适当路径调用 unmap | 使用 dma_unmap_single 配对,考虑使用 devm_ 版本 |
| 方向错误 | 数据损坏或设备超时 | map 时方向与传输方向不一致 | 严格按数据流选择 DMA_TO/FROM_DEVICE |
| 同步遗漏 | CPU 读到旧数据或设备读到脏数据 | DMA 进行中 CPU 访问缓冲区未同步 | 在 CPU 访问前后调用 sync 函数 |
| 缓冲区未对齐 | 某些平台 DMA 失败或性能下降 | 缓冲区地址未按缓存行对齐 | 使用 kmalloc 时指定 GFP_DMA 或 aligned_alloc |
| 并发访问 | 竞态导致数据不一致 | 多 CPU 同时访问同一 DMA 缓冲区 | 使用自旋锁或 per-CPU 缓冲区 |
最佳实践:在驱动开发初期就启用
CONFIG_DMA_API_DEBUG,并在所有 map/unmap 路径上添加 dma_mapping_error 检查。这可以在开发阶段捕获 90% 以上的 DMA 缓存一致性问题。