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,并对比它们的适用场景与性能特征。

学习前提: 在调用任何 DMA API 之前,驱动必须持有有效的 struct device * 指针,该指针通常来自 platform_device 或 pci_dev。所有 DMA 操作都通过这个设备对象与内核 DMA 子系统交互。

2.1 DMA API 全景图:从分配到同步

下图展示了 Linux DMA API 的完整工作流,从缓冲区分配、地址映射、传输控制到同步与释放。本章重点介绍的三个 API 分别对应图中“一致性分配”、“流式映射”和“池化分配”三个关键路径。

Linux DMA API 核心工作流 Linux DMA API 核心工作流 分配阶段 dma_alloc_coherent dma_pool_create / dma_pool_alloc 映射阶段 dma_map_single dma_map_sg (分散/聚集) 同步与释放 dma_sync_single_for_cpu/device dma_free_coherent / dma_unmap_single 说明:一致性分配(dma_alloc_coherent)保证 CPU 和设备看到相同的内存视图,无需显式同步; 流式映射(dma_map_single)需要调用 dma_sync_* 维护缓存一致性,适用于高频、短时传输。

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 环形缓冲区、视频帧缓冲、加密引擎上下文。
  • 对延迟敏感的小型传输: 每次传输数据量小但频率高,避免同步开销。
性能提示: 一致性缓冲区在 ARM/ARM64 平台上通常通过将页表属性设置为“非缓存(non-cacheable)”或“写结合(write-combine)”来实现。这意味着 CPU 访问这些缓冲区时不会经过 L1/L2 缓存,读/写延迟比普通内存高 2-5 倍。因此,不要将一致性缓冲区用于 CPU 频繁读写的临时数据。

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 调试用途 无操作

流式映射的典型生命周期

  1. 分配缓冲区: 使用 kmalloc 或 netdev_alloc_skb 分配数据缓冲区。
  2. 映射: 调用 dma_map_single 获取 DMA 地址,检查返回值是否为 dma_mapping_error。
  3. 提交传输: 将 DMA 地址写入设备描述符,触发设备 DMA 操作。
  4. 同步(可选): 在 CPU 访问数据前调用 dma_sync_single_for_cpu,在设备访问前调用 dma_sync_single_for_device。
  5. 解映射: 传输完成后调用 dma_unmap_single 释放映射关系。
常见错误: 流式映射的缓冲区在映射期间不能被 CPU 访问(除非显式同步)。如果驱动在 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),通过位图或空闲链表管理。

dma_pool 内部结构示意图 dma_pool 内部结构:块(Block)与槽位(Slot) dma_pool 实例 Block 0 (4KB) Slot 0 (空闲) Slot 1 (使用中) Slot 2 (空闲) Slot 3 (预分配) Block 1 (4KB) Slot 0 (空闲) Slot 1 (空闲) Slot 2 (空闲) Slot 3 (空闲) Block 2 (4KB) Slot 0 (使用中) Slot 1 (使用中) Slot 2 (使用中) Slot 3 (空闲) 每个 Block 由一次 dma_alloc_coherent 分配,Block 内部按固定大小切分为 Slot;空闲 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 误用,如:未检查映射错误、映射方向错误、同步遗漏、双重释放等。在开发阶段务必开启此选项。