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映射。
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%。
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倍。
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)。