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 的适用场景、性能特征与底层行为。
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 |
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_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 传输生命周期
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或预分配。