2. Linux DMA API 概述:核心接口与使用场景

Linux 内核提供了一套标准化的 DMA(Direct Memory Access)API,用于管理设备与内存之间的数据搬运。这些 API 屏蔽了不同架构(ARM、x86、RISC-V 等)的硬件差异,为驱动开发者提供统一的缓冲分配、映射和同步接口。本章聚焦三个最核心的 API:dma_alloc_coherent、dma_map_single 和 dma_pool,并介绍它们的适用场景、性能特征和典型用法。

核心原则:DMA API 的设计目标是保证 CPU 和设备看到的内存视图一致(一致性)或通过显式同步维护一致性(流式)。选择哪个 API,取决于数据传输的频率、大小和对延迟的敏感度。

2.1 DMA 地址空间与 API 分类

在深入具体 API 之前,需要理解两个关键地址:CPU 虚拟地址(驱动通过指针访问)和 DMA 总线地址(设备通过 DMA 引擎访问)。Linux DMA API 负责在这两个地址空间之间建立映射,并处理缓存一致性。

DMA 地址空间映射关系 CPU 虚拟地址 (void *) 驱动读写 DMA 总线地址 (dma_addr_t) 设备 DMA 引擎 DMA 映射层 IOMMU / SMMU 关键概念 • CPU 通过虚拟地址访问内存,设备通过 DMA 总线地址访问内存。 • DMA API 负责建立映射并维护缓存一致性,驱动无需关心底层硬件细节。

根据缓存一致性的处理方式,DMA API 分为两大类:

类别 代表 API 一致性保证 典型场景
一致性 DMA dma_alloc_coherent 硬件保证 CPU 和设备始终看到相同数据 描述符环、控制寄存器、小数据量控制结构
流式 DMA dma_map_single / dma_map_sg 通过显式 dma_sync_* 维护一致性 网络数据包、块设备 I/O、大块数据传输
DMA 池 dma_pool 基于一致性 DMA,预分配固定大小缓冲 USB 传输描述符、网络小包、固定大小控制块

2.2 dma_alloc_coherent:一致性 DMA 缓冲

dma_alloc_coherent 分配一段物理连续的内存,并同时返回 CPU 虚拟地址和 DMA 总线地址。硬件保证这段内存的缓存一致性——CPU 和 DMA 设备可以同时访问而不需要软件同步。

函数原型与参数

#include <linux/dma-mapping.h>

void *dma_alloc_coherent(struct device *dev, size_t size,
                         dma_addr_t *dma_handle, gfp_t flag);
参数 说明
dev 请求 DMA 的设备指针,用于关联 IOMMU 域
size 分配的字节数,会被向上对齐到页大小
dma_handle 输出参数,返回 DMA 总线地址
flag GFP 标志(如 GFP_KERNEL、GFP_ATOMIC)
返回值 CPU 虚拟地址(内核空间),失败返回 NULL

典型用法示例

struct device *dev = &pdev->dev;
dma_addr_t dma_handle;
void *cpu_addr;
size_t buf_size = 4096;  // 4KB

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

// 驱动可以直接读写 cpu_addr,设备可以通过 dma_handle 访问
memset(cpu_addr, 0, buf_size);
writel(dma_handle, reg_base + DMA_ADDR_REG);

// 使用完毕后释放
dma_free_coherent(dev, buf_size, cpu_addr, dma_handle);
性能注意:一致性 DMA 缓冲在每次 CPU 访问时可能触发缓存行刷新或失效,导致写操作延迟较高。对于高频小数据量(如描述符更新)影响不大,但对于大数据块传输,建议使用流式 DMA 以获得更好的性能。

2.3 dma_map_single:流式 DMA 映射

dma_map_single 将一段已有的内核缓冲区(通常来自 kmalloc 或 __get_free_pages)映射为 DMA 可访问的总线地址。它不分配新内存,只建立映射并处理缓存同步。流式映射要求驱动在 CPU 和设备访问之间显式调用同步函数。

函数原型与方向

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

dma_data_direction 枚举定义了数据传输方向,影响缓存同步策略:

方向 值 含义 缓存操作
DMA_TO_DEVICE 1 CPU 写入,设备读取 映射时刷出缓存行
DMA_FROM_DEVICE 2 设备写入,CPU 读取 映射时无效化缓存行
DMA_BIDIRECTIONAL 3 双向传输 映射时刷出并无效化
DMA_NONE 0 调试用途 无操作

流式 DMA 的完整生命周期

  1. 分配缓冲区:使用 kmalloc 或 alloc_pages 分配内存。
  2. 映射:调用 dma_map_single 获取 DMA 地址。
  3. 提交给设备:将 DMA 地址写入设备寄存器,启动传输。
  4. 同步(可选):在 CPU 访问前调用 dma_sync_single_for_cpu,在设备访问前调用 dma_sync_single_for_device。
  5. 解除映射:传输完成后调用 dma_unmap_single。
// 分配缓冲区
void *buf = kmalloc(buf_size, GFP_KERNEL);
if (!buf) return -ENOMEM;

// 映射为 DMA 地址
dma_addr_t dma_addr = dma_map_single(dev, buf, buf_size, DMA_TO_DEVICE);
if (dma_mapping_error(dev, dma_addr)) {
    kfree(buf);
    return -EIO;
}

// 启动 DMA 传输(伪代码)
writel(dma_addr, reg_base + DMA_SRC);
writel(buf_size, reg_base + DMA_LEN);
writel(1, reg_base + DMA_START);

// 等待传输完成...

// 解除映射
dma_unmap_single(dev, dma_addr, buf_size, DMA_TO_DEVICE);
kfree(buf);
最佳实践:流式 DMA 映射的开销主要在缓存维护操作上。对于大数据块(如网络 MTU 1500 字节以上),流式 DMA 的性能优于一致性 DMA,因为缓存行刷新的次数更少且更集中。

2.4 dma_pool:DMA 缓冲池

dma_pool 用于管理固定大小的一致性 DMA 缓冲。它预分配一大块一致性内存,然后从中切分出多个固定大小的对象,避免频繁调用 dma_alloc_coherent 带来的开销和碎片。

核心 API 函数

函数 作用
dma_pool_create 创建 DMA 缓冲池,指定对象大小、对齐和边界
dma_pool_alloc 从池中分配一个对象,返回 CPU 地址和 DMA 地址
dma_pool_free 将对象归还给池
dma_pool_destroy 销毁池,释放所有预分配内存

典型用法:USB 传输描述符管理

struct dma_pool *td_pool;
struct td_descriptor {
    __le32 flags;
    __le32 buf_addr;
    __le32 length;
    // ... 其他字段
};

// 创建池:每个对象 64 字节,32 字节对齐,无边界限制
td_pool = dma_pool_create("usb_td", dev, sizeof(struct td_descriptor),
                          32, 0);
if (!td_pool) {
    dev_err(dev, "Failed to create DMA pool\n");
    return -ENOMEM;
}

// 分配一个描述符
struct td_descriptor *td;
dma_addr_t td_dma;
td = dma_pool_alloc(td_pool, GFP_ATOMIC, &td_dma);
if (!td) {
    dev_err(dev, "Failed to allocate TD from pool\n");
    return -ENOMEM;
}

// 使用 td 和 td_dma...

// 释放回池
dma_pool_free(td_pool, td, td_dma);

// 驱动卸载时销毁池
dma_pool_destroy(td_pool);
DMA API 选择决策流程 DMA 传输需求 缓冲区大小是否固定且频繁分配? dma_pool 是否需要缓存一致性? dma_alloc_coherent dma_map_single 选择建议 • 控制结构/描述符 → dma_pool 或 dma_alloc_coherent • 大数据块传输 → dma_map_single + 流式同步

2.5 三种 API 的性能对比

下表从分配开销、缓存一致性和适用场景三个维度对比三种核心 DMA API:

特性 dma_alloc_coherent dma_map_single dma_pool
分配开销 高(物理连续、页对齐) 低(仅建立映射) 极低(预分配池)
缓存一致性 硬件保证 软件显式同步 硬件保证(基于一致性)
CPU 访问延迟 较高(每次访问可能触发缓存操作) 低(同步后直接访问) 较高(同左)
内存碎片 高(大块连续) 无(使用已有内存) 低(池化管理)
典型大小 4KB ~ 2MB 任意大小 32B ~ 4KB
推荐场景 描述符环、控制寄存器 网络包、文件系统 I/O USB TD、网络小包池

2.6 错误处理与调试

使用 DMA API 时,必须检查返回值并处理错误:

  • dma_alloc_coherent:返回 NULL 表示分配失败,常见原因是内存不足或 GFP 标志不匹配。
  • dma_map_single:使用 dma_mapping_error 检查返回的 DMA 地址是否有效。
  • dma_pool_alloc:返回 NULL 表示池中无可用对象,可考虑增大池容量或使用 GFP_ATOMIC。
// 检查 dma_map_single 是否成功
dma_addr_t dma_addr = dma_map_single(dev, buf, size, dir);
if (dma_mapping_error(dev, dma_addr)) {
    dev_err(dev, "DMA mapping failed\n");
    // 清理资源
    kfree(buf);
    return -EIO;
}
调试技巧:开启内核配置 CONFIG_DMA_API_DEBUG 可以检测常见的 DMA API 误用,如映射后未解映射、使用错误的同步方向等。在开发阶段建议始终开启此选项。

2.7 本章小结

Linux DMA API 为驱动开发者提供了三种核心缓冲管理方式:

  • dma_alloc_coherent:适用于小尺寸、高频访问的控制结构,硬件保证一致性但 CPU 访问延迟较高。
  • dma_map_single:适用于大块数据传输,通过显式同步获得最佳性能,是网络和存储驱动的首选。
  • dma_pool:适用于固定大小对象的频繁分配和释放,在 USB、网络子系统中广泛使用。