4. Linux 内核 DMA API:一致性 DMA 与流式 DMA

在 PCIe RC 驱动开发中,DMA 传输性能的优劣直接取决于开发者对 Linux 内核 DMA API 的理解深度与使用方式。本章将系统讲解两类核心 DMA 映射模型——一致性 DMA(Coherent DMA)与流式 DMA(Streaming DMA),并深入剖析 dma_alloc_coherent、dma_map_single、dma_map_sg 等关键 API 的适用场景、性能特征与底层行为。

核心认知:一致性 DMA 适合长期、小批量、对延迟敏感的控制路径;流式 DMA 适合大批量、高性能、对吞吐量敏感的数据路径。选错模型将导致性能瓶颈或数据一致性问题。

4.1 一致性 DMA 与流式 DMA 的概念对比

Linux 内核将 DMA 映射分为两种模型,其根本区别在于 CPU 与 DMA 控制器对内存的访问一致性保证 以及 Cache 协同策略。

对比维度 一致性 DMA(Coherent DMA) 流式 DMA(Streaming DMA)
内存属性 非 Cache 或硬件保证 Cache 一致性 通常为 Cacheable,需显式同步
映射开销 低(一次性分配,长期使用) 高(每次传输前后需 map/unmap)
适用场景 描述符环、控制寄存器、状态标志 大数据块、网络包、存储 I/O
API 函数 dma_alloc_coherent / dma_free_coherent dma_map_single / dma_unmap_single / dma_map_sg
Cache 同步 无需手动处理 需要 dma_sync_single_for_cpu/device
传输粒度 固定大小,通常较小(4KB ~ 64KB) 灵活,可大至数 MB
DMA 映射模型对比:一致性 vs 流式 一致性 DMA dma_alloc_coherent() CPU 与设备直接共享内存 无 Cache 一致性问题 适合:描述符环 / 控制状态 流式 DMA dma_map_single() / dma_map_sg() 每次传输前 map,传输后 unmap 需显式 Cache 同步 适合:大数据块 / 网络 / 存储

4.2 dma_alloc_coherent:一致性 DMA 分配

dma_alloc_coherent 是 Linux 内核为一致性 DMA 模型提供的最核心 API。它分配一段物理连续的内存,并同时返回 CPU 虚拟地址 和 DMA 总线地址,且保证 CPU 与设备对该内存的访问无需额外的 Cache 同步操作。

4.2.1 函数原型与参数

#include <linux/dma-mapping.h>

void *dma_alloc_coherent(struct device *dev, size_t size,
                         dma_addr_t *dma_handle, gfp_t flag);
参数 说明
dev PCIe 设备指针,通常来自 pci_dev->dev
size 分配大小(字节),建议按 PAGE_SIZE 对齐
dma_handle 输出参数,返回 DMA 总线地址(设备侧使用)
flag GFP 标志,常用 GFP_KERNEL 或 GFP_ATOMIC
返回值 CPU 虚拟地址(内核线性映射区),失败返回 NULL

4.2.2 典型使用场景:描述符环分配

在 PCIe RC 驱动中,一致性 DMA 最常见的用途是分配 TX/RX 描述符环。描述符环被设备硬件直接读取,CPU 频繁更新其状态,使用一致性 DMA 可避免每次访问都执行 Cache 刷新。

struct pcie_priv {
    struct pci_dev *pdev;
    void *desc_ring_cpu;      /* CPU 虚拟地址 */
    dma_addr_t desc_ring_dma; /* DMA 总线地址 */
    int desc_ring_size;
};

static int alloc_desc_ring(struct pcie_priv *priv)
{
    priv->desc_ring_size = 256 * sizeof(struct desc_entry);
    priv->desc_ring_cpu = dma_alloc_coherent(&priv->pdev->dev,
                                              priv->desc_ring_size,
                                              &priv->desc_ring_dma,
                                              GFP_KERNEL);
    if (!priv->desc_ring_cpu)
        return -ENOMEM;

    /* 将 DMA 地址写入 BAR 寄存器,通知设备 */
    writel(priv->desc_ring_dma, priv->bar0 + RING_BASE_ADDR);
    return 0;
}

static void free_desc_ring(struct pcie_priv *priv)
{
    dma_free_coherent(&priv->pdev->dev, priv->desc_ring_size,
                      priv->desc_ring_cpu, priv->desc_ring_dma);
}
性能陷阱:一致性 DMA 分配的内存通常来自 原子池(Atomic Pool),分配大块内存(超过 2MB)容易失败。对于大数据传输,应使用流式 DMA 配合 dma_map_sg 实现分散聚合。

4.3 dma_map_single:流式 DMA 单缓冲区映射

dma_map_single 用于将一段 已有的内核缓冲区(如 kmalloc 或 __get_free_pages 分配的内存)映射为 DMA 可访问的总线地址。映射过程中,内核会根据设备 DMA 掩码执行地址转换,并可能执行 Cache 刷新操作。

4.3.1 函数原型与方向

dma_addr_t dma_map_single(struct device *dev, void *cpu_addr,
                          size_t size, enum dma_data_direction dir);
方向枚举 含义 Cache 同步行为
DMA_TO_DEVICE CPU 写 → 设备读(如 TX 路径) 将 Cache 中脏数据写回内存
DMA_FROM_DEVICE 设备写 → CPU 读(如 RX 路径) 使 Cache 行失效,避免读到旧数据
DMA_BIDIRECTIONAL 双向传输 同时执行写回和失效(开销最大)
DMA_NONE 调试用途 无操作

4.3.2 流式 DMA 传输生命周期

流式 DMA 单缓冲区传输生命周期 1. 分配缓冲区 2. dma_map_single 3. 提交 DMA 4. dma_unmap 关键约束: • map 之后、unmap 之前,CPU 不得访问缓冲区(除非调用 dma_sync_*) • unmap 之后,DMA 总线地址失效,设备不能再访问该地址 • 方向必须与实际传输方向一致,否则导致数据损坏 • 对于 DMA_FROM_DEVICE,unmap 后 CPU 读取的是最新设备数据

4.4 dma_map_sg:分散聚合 DMA(Scatter-Gather)

对于高性能 PCIe 设备,数据往往分布在多个 非连续物理内存页 中。dma_map_sg 允许将一组 struct scatterlist 条目一次性映射为设备可访问的 DMA 地址列表,极大减少中断次数和 CPU 开销。

4.4.1 核心数据结构

struct scatterlist {
    unsigned long   page_link;  /* 指向物理页 */
    unsigned int    offset;     /* 页内偏移 */
    dma_addr_t      dma_address; /* 映射后的 DMA 地址(由 dma_map_sg 填充) */
    unsigned int    length;     /* 该段长度 */
};

4.4.2 典型使用流程

/* 1. 初始化 scatterlist 数组 */
struct scatterlist sg[NR_SG];
int i, nents;

sg_init_table(sg, NR_SG);
for (i = 0; i < NR_SG; i++) {
    /* 假设每个 sg 条目指向一个独立的物理页 */
    sg_set_page(&sg[i], alloc_page(GFP_KERNEL),
                PAGE_SIZE, 0);
}

/* 2. 映射所有 sg 条目为 DMA 地址 */
nents = dma_map_sg(dev, sg, NR_SG, DMA_TO_DEVICE);

/* 3. 遍历映射后的 sg 条目,构建硬件描述符 */
for (i = 0; i < nents; i++) {
    dma_addr_t dma_addr = sg_dma_address(&sg[i]);
    unsigned int len = sg_dma_len(&sg[i]);
    /* 将 dma_addr 和 len 写入硬件描述符环 */
}

/* 4. 通知设备开始 DMA 传输 */
writel(1, priv->bar0 + DOORBELL);

/* 5. 传输完成后解除映射 */
dma_unmap_sg(dev, sg, NR_SG, DMA_TO_DEVICE);

/* 6. 释放页面 */
for (i = 0; i < NR_SG; i++)
    __free_page(sg_page(&sg[i]));
性能优势:使用 dma_map_sg 后,硬件可以自动从一个描述符跳到下一个描述符,无需 CPU 干预。对于 10Gbps 以上的 PCIe 网卡,SG 模式可将 CPU 利用率降低 30%~50%。

4.5 性能调优对比:一致性 vs 流式 DMA

以下表格从性能调优角度对比两种模型在实际 PCIe RC 驱动中的表现差异:

调优维度 一致性 DMA 流式 DMA
延迟 低(无 Cache 同步开销) 中(map/unmap 引入额外延迟)
吞吐量 低(受限于分配大小和连续性) 高(支持大块和 SG 模式)
CPU 占用 低(分配后零维护) 中高(每次传输需 map/unmap)
内存碎片 高(要求物理连续) 低(支持分散物理页)
最佳实践 描述符环、控制寄存器 数据缓冲区、网络包、存储 I/O

4.6 常见错误与避坑指南

  • 错误 1:在 dma_map_single 之后、dma_unmap_single 之前访问缓冲区。这会导致 CPU 读到过时数据或写入的数据未被设备看到。
  • 错误 2:使用 DMA_BIDIRECTIONAL 代替单向方向。双向映射会强制同时执行 Cache 写回和失效,增加 2~3 倍开销。
  • 错误 3:未检查 dma_map_sg 返回的 nents。硬件可能合并相邻的 sg 条目,实际条目数可能少于传入数。
  • 错误 4:在中断上下文中使用 GFP_KERNEL 调用 dma_alloc_coherent。应使用 GFP_ATOMIC 或预分配。
调优建议:对于高性能 PCIe RC 驱动,建议采用混合策略——使用一致性 DMA 分配描述符环,使用流式 DMA + SG 模式处理数据缓冲区。这种组合既能保证控制路径的低延迟,又能实现数据路径的高吞吐。

4.7 本章小结