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 标准使用流程

  1. 分配缓冲区:使用 kmalloc 或 __get_free_pages 分配 CPU 可访问的缓冲区。
  2. 调用 dma_map_single:将 CPU 虚拟地址转换为 DMA 总线地址,同时根据方向执行缓存同步。
  3. 检查返回值:使用 dma_mapping_error 验证映射是否成功。
  4. 启动 DMA:将 DMA 地址写入设备寄存器,触发传输。
  5. 等待传输完成:通过中断或轮询确认 DMA 结束。
  6. 调用 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 架构图)

DMA 缓存一致性操作流程 CPU 缓存 (Cache) 系统内存 (DRAM) DMA 外设 流式映射 (dma_map_single / dma_unmap_single) 刷出 (TO_DEVICE) 失效 (FROM_DEVICE) DMA 读取 DMA 写入 显式同步 (dma_sync_single_for_cpu / for_device) for_cpu (失效缓存) for_device (刷出缓存) 一致性内存 (dma_alloc_coherent)

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 缓存一致性问题。