3、Linux DMA API详解:dma_alloc_coherent、dma_map_single、dma_pool的使用

在PCIe驱动开发中,DMA(Direct Memory Access)是提升数据传输性能的核心机制。Linux内核提供了一套完善的DMA API,用于管理设备与内存之间的数据搬运。本章将深入剖析三个最常用的DMA接口:dma_alloc_coherent、dma_map_single 和 dma_pool,帮助你在驱动中正确选择和使用它们。

核心原则:DMA API的设计目标是让驱动程序无需关心底层硬件(如IOMMU、总线拓扑)的细节,通过统一的接口完成内存分配、地址映射和同步操作。

3.1 一致性DMA映射:dma_alloc_coherent

dma_alloc_coherent 用于分配一段 一致性(Coherent) 的DMA缓冲区。所谓“一致性”,是指CPU和设备在任何时刻观察到的内存数据都是一致的,无需额外的缓存刷新操作。该函数返回两个地址: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的设备指针,通常来自PCIe设备的 struct pci_dev * 的 &pdev->dev
size 分配的字节数,必须是页大小的整数倍(通常为4KB对齐)
dma_handle 输出参数,返回DMA总线地址,供设备DMA引擎使用
flag 内存分配标志,常用 GFP_KERNEL(可睡眠)或 GFP_ATOMIC(原子上下文)
返回值 CPU虚拟地址(内核空间),失败返回 NULL

典型使用场景

  • 控制结构体:如描述符环(Descriptor Ring)、命令队列控制块。
  • 小批量、频繁访问的数据:设备与CPU需要共享状态标志的场景。
  • 无需手动同步:硬件保证缓存一致性,适合对延迟敏感的控制路径。

释放接口

void dma_free_coherent(struct device *dev, size_t size,
                       void *cpu_addr, dma_addr_t dma_handle);
注意:一致性映射的代价较高,因为内核可能需要禁用缓存或使用特殊内存区域。对于大块数据流(如网络包、存储块),应优先考虑流式DMA映射。
一致性DMA映射架构 (dma_alloc_coherent) CPU 直接访问虚拟地址 一致性DMA缓冲区 CPU & 设备共享视图 PCIe设备 DMA引擎访问总线地址 虚拟地址 总线地址 关键特性 • 无需调用 dma_sync_single_for_cpu/device 进行缓存同步 • 适用于控制路径,不推荐用于大数据块传输

3.2 流式DMA映射:dma_map_single

流式映射(Streaming Mapping) 是处理大数据块传输的首选方案。它不保证缓存一致性,但通过显式的同步操作(dma_sync_single_for_cpu / dma_sync_single_for_device)来管理数据一致性,从而获得更高的性能和灵活性。

函数原型与参数

dma_addr_t dma_map_single(struct device *dev, void *cpu_addr,
                          size_t size, enum dma_data_direction dir);
参数 说明
dev 设备指针
cpu_addr 要映射的CPU虚拟地址(必须来自 kmalloc 或页分配器)
size 映射的字节数
dir 数据传输方向(见下表)
返回值 DMA总线地址,失败返回 0(需用 dma_mapping_error 检查)

数据传输方向枚举

方向常量 含义 典型场景
DMA_TO_DEVICE CPU → 设备(内存写,设备读) 发送网络包、提交命令描述符
DMA_FROM_DEVICE 设备 → CPU(设备写,内存读) 接收网络包、读取数据缓冲区
DMA_BIDIRECTIONAL 双向传输 共享状态区(不推荐,性能较差)
DMA_NONE 调试用途 仅用于校验

同步与解映射

// 同步:在CPU访问前调用
void dma_sync_single_for_cpu(struct device *dev, dma_addr_t dma_handle,
                             size_t size, enum dma_data_direction dir);

// 同步:在设备访问前调用
void dma_sync_single_for_device(struct device *dev, dma_addr_t dma_handle,
                                size_t size, enum dma_data_direction dir);

// 解映射:传输完成后释放
void dma_unmap_single(struct device *dev, dma_addr_t dma_handle,
                      size_t size, enum dma_data_direction dir);
性能要点:流式映射的同步开销远小于一致性映射的全局缓存操作。对于大数据块(如4KB以上),流式映射的吞吐量通常比一致性映射高30%~50%。
流式DMA映射生命周期 (dma_map_single) 1. 分配内存 kmalloc / 页分配器 2. dma_map_single 获取总线地址 3. DMA传输 设备读写内存 4. 解映射 dma_unmap 同步操作(可选循环) • CPU读取前:dma_sync_single_for_cpu → 使缓存失效,获取最新数据 • 设备读取前:dma_sync_single_for_device → 刷出缓存,确保设备可见 错误检查:dma_mapping_error(dev, dma_addr) 映射失败时返回非零值,必须检查!

3.3 DMA池:dma_pool

dma_pool 是内核提供的一种 固定大小缓冲区预分配机制,专门用于管理大量相同尺寸的DMA缓冲区。它底层基于 dma_alloc_coherent,但通过内存池化技术减少了频繁分配/释放的开销,并避免了内存碎片。

核心API

// 创建DMA池
struct dma_pool *dma_pool_create(const char *name, struct device *dev,
                                 size_t size, size_t align,
                                 size_t boundary);

// 从池中分配一个缓冲区
void *dma_pool_alloc(struct dma_pool *pool, gfp_t mem_flags,
                     dma_addr_t *handle);

// 释放缓冲区回池
void dma_pool_free(struct dma_pool *pool, void *vaddr,
                   dma_addr_t dma);

// 销毁DMA池
void dma_pool_destroy(struct dma_pool *pool);
参数 说明
name 调试用名称,会出现在 /proc/slabinfo 中
size 每个缓冲区的大小(字节)
align 对齐要求(如16、32、64),必须是2的幂
boundary 边界限制(如 PAGE_SIZE),防止跨页;0表示不限制

典型应用场景

  • 网络驱动:管理固定大小的SKB数据缓冲区(如1500字节MTU)。
  • 存储驱动:管理SCSI命令描述块(CDB)或NVMe队列条目。
  • USB/视频驱动:管理等时传输的固定大小数据包。
选择建议:当驱动需要频繁分配/释放相同大小的DMA缓冲区(如每秒钟数千次),且缓冲区大小在64字节到4KB之间时,dma_pool 是性能最优的选择。它比直接调用 dma_alloc_coherent 快2~5倍。
DMA池 (dma_pool) 工作原理 dma_pool_create 预分配大块一致性内存 内部切分为固定大小槽 槽1 槽2 槽3 槽4 分配 释放 PCIe设备 直接使用槽位总线地址 性能优势 • 分配/释放无锁或自旋锁,延迟极低 • 避免外部碎片,内存利用率高

3.4 三种DMA API对比与选型

特性 dma_alloc_coherent dma_map_single dma_pool
缓存一致性 硬件保证 需手动同步 硬件保证(基于一致性内存)
分配粒度 页对齐,最小4KB 任意大小(需对齐) 固定大小(创建时指定)
分配/释放开销 高(涉及页分配器) 中(涉及TLB/缓存操作) 低(池化预分配)
适用数据大小 小(控制结构) 大(数据流) 中(固定大小频繁分配)
典型延迟 10~100 µs 1~10 µs(不含同步) <1 µs
推荐场景 描述符环、状态寄存器 网络包、存储数据块 命令槽、USB请求块
实战口诀:控制结构用一致(coherent),数据流用流式(streaming),频繁分配用池化(pool)。三者可以混合使用,例如描述符环用 dma_alloc_coherent,数据缓冲区用 dma_map_single,命令槽用 dma_pool。

3.5 完整使用示例:PCIe驱动DMA初始化

以下代码展示了一个典型的PCIe驱动中,如何组合使用三种DMA API进行初始化:

struct my_pcie_dev {
    struct pci_dev *pdev;
    // 一致性映射:描述符环
    struct desc_ring *ring_cpu;
    dma_addr_t ring_dma;
    // 流式映射:数据缓冲区
    void *data_buf;
    dma_addr_t data_dma;
    // DMA池:命令槽
    struct dma_pool *cmd_pool;
};

static int my_pcie_dma_init(struct my_pcie_dev *dev)
{
    struct device *d = &dev->pdev->dev;

    // 1. 分配一致性内存(描述符环,4KB对齐)
    dev->ring_cpu = dma_alloc_coherent(d, PAGE_SIZE,
                                       &dev->ring_dma, GFP_KERNEL);
    if (!dev->ring_cpu)
        return -ENOMEM;

    // 2. 分配数据缓冲区,并建立流式映射
    dev->data_buf = kmalloc(64 * 1024, GFP_KERNEL);
    if (!dev->data_buf) {
        dma_free_coherent(d, PAGE_SIZE, dev->ring_cpu, dev->ring_dma);
        return -ENOMEM;
    }
    dev->data_dma = dma_map_single(d, dev->data_buf,
                                   64 * 1024, DMA_BIDIRECTIONAL);
    if (dma_mapping_error(d, dev->data_dma)) {
        kfree(dev->data_buf);
        dma_free_coherent(d, PAGE_SIZE, dev->ring_cpu, dev->ring_dma);
        return -ENOMEM;
    }

    // 3. 创建DMA池(每个命令槽256字节,32字节对齐)
    dev->cmd_pool = dma_pool_create("my_cmd_pool", d,
                                    256, 32, 0);
    if (!dev->cmd_pool) {
        dma_unmap_single(d, dev->data_dma, 64 * 1024, DMA_BIDIRECTIONAL);
        kfree(dev->data_buf);
        dma_free_coherent(d, PAGE_SIZE, dev->ring_cpu, dev->ring_dma);
        return -ENOMEM;
    }

    return 0;
}

static void my_pcie_dma_exit(struct my_pcie_dev *dev)
{
    struct device *d = &dev->pdev->dev;

    dma_pool_destroy(dev->cmd_pool);
    dma_unmap_single(d, dev->data_dma, 64 * 1024, DMA_BIDIRECTIONAL);
    kfree(dev->data_buf);
    dma_free_coherent(d, PAGE_SIZE, dev->ring_cpu, dev->ring_dma);
}
常见错误:
  • 忘记调用 dma_mapping_error 检查映射是否成功。
  • 在 dma_unmap_single 之后继续访问数据缓冲区。
  • 流式映射方向设置错误(如将 DMA_FROM_DEVICE 误用为 DMA_TO_DEVICE),导致数据损坏。
  • 在中断上下文中使用 GFP_KERNEL 标志(应使用 GFP_ATOMIC)。