2. Linux DMA API 概览:核心接口与使用场景
在 Linux 设备驱动中,DMA(Direct Memory Access)是提升外设与内存间数据吞吐量的关键机制。Linux 内核提供了一套标准化的 DMA API,屏蔽了不同架构(x86、ARM、RISC-V 等)的硬件差异,使驱动开发者能够以统一的方式分配、映射和管理 DMA 缓冲区。本章将深入剖析三个最核心的 DMA API:dma_alloc_coherent、dma_map_single 和 dma_pool,并对比它们的适用场景与性能特征。
struct device * 指针,该指针通常来自 platform_device 或 pci_dev。所有 DMA 操作都通过这个设备对象与内核 DMA 子系统交互。
2.1 DMA API 全景图:从分配到同步
下图展示了 Linux DMA API 的完整工作流,从缓冲区分配、地址映射、传输控制到同步与释放。本章重点介绍的三个 API 分别对应图中“一致性分配”、“流式映射”和“池化分配”三个关键路径。
2.2 dma_alloc_coherent:一致性 DMA 缓冲区
dma_alloc_coherent 是 Linux 内核中最常用的 DMA 缓冲区分配接口。它分配一段物理连续的内存,并同时返回 CPU 虚拟地址和 DMA 总线地址。最关键的特性是:硬件(设备)和软件(CPU)访问这段内存时,不需要任何额外的缓存同步操作——内核通过页表属性或硬件机制保证了缓存一致性。
函数原型与参数
#include <linux/dma-mapping.h>
void *dma_alloc_coherent(struct device *dev, size_t size,
dma_addr_t *dma_handle, gfp_t flag);
| 参数 | 类型 | 说明 |
|---|---|---|
dev |
struct device * |
请求 DMA 操作的设备对象,用于关联 IOMMU/DMA 域 |
size |
size_t |
请求分配的字节数,必须为页大小(PAGE_SIZE)的整数倍 |
dma_handle |
dma_addr_t * |
输出参数,返回设备可用的 DMA 总线地址 |
flag |
gfp_t |
内存分配标志,常用 GFP_KERNEL(可睡眠)或 GFP_ATOMIC(原子上下文) |
典型使用场景
- 控制/状态寄存器块: 网卡描述符环、USB 传输描述符、存储控制器命令队列。
- 长期驻留缓冲区: 音频 DMA 环形缓冲区、视频帧缓冲、加密引擎上下文。
- 对延迟敏感的小型传输: 每次传输数据量小但频率高,避免同步开销。
2.3 dma_map_single:流式 DMA 映射
与一致性分配不同,dma_map_single 并不分配新内存,而是将驱动已有的内核缓冲区(通常由 kmalloc 或 __get_free_pages 分配)映射到 DMA 总线地址空间。映射过程中,内核会执行必要的缓存维护操作(如刷出或失效缓存行),以保证设备访问时数据的一致性。
函数原型与配对接口
dma_addr_t dma_map_single(struct device *dev, void *cpu_addr,
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);
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);
数据传输方向枚举
| 方向常量 | 含义 | 缓存操作 |
|---|---|---|
DMA_TO_DEVICE |
CPU → 设备(如发送网络包) | 映射时刷出(flush)缓存,确保设备看到最新数据 |
DMA_FROM_DEVICE |
设备 → CPU(如接收网络包) | 映射时失效(invalidate)缓存,解除后 CPU 读取前需再次失效 |
DMA_BIDIRECTIONAL |
双向传输 | 映射时刷出并失效,同步开销最大 |
DMA_NONE |
调试用途 | 无操作 |
流式映射的典型生命周期
- 分配缓冲区: 使用
kmalloc或netdev_alloc_skb分配数据缓冲区。 - 映射: 调用
dma_map_single获取 DMA 地址,检查返回值是否为dma_mapping_error。 - 提交传输: 将 DMA 地址写入设备描述符,触发设备 DMA 操作。
- 同步(可选): 在 CPU 访问数据前调用
dma_sync_single_for_cpu,在设备访问前调用dma_sync_single_for_device。 - 解映射: 传输完成后调用
dma_unmap_single释放映射关系。
dma_map_single 之后、dma_unmap_single 之前直接读写缓冲区内容,会导致缓存一致性问题,产生数据损坏。这是 DMA 驱动调试中最难排查的 bug 之一。
2.4 dma_pool:固定大小 DMA 缓冲区池
当驱动需要频繁分配和释放大量相同大小的 DMA 缓冲区(如网络 MTU 大小的包缓冲区、USB 传输块)时,直接使用 dma_alloc_coherent 会产生较高的内存碎片和分配延迟。dma_pool 提供了一个预分配、固定大小的缓冲区池,分配和释放操作在池内完成,速度极快且不会产生碎片。
核心 API 函数
struct dma_pool *dma_pool_create(const char *name, struct device *dev,
size_t size, size_t align,
size_t allocation);
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);
void dma_pool_destroy(struct dma_pool *pool);
参数详解
| 参数 | 说明 |
|---|---|
name |
调试用名称,会出现在 /proc/slabinfo 中 |
size |
每个缓冲区的大小(字节),池内所有缓冲区大小相同 |
align |
对齐要求(字节),通常设为 dma_get_cache_alignment 的返回值 |
allocation |
底层分配边界,如果不需要特殊边界,设为 0 或 size |
dma_pool 内部结构
下图展示了 dma_pool 的内部组织方式:池由多个“块(block)”组成,每个块是一次 dma_alloc_coherent 分配的大页内存;块内部被切分为固定大小的槽位(slot),通过位图或空闲链表管理。
2.5 三大 API 对比与选型指南
选择合适的 DMA API 是驱动性能优化的第一步。下表从分配粒度、缓存一致性、分配/释放开销、适用场景四个维度进行对比:
| 特性 | dma_alloc_coherent | dma_map_single | dma_pool |
|---|---|---|---|
| 分配粒度 | 页对齐,最小 4KB | 任意字节(由原始缓冲区决定) | 固定大小(创建时指定) |
| 缓存一致性 | 硬件保证,无需同步 | 需要显式 dma_sync_* 维护 | 同 dma_alloc_coherent(底层使用它) |
| 分配/释放开销 | 高(涉及页分配和页表修改) | 中(仅映射操作,无内存分配) | 极低(池内指针操作) |
| CPU 访问延迟 | 高(非缓存/写结合区域) | 低(缓存区域,同步后访问) | 高(同 dma_alloc_coherent) |
| 适用场景 | 描述符环、控制结构、长期缓冲区 | 网络 skb、块设备 bio、临时数据 | 固定大小高频分配(如 USB isoc 传输) |
| 内存碎片 | 容易产生外部碎片 | 无额外碎片(复用已有内存) | 无碎片(池内固定槽位) |
2.6 实践建议与避坑指南
- 优先使用 dma_alloc_coherent 管理控制路径: 描述符环、命令队列等控制结构对延迟不敏感,但要求绝对的数据一致性,一致性分配是最安全的选择。
- 数据路径使用 dma_map_single + 流式映射: 网络和存储驱动的数据缓冲区通常由上层协议栈分配,使用流式映射可以避免额外的内存拷贝,且能利用 CPU 缓存的低延迟优势。
- 高频分配场景必须使用 dma_pool: 如果每秒需要分配/释放数千次相同大小的 DMA 缓冲区(如 USB 视频帧、音频采样块),
dma_pool的分配延迟比dma_alloc_coherent低 10-100 倍。 - 始终检查映射错误:
dma_map_single返回的地址必须用dma_mapping_error检查,IOMMU 开启时映射可能失败。 - 对齐要求不可忽视: 某些 DMA 控制器要求缓冲区地址对齐到 32 字节或 64 字节边界,使用
dma_pool_create时务必设置正确的align参数。
CONFIG_DMA_API_DEBUG 和 CONFIG_DMA_API_DEBUG_SG 调试选项。开启后,内核会检测常见的 DMA API 误用,如:未检查映射错误、映射方向错误、同步遗漏、双重释放等。在开发阶段务必开启此选项。