2. Linux DMA API 概览:核心分配与映射接口

在 videobuf2 框架中,DMA 内存的分配与映射是驱动开发者必须掌握的基础能力。Linux 内核提供了多套 DMA API,分别适用于不同场景:一致性分配、流式映射、分散/聚集映射 和 DMA 池。本章将逐一剖析这些 API 的语义、适用场景与典型用法。

核心原则:DMA API 的选择取决于硬件对缓存一致性的要求、传输方向以及数据结构的尺寸。选错 API 轻则性能下降,重则导致数据损坏。

2.1 dma_alloc_coherent —— 一致性 DMA 缓冲区

dma_alloc_coherent 分配一段 CPU 与 DMA 控制器共享且缓存一致 的内存。硬件在访问该内存时不会产生缓存一致性问题,适合用于描述符环、控制寄存器块等需要频繁同步的数据结构。

属性 说明
分配方式 连续物理内存(通常来自 CMA 或原子池)
缓存策略 非缓存(Non-cacheable)或写合并(Write-combine)
返回地址 CPU 虚拟地址 + DMA 总线地址
释放接口 dma_free_coherent
典型用途 描述符表、控制寄存器、状态反馈区
// 分配 4KB 一致性缓冲区
dma_addr_t dma_handle;
void *cpu_addr;

cpu_addr = dma_alloc_coherent(dev, SZ_4K, &dma_handle, GFP_KERNEL);
if (!cpu_addr) {
    dev_err(dev, "Failed to allocate coherent DMA memory\n");
    return -ENOMEM;
}

// 使用完毕后释放
dma_free_coherent(dev, SZ_4K, cpu_addr, dma_handle);
注意:一致性分配开销较大,且占用宝贵的连续物理内存。对于大数据块(如视频帧缓冲区),应优先考虑流式映射而非一致性分配。
dma_alloc_coherent 内存视图 CPU 虚拟地址空间 cpu_addr (0xFFFF...) DMA 总线地址空间 dma_handle (0x8FFF...) 物理内存(连续,非缓存) 4KB 对齐,无 cache alias CPU 和 DMA 通过同一段物理内存通信,无需手动 cache 刷新

2.2 dma_map_single —— 流式映射单缓冲区

dma_map_single 将一段 已存在的内核缓冲区(通常来自 kmalloc 或栈)映射到 DMA 总线地址,并执行必要的 cache 维护操作。它适用于一次性或短时传输,映射开销低,但需要驱动在传输前后显式处理 cache 同步。

步骤 API 调用 说明
1 dma_map_single(dev, cpu_addr, size, dir) 获取 DMA 地址,执行 cache 刷新
2 dma_unmap_single(dev, dma_handle, size, dir) 传输完成后解除映射
3 dma_sync_single_for_cpu/device 部分同步(可选,用于部分更新)
void *buffer = kmalloc(buf_size, GFP_KERNEL);
dma_addr_t dma_addr;

dma_addr = dma_map_single(dev, buffer, buf_size, DMA_FROM_DEVICE);
if (dma_mapping_error(dev, dma_addr)) {
    kfree(buffer);
    return -EIO;
}

// 启动 DMA 传输...
// 等待传输完成

dma_unmap_single(dev, dma_addr, buf_size, DMA_FROM_DEVICE);
kfree(buffer);
性能提示:流式映射不要求物理连续性,但映射操作本身涉及 cache 刷写。对于高频小数据块,建议在初始化时一次性映射并复用 DMA 地址。
dma_map_single 生命周期 kmalloc 分配缓冲区 dma_map_single DMA 传输进行中 dma_unmap_single CPU 可读写 cache 可能脏 cache 刷写到内存 DMA 地址有效 cache 失效 CPU 重新可见

2.3 dma_map_sg —— 分散/聚集映射

对于非连续的物理内存(如来自 __get_free_pages 或 vmalloc 的页面),dma_map_sg 将 分散/聚集列表(SG list) 映射为 DMA 可访问的地址集合。它是 videobuf2 中处理用户态缓冲区、帧缓冲区的核心映射方式。

概念 说明
SG 表项 每个 scatterlist 描述一段物理连续区域
映射后 每个 SG 表项获得 DMA 地址和长度
返回值 映射成功的 DMA 段数(可能少于原始 SG 条目数)
解除映射 dma_unmap_sg
struct scatterlist *sgl;
int nents, i;

// 假设 sgl 已初始化,nents 为 SG 条目数
nents = dma_map_sg(dev, sgl, nents, DMA_BIDIRECTIONAL);
if (nents == 0) {
    dev_err(dev, "dma_map_sg failed\n");
    return -ENOMEM;
}

// 遍历映射后的 SG 表
for_each_sg(sgl, sg, nents, i) {
    dma_addr_t dma_addr = sg_dma_address(sg);
    unsigned int dma_len = sg_dma_len(sg);
    // 使用 dma_addr / dma_len 配置 DMA 描述符
}

// 传输完成后解除映射
dma_unmap_sg(dev, sgl, nents, DMA_BIDIRECTIONAL);
videobuf2 关联:在 vb2 的 buf_prepare 回调中,驱动通常调用 dma_map_sg 将帧缓冲区映射到 DMA 地址,并将结果填入硬件描述符链。
dma_map_sg 分散/聚集映射 物理页 0 物理页 1 物理页 2 SG[0]: page 0, len 4K SG[1]: page 1, len 4K SG[2]: page 2, len 4K DMA addr 0xA000 DMA addr 0xB000 DMA addr 0xC000 dma_map_sg 将分散物理页映射为连续 DMA 地址空间

2.4 dma_pool —— 小尺寸 DMA 缓冲区池

dma_pool 用于管理 固定大小、频繁分配/释放 的小型 DMA 缓冲区(如 USB 请求块、网络描述符)。它预分配一大块一致性内存,然后从中切分固定大小的对象,减少分配开销和碎片。

操作 API 说明
创建池 dma_pool_create(name, dev, size, align, alloc) 指定对象大小、对齐和边界
分配对象 dma_pool_alloc(pool, flags, &dma_handle) 返回 CPU 地址和 DMA 地址
释放对象 dma_pool_free(pool, cpu_addr, dma_handle) 归还对象到池中
销毁池 dma_pool_destroy(pool) 释放所有预分配内存
struct dma_pool *pool;
dma_addr_t dma_handle;
void *cpu_addr;

// 创建对象大小为 64 字节、16 字节对齐的 DMA 池
pool = dma_pool_create("my_desc_pool", dev, 64, 16, 0);
if (!pool)
    return -ENOMEM;

// 从池中分配一个对象
cpu_addr = dma_pool_alloc(pool, GFP_KERNEL, &dma_handle);
if (!cpu_addr) {
    dma_pool_destroy(pool);
    return -ENOMEM;
}

// 使用 cpu_addr / dma_handle ...

// 释放对象
dma_pool_free(pool, cpu_addr, dma_handle);
dma_pool_destroy(pool);
对齐约束:dma_pool 的 align 参数必须是 2 的幂,且不能超过 PAGE_SIZE。对于需要特定硬件对齐的描述符(如 64 字节对齐),务必正确设置此参数。

2.5 API 对比与选型指南

下表从多个维度对比四种 DMA API,帮助你在不同场景下做出正确选择:

特性 dma_alloc_coherent dma_map_single dma_map_sg dma_pool
内存来源 内核分配 已有缓冲区 SG 列表 预分配池
物理连续性 连续 不要求 不要求 连续
缓存一致性 硬件保证 需手动 sync 需手动 sync 硬件保证
分配开销 高 低 中 极低
典型对象尺寸 任意 任意 大块(帧) 小(≤ 4KB)
vb2 中使用 描述符 少用 帧缓冲区 硬件上下文
实战建议:在 videobuf2 驱动中,dma_map_sg 是处理帧缓冲区的主力 API;dma_alloc_coherent 用于分配描述符环和硬件寄存器备份区;dma_pool 适合管理 USB 或网络类驱动中的小尺寸控制结构。
DMA API 选型决策树 需要 DMA 内存? 小对象频繁分配? 大块数据? 需要一致性? dma_pool dma_map_sg dma_alloc_coherent dma_map_single 根据数据特征选择最合适的 DMA API,避免一刀切使用 dma_alloc_coherent

2.6 常见错误与调试技巧

  • 忘记检查 dma_mapping_error:映射失败时返回的 DMA 地址可能为 0 或特殊标记,必须用 dma_mapping_error 验证。
  • 方向错误:DMA_FROM_DEVICE 和 DMA_TO_DEVICE 混淆会导致 cache 刷写方向错误,数据损坏。
  • 重复映射:对同一缓冲区多次调用 dma_map_single 而不先 unmap,会造成资源泄漏。
  • SG 遍历错误:使用 for_each_sg 时,循环次数应使用 dma_map_sg 的返回值,而非原始 nents。
调试手段:开启内核配置 CONFIG_DMA_API_DEBUG 和 CONFIG_DMA_API_DEBUG_SG,可以在运行时检测常见的 DMA API 误用,如未映射就使用、方向不匹配等。