4. Linux DMA API 入门:核心函数与数据流
DMA(Direct Memory Access)是设备驱动中实现零拷贝、高吞吐数据搬运的基础设施。Linux 内核提供了一套标准化的 DMA API,屏蔽了不同架构(x86、ARM、RISC-V 等)的硬件差异。本章聚焦四个最核心的 API:dma_alloc_coherent、dma_map_single、dma_unmap_single 和 dma_map_sg,并深入其数据流与性能特征。
学习前提: 确保内核已开启
CONFIG_DMA_API_DEBUG,该选项可在开发阶段检测 DMA 映射泄漏、双释放等常见错误。
4.1 一致性 DMA 缓冲区:dma_alloc_coherent
dma_alloc_coherent 用于分配一段 CPU 与设备均可直接访问 的连续物理内存,且硬件会自动维护缓存一致性(Cache Coherency)。适用于描述符环、控制寄存器块等需要频繁同步的场景。
| 特性 | 说明 |
|---|---|
| 分配方式 | 从 CMA(Contiguous Memory Allocator)或预留区域分配物理连续页 |
| 一致性保证 | CPU 与 DMA 设备看到的内存视图始终一致,无需手动刷新缓存 |
| 典型用途 | 网卡描述符环、USB 控制器数据结构、音频 DMA 缓冲区 |
| 释放函数 | dma_free_coherent(dev, size, cpu_addr, dma_handle) |
// 分配 4KB 一致性缓冲区
struct device *dev = &pdev->dev;
size_t size = 4096;
dma_addr_t dma_handle;
void *cpu_addr;
cpu_addr = dma_alloc_coherent(dev, size, &dma_handle, GFP_KERNEL);
if (!cpu_addr) {
dev_err(dev, "Failed to allocate coherent DMA buffer\n");
return -ENOMEM;
}
// 使用缓冲区(CPU 与设备均可直接读写)
memset(cpu_addr, 0, size);
writel(desc_ready, dma_handle + offset); // 设备侧写入
// 释放
dma_free_coherent(dev, size, cpu_addr, dma_handle);
性能注意: 一致性缓冲区每次访问都可能触发总线事务,且分配开销较大。对于高频小数据块,应优先考虑流式映射(streaming DMA)。
4.2 流式 DMA 映射:dma_map_single / dma_unmap_single
流式映射用于将 已存在的内核缓冲区(如 skb->data、kmalloc 内存)映射给 DMA 设备使用。映射期间,CPU 必须通过 dma_sync_single_for_cpu / dma_sync_single_for_device 管理缓存一致性。
4.2.1 映射方向与语义
| 方向枚举 | 含义 | 典型场景 |
|---|---|---|
DMA_TO_DEVICE |
CPU 写入 → 设备读取 | 发送网络数据包 |
DMA_FROM_DEVICE |
设备写入 → CPU 读取 | 接收网络数据包 |
DMA_BIDIRECTIONAL |
双向传输 | 控制结构、状态查询 |
// 发送路径:映射 skb->data 给网卡 DMA 读取
struct sk_buff *skb = ...;
dma_addr_t dma_addr;
dma_addr = dma_map_single(dev, skb->data, skb->len, DMA_TO_DEVICE);
if (dma_mapping_error(dev, dma_addr)) {
dev_err(dev, "DMA mapping failed\n");
goto err_free_skb;
}
// 将 dma_addr 写入描述符,触发 DMA 发送
tx_desc->addr = cpu_to_le32(dma_addr);
tx_desc->len = cpu_to_le16(skb->len);
wmb(); // 确保描述符写入完成
writel(1, &priv->tx_regs->tx_start);
// 发送完成中断后,解除映射
dma_unmap_single(dev, dma_addr, skb->len, DMA_TO_DEVICE);
dev_kfree_skb(skb);
关键规则: 映射后的缓冲区在
dma_unmap_single 之前,CPU 不能直接读写(除非调用 sync 函数)。违反此规则会导致数据损坏或缓存一致性问题。
4.3 聚合映射:dma_map_sg
网络驱动中,一个 skb 可能包含多个不连续的物理片段(fragments)。dma_map_sg 将 分散-聚集列表(SG list) 批量映射为 DMA 可用的地址列表,大幅减少映射次数和描述符数量。
4.3.1 SG 列表结构
struct scatterlist {
unsigned long page_link; // 指向物理页或链表
unsigned int offset; // 页内偏移
unsigned int length; // 片段长度
dma_addr_t dma_address; // 映射后的 DMA 地址(由 API 填充)
unsigned int dma_length; // 映射后的 DMA 长度
};
4.3.2 映射与遍历
struct scatterlist sg[10];
int nents, i;
// 初始化 SG 列表(通常由网络栈填充)
skb_to_sgvec(skb, sg, 0, skb->len);
// 批量映射,返回有效 DMA 条目数
nents = dma_map_sg(dev, sg, sg_nents(sg), DMA_TO_DEVICE);
if (nents == 0) {
dev_err(dev, "dma_map_sg failed\n");
goto err;
}
// 遍历映射后的 DMA 地址,填充描述符
for_each_sg(sg, s, nents, i) {
tx_desc[i].addr = cpu_to_le32(sg_dma_address(s));
tx_desc[i].len = cpu_to_le16(sg_dma_len(s));
}
// 传输完成后解除映射
dma_unmap_sg(dev, sg, nents, DMA_TO_DEVICE);
| 对比维度 | dma_map_single | dma_map_sg |
|---|---|---|
| 输入 | 单个虚拟地址 + 长度 | scatterlist 数组 |
| 映射次数 | 每个缓冲区 1 次 | 批量映射,可能合并连续页 |
| 返回 | 单个 dma_addr_t | 有效 DMA 条目数(≤ 输入条目) |
| 典型场景 | 小数据块、控制结构 | 网络大数据包、存储 I/O |
| 性能优势 | 简单直接 | 减少描述符数量,降低 PCIe 事务开销 |
4.4 性能对比与选型建议
| API | 分配/映射开销 | 缓存一致性维护 | 推荐场景 |
|---|---|---|---|
| dma_alloc_coherent | 高(分配时清零) | 硬件自动 | 描述符环、控制结构 |
| dma_map_single | 低(仅映射) | 需 sync 管理 | 小数据包、控制消息 |
| dma_map_sg | 中(批量处理) | 需 sync 管理 | 大数据包、存储 I/O |
性能优化要点:
- 尽量在 中断上下文之外 完成映射操作,避免阻塞。
- 使用
dma_map_sg时,优先保证 SG 列表中的片段按物理地址排序,以最大化合并效率。 - 对于高频小数据,考虑使用 DMA 池(dma_pool) 预分配固定大小的一致性缓冲区。
- 启用
CONFIG_DMA_API_DEBUG后,内核会校验映射/释放的配对,但会引入额外开销,生产环境应关闭。
4.5 常见错误与调试
- 映射泄漏: 每次
dma_map_*必须有对应的dma_unmap_*,否则 DMA 地址空间耗尽。 - 方向错误: 使用
DMA_TO_DEVICE映射接收缓冲区,会导致设备写入时缓存未刷新,数据损坏。 - 双重释放: 对同一 DMA 地址调用两次
dma_unmap_*,内核会触发 WARN_ON。 - 未检查错误: 映射后必须调用
dma_mapping_error检查返回值,尤其在 IOMMU 使能时。
// 调试示例:使用 DMA API 调试宏
#define DEBUG_DMA
#ifdef DEBUG_DMA
#define dma_map_single_dbg(dev, addr, size, dir) ({ \
dma_addr_t __dma = dma_map_single(dev, addr, size, dir); \
dev_dbg(dev, "dma_map_single: cpu=%p dma=%pad size=%zu\n", \
addr, &__dma, size); \
__dma; \
})
#endif