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);
注意:一致性分配开销较大,且占用宝贵的连续物理内存。对于大数据块(如视频帧缓冲区),应优先考虑流式映射而非一致性分配。
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 地址。
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 地址,并将结果填入硬件描述符链。
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 或网络类驱动中的小尺寸控制结构。
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 误用,如未映射就使用、方向不匹配等。