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 回写 | 必须配对调用 |
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
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 选择决策流程
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,并严格遵守映射/同步/解映射的调用契约,才能保证数据正确性和系统性能。