4. Linux DMA API 概述:核心内存映射接口

Linux 内核提供了一套标准化的 DMA API,用于在驱动程序和硬件之间安全、高效地管理内存缓冲区。本章深入剖析三个最核心的 API:dma_alloc_coherent、dma_map_single 和 dma_map_sg,涵盖其语义、适用场景、参数含义及底层内存映射机制。

4.1 DMA API 设计原则与分类

DMA API 的设计遵循两条核心原则:设备无关性和缓存一致性管理。根据缓冲区使用模式,API 分为两类:

类别 核心 API 缓存一致性 典型场景
一致性映射 (Coherent) dma_alloc_coherent 硬件与 CPU 始终一致,无需手动刷新 描述符环、控制寄存器、持续轮询的状态区
流式映射 (Streaming) dma_map_single / dma_map_sg 需要显式同步(dma_sync_single_for_cpu/device) 网络数据包、块设备 I/O、一次性传输
选择指南: 如果设备需要频繁、随机地访问同一块内存(如 DMA 描述符),使用一致性映射;如果数据是流式传输(如网络帧),使用流式映射以获得更好的性能。

4.2 dma_alloc_coherent:一致性 DMA 缓冲区

dma_alloc_coherent 分配一段物理连续且缓存一致的内存,返回 CPU 虚拟地址和 DMA 总线地址。其核心机制是:分配时禁用该内存区域的 CPU 缓存行(或使用非缓存映射),从而避免缓存一致性问题。

// 原型
void *dma_alloc_coherent(struct device *dev, size_t size,
                         dma_addr_t *dma_handle, gfp_t flag);

// 示例:分配 64KB 一致性缓冲区
dma_addr_t dma_handle;
void *cpu_addr;

cpu_addr = dma_alloc_coherent(&pdev->dev, 64 * 1024,
                              &dma_handle, GFP_KERNEL);
if (!cpu_addr) {
    dev_err(&pdev->dev, "Failed to allocate coherent DMA memory\n");
    return -ENOMEM;
}
// 使用 cpu_addr 和 dma_handle 编程硬件寄存器
// ...
dma_free_coherent(&pdev->dev, 64 * 1024, cpu_addr, dma_handle);
关键参数:
  • dev:DMA 设备结构体,用于关联 IOMMU/总线。
  • size:请求字节数,必须为页大小(PAGE_SIZE)的整数倍。
  • dma_handle:输出参数,返回设备可用的 DMA 总线地址。
  • flag:内存分配标志,通常为 GFP_KERNEL(可睡眠)或 GFP_ATOMIC(中断上下文)。
dma_alloc_coherent 内存映射架构 CPU 虚拟地址 cpu_addr (0xC0001000) 内核页表映射 物理内存页 连续物理页框 IOMMU 页表 DMA 总线地址 dma_handle (0xFFA00000) CPU 与设备通过不同的地址空间访问同一段物理内存,IOMMU 负责地址转换

4.3 dma_map_single:流式单缓冲区映射

dma_map_single 将一段已分配的虚拟地址映射为 DMA 总线地址,并执行必要的缓存刷新操作。它不分配物理内存,只建立映射关系。

// 原型
dma_addr_t dma_map_single(struct device *dev, void *cpu_addr,
                          size_t size, enum dma_data_direction dir);

// 示例:映射一个网络 skb 的数据区
struct sk_buff *skb = ...;
dma_addr_t dma_addr;

dma_addr = dma_map_single(&pdev->dev, skb->data, skb->len,
                          DMA_TO_DEVICE);
if (dma_mapping_error(&pdev->dev, dma_addr)) {
    dev_err(&pdev->dev, "DMA mapping failed\n");
    return -EIO;
}
// 将 dma_addr 写入 DMA 描述符
// ...
dma_unmap_single(&pdev->dev, dma_addr, skb->len, DMA_TO_DEVICE);
方向常量 含义 缓存操作
DMA_TO_DEVICE CPU 写入,设备读取(如网络发送) 映射时刷新 CPU 脏缓存行到内存
DMA_FROM_DEVICE 设备写入,CPU 读取(如网络接收) 映射时无效化 CPU 缓存行,避免读到旧数据
DMA_BIDIRECTIONAL 双向传输 映射时同时刷新和无效化
DMA_NONE 调试用途 无操作
常见错误: 映射后,CPU 不应再直接访问原始虚拟地址,除非先调用 dma_sync_single_for_cpu。否则可能导致缓存一致性问题,产生数据损坏。

4.4 dma_map_sg:流式聚合散列表映射

dma_map_sg 用于将多个不连续的物理内存片段(scatterlist)映射为设备可访问的 DMA 地址列表。它返回映射成功的片段数,并可能合并相邻物理页以优化传输。

// 原型
int dma_map_sg(struct device *dev, struct scatterlist *sg,
               int nents, enum dma_data_direction dir);

// 示例:映射一个包含 3 个片段的 SG 列表
struct scatterlist sg[3];
int i, mapped;

sg_init_table(sg, 3);
sg_set_buf(&sg[0], buf1, len1);
sg_set_buf(&sg[1], buf2, len2);
sg_set_buf(&sg[2], buf3, len3);

mapped = dma_map_sg(&pdev->dev, sg, 3, DMA_BIDIRECTIONAL);
if (mapped <= 0) {
    dev_err(&pdev->dev, "SG mapping failed\n");
    return -EIO;
}

// 遍历映射后的 SG 列表
for_each_sg(sg, s, mapped, i) {
    dma_addr_t addr = sg_dma_address(s);
    unsigned int len = sg_dma_len(s);
    // 将 addr 和 len 编程到 DMA 引擎
}
// ...
dma_unmap_sg(&pdev->dev, sg, mapped, DMA_BIDIRECTIONAL);
dma_map_sg 散列表映射流程 输入 SG 列表 (3 个片段) 片段 0: buf1, len1 片段 1: buf2, len2 片段 2: buf3, len3 DMA 映射层 合并相邻物理页 缓存刷新/无效化 IOMMU 地址转换 输出 DMA 地址列表 DMA 地址 0, 长度 len1 DMA 地址 1, 长度 len2+len3 关键行为 dma_map_sg 可能合并物理上连续的片段,返回的 mapped 值 ≤ nents;使用 sg_dma_address/sg_dma_len 访问映射后的地址

4.5 核心 API 对比与选择

特性 dma_alloc_coherent dma_map_single dma_map_sg
内存所有权 分配并拥有 仅映射,不分配 仅映射,不分配
物理连续性 必须连续 取决于原始缓冲区 不要求连续
缓存一致性 硬件保证 需显式同步 需显式同步
分配开销 高(页分配 + 页表修改) 低(仅映射) 中(遍历 + 合并)
典型用途 描述符环、控制结构 单缓冲区 I/O 多片段 I/O(网络、存储)
性能要点:
  • 一致性映射在分配时开销大,但后续访问无同步开销,适合长期持有的缓冲区。
  • 流式映射在每次传输前后需要同步,但分配灵活,适合高频、短生命周期的 I/O。
  • 对于支持 IOMMU 的系统,dma_map_sg 可以避免物理连续性的限制,简化驱动设计。

4.6 错误处理与调试

所有 DMA 映射 API 都可能失败,必须检查返回值:

// 检查 dma_alloc_coherent 返回值
if (!cpu_addr) {
    dev_err(dev, "Coherent allocation failed\n");
    return -ENOMEM;
}

// 检查 dma_map_single 返回值
dma_addr_t addr = dma_map_single(dev, buf, size, dir);
if (dma_mapping_error(dev, addr)) {
    dev_err(dev, "Single mapping failed\n");
    return -EIO;
}

// 检查 dma_map_sg 返回值
int mapped = dma_map_sg(dev, sg, nents, dir);
if (mapped <= 0) {
    dev_err(dev, "SG mapping failed\n");
    return -EIO;
}
调试技巧: 开启内核配置 CONFIG_DMA_API_DEBUG 可以检测常见的 DMA API 误用,如未匹配的 map/unmap、错误的方向参数、双重释放等。在开发阶段建议始终开启。