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)。
一致性 DMA 缓冲区访问模型 CPU 一致性 DMA 缓冲区 DMA 设备 • CPU 与设备通过同一物理地址访问缓冲区 • 硬件自动维护缓存一致性,无需软件刷新 • 适用于描述符环、控制结构等低频同步场景

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 函数)。违反此规则会导致数据损坏或缓存一致性问题。
流式 DMA 映射生命周期 分配缓冲区 dma_map_single DMA 传输 unmap 同步操作(可选,用于部分传输场景) • dma_sync_single_for_cpu(dev, dma_addr, size, dir) — 传输中 CPU 读取 • dma_sync_single_for_device(dev, dma_addr, size, dir) — 传输中 CPU 写入 • 同步开销较大,应尽量减少调用次数 • 完整传输后直接 unmap,无需额外 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 事务开销
dma_map_sg 聚合映射流程 输入 SG 列表 sg[0]: page 0x1000, len 2048 sg[1]: page 0x2000, len 1024 sg[2]: page 0x3000, len 4096 sg[3]: page 0x4000, len 512 sg[4]: page 0x5000, len 2048 dma_map_sg 合并连续页 返回 nents=3 输出 DMA 地址 dma[0]: addr 0x8000, len 3072 dma[1]: addr 0x9000, len 4096 dma[2]: addr 0xA000, len 2560 (5 个片段 → 3 个 DMA 条目)

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