3. Linux DMA API 详解:核心函数的使用场景与对比

在 V4L2 驱动中,DMA 缓冲区分配与缓存一致性管理是决定视频数据能否正确、高效传输的关键。Linux 内核提供了多套 DMA API,分别适用于不同的缓冲区生命周期和映射策略。本章将深入剖析 dma_alloc_coherent、dma_map_single、dma_map_sg 及对应的 unmap 函数,明确其使用场景、性能特征和注意事项。

3.1 DMA API 分类概览

Linux DMA API 按缓冲区管理方式可分为两类:一致性映射(Coherent Mapping) 和 流式映射(Streaming Mapping)。前者保证 CPU 与设备访问的一致性,后者通过显式同步操作管理缓存。

API 函数 类型 适用场景 缓存一致性处理 典型开销
dma_alloc_coherent 一致性映射 长期使用的控制结构、描述符环 硬件自动维护 分配慢,访问延迟高
dma_map_single 流式映射 单次传输的连续缓冲区 需手动 sync 映射快,适合短时使用
dma_map_sg 流式映射(聚合) 分散/聚合 DMA(SG DMA) 需手动 sync 映射快,支持非连续内存
dma_unmap_sg 流式映射(释放) SG DMA 传输完成后的清理 自动触发 cache 回写 必须配对调用
选择原则: 对于 V4L2 视频帧缓冲区(频繁更新、大块数据),优先使用流式映射;对于 video_device 注册用的控制结构、vb2 队列管理结构,使用一致性映射。

3.2 dma_alloc_coherent:一致性 DMA 缓冲区

dma_alloc_coherent 分配一块物理连续且缓存一致性由硬件保证的内存。CPU 和 DMA 设备看到的视图始终一致,无需软件干预。

// 原型
void *dma_alloc_coherent(struct device *dev, size_t size,
                         dma_addr_t *dma_handle, gfp_t flag);

// 典型用法:分配描述符环
struct my_desc_ring {
    u32 status;
    u32 buf_addr;
    u32 length;
};

struct my_desc_ring *ring;
dma_addr_t ring_dma;

ring = dma_alloc_coherent(dev, PAGE_SIZE, &ring_dma, GFP_KERNEL);
if (!ring) {
    dev_err(dev, "Failed to allocate coherent descriptor ring\n");
    return -ENOMEM;
}
// 初始化描述符
ring[0].status = 0;
ring[0].buf_addr = cpu_to_le32(some_dma_addr);
ring[0].length = cpu_to_le32(1024);
// 设备可直接访问 ring_dma,无需 sync
性能陷阱: 一致性映射内部通常禁用 CPU 缓存或使用非缓存映射,导致 CPU 读写延迟较高。不要在视频帧数据路径上使用 dma_alloc_coherent 分配大块缓冲区。

3.3 dma_map_single:流式单缓冲区映射

dma_map_single 将一段已分配的虚拟地址映射为 DMA 总线地址,并执行必要的 cache 操作。适用于单次、连续、短生命周期的传输。

// 原型
dma_addr_t dma_map_single(struct device *dev, void *cpu_addr,
                          size_t size, enum dma_data_direction dir);

// 典型用法:映射视频帧缓冲区
void *frame_buf = vb2_plane_vaddr(&vb, 0);
dma_addr_t dma_addr;

dma_addr = dma_map_single(dev, frame_buf, size, DMA_FROM_DEVICE);
if (dma_mapping_error(dev, dma_addr)) {
    dev_err(dev, "dma_map_single failed\n");
    return -EIO;
}

// 将 dma_addr 写入 DMA 描述符,启动传输
// 传输完成后必须 unmap
dma_unmap_single(dev, dma_addr, size, DMA_FROM_DEVICE);
方向参数:
  • DMA_FROM_DEVICE:设备写内存(如摄像头采集)
  • DMA_TO_DEVICE:CPU 写内存给设备(如显示输出)
  • DMA_BIDIRECTIONAL:双向传输(慎用,性能较差)

3.4 dma_map_sg / dma_unmap_sg:分散/聚合 DMA

当视频帧由多个不连续的物理页组成时(如用户态分配的非连续缓冲区),dma_map_sg 将 scatterlist 链表映射为 DMA 可用的分散/聚合列表。这是 V4L2 驱动中最常用的映射方式。

// 原型
int dma_map_sg(struct device *dev, struct scatterlist *sg,
               int nents, enum dma_data_direction dir);

// 典型用法:映射 vb2 缓冲区中的 scatterlist
struct vb2_buffer *vb = ...;
struct scatterlist *sgl = vb2_dma_sg_plane_desc(vb, 0);
int n_sg = dma_map_sg(dev, sgl, sg_nents(sgl), DMA_FROM_DEVICE);
if (n_sg == 0) {
    dev_err(dev, "dma_map_sg failed\n");
    return -EIO;
}

// 遍历映射后的 SG 条目,填充 DMA 描述符
struct scatterlist *sg;
int i;
for_each_sg(sgl, sg, n_sg, i) {
    dma_addr_t addr = sg_dma_address(sg);
    unsigned int len = sg_dma_len(sg);
    // 将 addr, len 写入硬件描述符
}

// 传输完成后
dma_unmap_sg(dev, sgl, sg_nents(sgl), DMA_FROM_DEVICE);
关键区别: sg_nents(sgl) 是原始物理段数,dma_map_sg 返回值是映射后的 DMA 段数(可能因合并而减少)。遍历时必须使用 sg_dma_address 和 sg_dma_len,而非 sg_phys 和 sg->length。

3.5 缓存同步操作:dma_sync_single_for_* / dma_sync_sg_for_*

流式映射在传输过程中,如果 CPU 需要访问缓冲区(如预览、分析),必须显式调用同步函数,否则会导致数据不一致。

场景 调用函数 说明
设备写入完成,CPU 读取 dma_sync_single_for_cpu 将 cache 无效化,确保 CPU 读到设备写入的最新数据
CPU 修改数据,设备读取 dma_sync_single_for_device 将 cache 回写,确保设备看到 CPU 写入的数据
SG 列表同步 dma_sync_sg_for_cpu/device 与 single 版本语义相同,但作用于整个 SG 列表
// 示例:CPU 读取采集完成的帧
dma_sync_single_for_cpu(dev, dma_addr, size, DMA_FROM_DEVICE);
// 此时 CPU 可以安全访问 frame_buf
process_frame(frame_buf);
// CPU 处理完成后,重新交给设备
dma_sync_single_for_device(dev, dma_addr, size, DMA_FROM_DEVICE);

3.6 DMA API 选择决策流程

DMA API 选择决策流程 开始选择 DMA API 缓冲区生命周期长? 是 否 dma_alloc_coherent 流式映射 内存是否连续? 是 否 dma_map_single dma_map_sg 流式映射必须配对 unmap CPU 访问前调用 dma_sync_*_for_cpu,访问后调用 dma_sync_*_for_device

3.7 常见错误与调试技巧

  • 忘记 unmap: 流式映射必须与 unmap 配对,否则会导致 cache 污染和内存泄漏。使用 CONFIG_DMA_API_DEBUG 内核选项可检测此类错误。
  • 方向错误: 使用 DMA_FROM_DEVICE 但 CPU 先写入数据,会导致设备读到旧数据。务必根据实际数据流向选择方向。
  • SG 遍历错误: 使用 sg->length 而非 sg_dma_len(sg),导致 DMA 传输长度错误。映射后必须使用 DMA 专用访问器。
  • 同步遗漏: 在流式映射有效期内,CPU 访问缓冲区前未调用 dma_sync_single_for_cpu,导致读取到 stale cache 数据。
调试命令:
# 启用 DMA API 调试
echo 1 > /sys/kernel/debug/dma-api/dump
# 查看 DMA 映射泄漏
cat /sys/kernel/debug/dma-api/all_errors

3.8 本章小结

Linux DMA API 提供了灵活的内存映射机制:dma_alloc_coherent 适用于控制结构等长生命周期场景,dma_map_single 和 dma_map_sg 适用于视频帧数据等短生命周期场景。V4L2 驱动开发者必须根据缓冲区生命周期、连续性要求和访问模式选择合适的 API,并严格遵守映射/同步/解映射的调用契约,才能保证数据正确性和系统性能。