一、DMA Engine 子系统概述
DMA(Direct Memory Access)是计算机系统中实现高速外设与内存间数据传输的核心机制。在没有 DMA CPU 的干预,从而实现真正的并行处理和 CPU 资源释放。
The Linux DMA Engine 子系统提供了统一的 DMA 控制器驱动框架和 API 层,屏蔽了不同厂商 DMA 控制器硬件的差异:
/drivers/dma/
├── core.c # DMA engine 核心框架(channel 分配、cookie 管理)
├── virt-dma.c # 虚拟 DMA 层(描述符链表管理)
├── pl330.c # ARM PL330 DMA 控制器
├── edma.c # Texas Instruments EDMA
├── ste_dma40.c # ST-Ericsson DMA40
├── imx-sdma.c # i.MX SoC (Freescale/NXP)
├── xilinx_dma.c # Xilinx PCIe DMA engine
├── dw_dmac.c # Synopsys DesignWare DMA
├── at_hdmac.c # Atmel AHB DMA
├── dma-jz4780.c # Ingenic JZ4780
├── sf-pdma.c # Sophgo PDMA
└── bcm2835-dma.c # Broadcom BCM2835 (Raspberry Pi)
1.1 核心数据结构层次
DMA Engine 子系统的核心是四个关键数据结构的有机配合:
struct dma_device — 描述一个 DMA 控制器硬件(capabilities、channels 列表)
├── struct dma_chan — 一个 DMA 通道(per-channel 状态锁、cookie 生成器)
│ ├── struct virt_dma_chan — 虚拟 DMA 通道(描述符环形链表、pause 处理)
│ │ └── struct virt_dma_desc — 虚拟 DMA 描述符(硬件描述符容器)
│ └── struct dma_slave_config — 从设备方向配置(地址、突发长度、数据宽度)
└── struct dma_async_tx_descriptor — 异步传输描述符(.tx_submit、.issue_pending)
关键设计思路:virt-dma.c 将所有控制器硬件抽象为一个"虚拟通道"模型,描述符可以在硬件队列未满时预先排队,避免硬件空闲。这样,即使硬件通道只有 4 个 slots,也可以预链更多描述符形成流水线。
二、DMA 控制器硬件原理
2.1 基本传输模式
| 模式 | 方向 | 描述 |
|---|---|---|
| Mem-to-Mem | 内存→内存 | 软件触发,用于大块内存拷贝(如 memcpy 加速) |
| Mem-to-Dev | 内存→设备 | 外设 TX(如 UART 发送、SPI 写入) |
| Dev-to-Mem | 设备→内存 | 外设 RX(如 ADC 采集、网卡接收) |
| Dev-to-Dev | 设备→设备 | 特殊场景(如 FIFO→FIFO 转发) |
2.2 地址突发(Burst)与 FIFO 阈值
DMA 控制器通常内部集成 FIFO,配合突发传输以提高总线效率。单次突发传输会连续读写多个节拍后才释放总线,4/8/16 字突发可将 AHB/AXI 总线利用率从 50%(单次)提升至 90% 以上。
- FIFO 阈值:触发 DMA 请求的水位线。设置过小→频繁请求(额外握手开销);设置过大→数据延迟增加。
- 突发长度:通常与 FIFO 深度匹配(16 字节 FIFO → 4 字突发)。
- 数据宽度:Peripheral 的.data_width 通常固定(如 UART 仅支持 8-bit),Memory 侧可设 8/16/32/64/128-bit。
2.3 Gather / Scatter
Scatter-Gather DMA 允许单次传输跨越多个不连续的内存块。控制器内部维护一个描述符链表(Descriptor Chain),每个条目指定一段连续区域的源地址、目标地址和长度。硬件自动遍历这些条目、依次完成传输后发出完成中断,避免了 CPU 逐段配置的开销。
三、dmaengine API 核心编程
3.1 通道申请与配置
通道的申请流程遵循 dma_request_chan → dmaengine_slave_config → prepare → submit 的经典模式,即先查设备树或 ACPI 获取通道句柄,再配置从设备方向参数,然后准备硬件描述符、最后提交至通道队列、等待完成。
dma_slave_config 各字段语义:
direction:DMA_MEM_TO_DEV / DMA_DEV_TO_MEM / DMA_MEM_TO_MEMsrc_addr:源物理地址(总线地址,不是虚拟地址!iommu 映射后)dst_addr:目标物理地址src_addr_width/dst_addr_width:传输位宽(1/2/4/8 字节)src_maxburst/dst_maxburst:突发节拍数slave_id:外设 ID 线编号(SoC 内部路由用)
3.2 Synchronous vs Asynchronous API
Synchronous(同步)API 在执行时阻塞等待传输完成,以 dmaengine_prep_slave_sg 创建描述符,通过 dmaengine_submit 提交,然后 dma_async_issue_pending 启动传输,最后 dma_wait_for_async_tx 同步等待完成返回。这种方式适合简单场景,缺点是阻塞期间 CPU 无法执行其他任务,但对于快速传输(如控制命令)开销可接受。
Asynchronous(异步)API 使用回调模式,描述符初始化后设置 callback 函数,提交后由中断自动调用回调,完全不阻塞 CPU。通过 dmaengine_terminate_sync 终止进行中的传输,用 dma_release_channel 释放通道。
3.3 Transaction Types 详解
dmaengine 提供多种传输预处理函数,分别对应不同的硬件操作模式。Interleaved DMA(dmaengine_prep_interleaved_dma)用于 2D 传输,特别适合图像处理场景——通过 src/dst 方向的 stride 实现行间步进和帧间跳转。Slave Sg(dmaengine_prep_slave_sg)是最通用的模式,支持不连续区域的菊花链式传输,最多支持 sg_len 个段。 Cyclic DMA(dmaengine_prep_dma_cyclic)是环形缓冲区传输,一次配置、循环传输,非常适合音频等周期性外设场景,通过 callback 通知半满/全满状态。Memcpy(dmaengine_prep_dma_memcpy)则是最简单的内存到内存传输,用于大块拷贝加速。
四、Scatter-Gather 链式编程深度
4.1 scatterlist 数据结构
scatterlist 是 Linux 内核中表示一个可能包含不连续物理页的 I/O 缓冲区的标准数据结构。它由 page offset、length 和 dma_address 三个关键字段组成。当我需要将大块内存用于 DMA 时,首先通过 dma_map_sg 将其映射为总线地址列表,填满 sg 数组后,再通过 dmaengine_prep_slave_sg 将整个 sg 列表提交给硬件控制器,实现零拷贝的链式传输。
4.2 描述符链(Descriptor Chaining)
现代 DMA 控制器(如 PL330、DesignWare DMA)支持硬件描述链:一次提交后,硬件自动处理所有描述符、并在最后一个描述符完成时触发中断(或仅每个描述符触发一次),实现"批量入队、一次中断"的高效模式。virt-dma 虚拟层在软件层为每条描述符创建一个 virt_dma_desc 节点,形成环形链表,硬件完成后通过 IRQ handler 自动推进到下一个描述符。
4.3 Pick List vs. Fixed List
旧版 PL330 驱动使用 Pick List 方式(软件选择空闲 Microchannel),新版 DMAengine 采用 Fixed List(静态通道映射),更易于预测和实时性要求较高的嵌入式场景。
五、Cyclic DMA 环形传输 — 音频与实时场景
Cyclic DMA 是 DMA Engine 最强大的模式之一,它允许在环形缓冲区上持续传输而不需要 CPU 反复重新配置。
驱动中的周期性buffer管理:以ALSA SoC为例,在 hw_params 阶段通过 snd_pcm_lib_malloc_pages 分配一个包含 period_size × periods 的大 buffer,然后 dmaengine_prep_dma_cyclic 配置硬件在该 buffer 上做周期循环传输。每个周期完成后触发通知,驱动在中断处理程序中调用 dmaengine_tx_status 查询当前硬件 write position(通过 DMA_RESIDUE 机制获取剩余数据量),并通过 bytes_to_frames 转换为 frame offset,最终调用 snd_pcm_period_elapsed 通知 ALSA 层写入了一个新的 period。debugfs 中的 residue 节点通过 dmaengine_tx_status 的 residue字段将内部状态暴露给用户态,用于调试。
Cyclic 与 Normal(Slave Sg)的区别:
- Normal:传输完一个 descriptor 后停止,需 CPU 重新提交
- Cyclic:传输完一个 period 后自动跳转到下一个,循环往复,仅当调用 dmaengine_terminate_sync 时才停止
- Cyclic 中断频率可配置(每个 period / 每 N 个 period),降低中断开销
六、用户态访问 DMA Engine
6.1 DMA Engine 的 ioctl 接口
Linux 通过 /dev/dma 或自定义 misc 设备暴露 DMA Engine 能力到用户态。最广泛使用的是 UIO(Userspace I/O)框架 + VFIO(Virtual Function I/O)框架。
通过 VFIO 使用 DMA:VFIO 提供安全、IOMMU 保护的 DMA 访问模式。应用程序首先通过 VFIO API 获取设备文件描述符,然后使用 VFIO_IOMMU_MAP_DMA 控制 IOMMU 建立虚拟到物理的映射,并结合 VFIO_IOMMU_GET_INFO 获取 IOMMU 能力信息。
6.2 用户态 DMA 与网络栈整合
高性能网络框架如 DPDK、io_uring 可以通过 VFIO 实现真正的零拷贝收发包。其可编程的 DMA 引擎(如 Intel ixgbe 的 Admin Queue、NVIDIA ConnectX-5 的 Elastic Packet Accelerator)通常通过寄存器间接编程或"制造商命令"方式进行管理。
七、缓存一致性维护
7.1 DMA 映射 API 全景
Linux 内核提供四种 DMA 映射函数来解决不同场景下的缓存一致性问题。dma_map_sg 是最关键的函数,它确定了一段区域的映射方向(到设备、从设备、双向),并执行相应的缓存同步操作。dma_sync_sg_for_cpu 和 dma_sync_sg_for_device 分别用于在 CPU 或设备访问前后同步缓存。dma_unmap_sg 则用于设备传输完成后的解映射操作。
7.2 Coherent DMA(一致性 DMA)与 Streaming DMA
Coherent DMA通过架构特定的方式自动维护一致性(ARM 使用 dma_alloc_coherent 分配的内存,要么始终写穿透,要么通过硬件缓存一致性协议 SMMU 维护)。这种方式编程简单但性能较差,适用于控制结构体等小体积、频繁访问的场景。
Streaming DMA则要求使用者显式同步——在进行 DMA 传输前调用 dma_map_single 或 dma_map_sg 将缓存所有权交给外设,传输完成后调用 dma_unmap_single 或 dma_unmap_sg 将所有权归还给 CPU。如果 CPU 需要在 DMA 运行中访问 buffer,必须调用 dma_sync_single_for_cpu 或 dma_sync_sg_for_cpu 来获取最新的数据。
7.3 DMA_ATTR 属性标志
DMA_ATTR_WRITE_BARRIER:强制写屏障(SAMA5D2 等需要)DMA_ATTR_WEAK_ORDERING:允许弱排序(提升性能但需注意内存序)DMA_ATTR_SKIP_CPU_SYNC:跳过 CPU 同步(使用者自行管理)DMA_ATTR_FORCE_CONTIGUOUS:强制连续物理内存(降级为 alloc_pages)DMA_ATTR_ALLOC_SINGLE_PAGES:单页分配(避免高阶分配失败)
八、PCIe Multi-Queue DMA 实战
8.1 PCIe DMA 架构
PCIe DMA 用于 Root Complex(RC)与 Endpoint(EP)之间,或 EP 与 EP(通过 NTB)之间的大块数据传输。典型的 PCIe DMA 控制器支持多队列模式,每个队列独立投递描述符,硬件轮询门铃寄存器获取新任务。
多队列DMA(XDMA驱动分析):XDMA使用H2C和C2H两个方向的4个通道,每个通道有独立的提交门铃、完成写回和中断。描述符自动在内存环形队列中轮询,写回包含状态、字节长度等信息。中断支持用户模式和中断聚合模式(coalesce),后者通过设置count字段来降低中断频率。读写使用简单的地址-长度模式,IOMMU支持在物理地址上启用的可选功能。
8.2 性能调优关键参数
- Description Ring Size:队列深度,通常为 1024~4096。深度太小 → 硬件空闲;太大 → 延迟增加
- Interrupt Coalesce:聚合中断数(1~255),高吞吐场景设较大值降低 CPU 开销
- Max Transfer Size:单次最大传输(通常 16 MiB),超大块需拆分为多段
- Max Read Request Size:PCIe 最大读请求(通常 512 B ~ 4 KB),影响 RC 端到 EP 的延迟
8.3 性能基准
| DMA控制器 | 方向 | 理论带宽 | 实测带宽 | CPU占用 |
|---|---|---|---|---|
| Xilinx XDMA | H2C | 16 GB/s (Gen3×8) | 14.2 GB/s | <1% |
| Xilinx XDMA | C2H | 16 GB/s | 13.8 GB/s | <1% |
| PL330 (嵌入式) | Mem-to-Mem | 3.2 GB/s (AXI64) | 2.8 GB/s | ~5% |
| intel-sedma | Mem-to-Dev | 4 GB/s | 3.6 GB/s | ~2% |
九、DMA Engine 在 Linux 内核中的典型应用实例
9.1 SPI 控制器驱动中的 DMA 应用
SPI 控制器驱动(如 spi-pl022、spi-stm32)是 DMA Engine 最频繁的使用者之一。tx_sg 和 rx_sg 的联合使用实现了全双工同步收发:DMA 控制器同时从 TX FIFO 写数据和从 RX FIFO 读数据到内存,两个通道的中断合并后视为一次完整传输完成。如果传输长度超过 DMA 最大段长,则自动拆分为多个 Slave Sg 描述符。
9.2 MMC/SD 卡驱动中的 DMA 应用
SD/MMC 驱动使用 DMA Engine 实现块数据的高吞吐量读写。bcm2835-sdhost(树莓_pi SD 卡驱动)就是一个典型例子:它先配置 DMA 的 src/dst 为 SDHOST DATA/FIFO 寄存器,再通过 scatterlist 遍历 page 链表,最后通过 dmaengine_prep_slave_sg 提交至硬件,实现整卡拷贝时 <2% CPU 占用。
9.3 网络驱动中的 DMA 应用
万兆网卡(如 stmmac、igb、ixgbe)的 TX/RX ring buffer 完全靠 DMA Engine 维护。TX 流程由驱动先准备 SKB 链表并解析为一个 sg 数组,DMA 控制器通过 dma_map_sg 将每个 page 的物理地址和长度传递给硬件,并在最后一个描述符添加"中断使能”位以实现间歇性中断。RX 流程则由驱动预分配 SKB 并映射为 sg 数组,通过 dmaengine_prep_slave_sg 提交,硬件通过 DMA 将数据从 RX FIFO 写入该 buffer。
9.4 Framebuffer 驱动中的 DMA 应用
嵌入式 LCD 控制器(如 ili9341、ssd1306)驱动通过 DMA Engine 实现零停顿更新帧缓冲区。以 ili9341 为例,通过设置 DMA 的 dst_addr 为 LCD 数据寄存器、src_addr 为 framebuffer 内存,设置 16-bit 数据宽度及突发长度 16,然后提交 Slave Sg 描述符,实现"单次提交、持续刷屏"的零 CPU 开销。
十、DMA Engine 性能调优与故障排查
10.1 性能监控节点
DMA Engine 注册时在 debugfs 创建 /sys/kernel/debug/dmaengine/ 目录,其中dmaX/channels列出所有通道的配置,dmaX/in_use显示当前活跃通道,还有摘要文件包含 bytes_transferred 和 issued 两个关键计数器。
10.2 热路径优化
- 减少映射开销:对频繁使用的大块 buffer 使用
dma_map_sg映射后、多次dma_sync_sg_for_device同步,而非每次重新映射。 - 描述符预分配:对于 Cyclic 或高频 Slave Sg,可使用
dma_pool_create()预分配描述符池,避免运行时 kmalloc 开销。 - 中断聚合:在描述符级别标记
DMA_PREP_INTERRUPT,仅让需要通知的描述符产生中断,降低中断频率。 - 使用 IOMMU 大页:IOMMU 映射的 TLB miss 开销较大,使用 2MB/1GB 大页映射 DMA buffer 可以减少 TLB 抖动。
- 调整 Gate/Ready 采样:在一些控制器中,调整 GPIO 信号门控时序可以避免"虚假READY",消除虚假中断(PL330 常见陷阱)
10.3 常见故障与排查
| "BUSY" channel | 传输未终止又重新申请。解决:dmaengine_terminate_sync() 清状态再重申请 |
| "Descriptor not queued | 内部 ring 满,需要等待 .tx_submit 返回 ERR 或调整 ring 大小 |
| Data corruption | 缓存不一致:在 DMA 读回后遗漏 dma_sync_sg_for_cpu();或 src 地址传入虚拟地址但硬件要求物理地址 |
| "Invalid source/target address" | DMA 控制器 32-bit 地址限制,buffer 超出范围。需使用 DMA mask 或 IOMMU |
| "Timeout" | 设备未产生 DMA request。检查 Peripheral 的 DMAEN 位、时钟、GPIO 复用 |
| DMA 带宽低 | 总线仲裁延迟。调整 bus matrix 优先级、提升 AXI outstanding 数 |
十一、DMA Engine 编程模板(驱动开发参考)
11.1 初始化完整流程模板
DMA 驱动初始化的完整流程分为几个关键步骤。首先通过_get 获取时钟和 reset 控制,接下来 dmaengine_slave_config 配置从机方向,然后 dma_alloc_coherent 为描述符预分配一致性内存。在传输准备阶段,根据同步或异步模式选择 dmaengine_prep_slave_sg 或 dmaengine_prep_dma_memcpy 来构建硬件描述符。传输完成后,使用 dmaengine_terminate_sync 来终止所有进行中的传输,最后释放通道和时钟资源。这套流程确保了 DMA 通道在驱动生命周期内的正确管理和资源释放。
11.2 Cyclic 模式特殊注意事项
Cyclic模式下,每个 period 的回调函数调用非常密集,必须遵循以下原则:回调函数中不可睡眠(在 tasklet 或硬中断上下文中运行);回调函数执行时间应远小于 cycle_time;绝不能在回调中调用 dmaengine_terminate_sync(会死锁,因为 terminate 会等待回调完成);如果需要终止传输,应设置标志,在进程上下文中调用 terminate。此外,Cyclic 模式下的 buffer 管理应使用单一块而非 scatterlist,避免 PAGE 跨越引入延迟。
十二、eBPF + DMA Engine:可编程数据面融合
随着智能网卡(DPU/IPU)的兴起,DMA Engine 的前沿发展方向是与 eBPF/XDP 的深度集成。
DXGKMD(XDP-to-DMA Extensions)允许通过 BPF_MAP_TYPE_DMA_RING_MAP 将 DMA 环形缓冲区暴露给 XDP 程序,实现网卡→DMA→eBPF 处理→DMA→内存全路径零 CPU 参与。这种"硬件直通+可编程"模式正在重新定义网络数据面的实现方式。
AF_XDP (Address Family eXpress Data Path) 的用户态驱动可以通过 VFIO 获取设备 FD,然后使用 dma_alloc_coherent 分配连续的 packet buffer 并通过 dma_map_sg 映射物理地址给网卡,实现真正的零拷贝收发包。结合 io_uring 的 fixed buffer 绑定,可以构建延迟低于 5μs 的高频交易数据路径。
总结
Linux DMA Engine 子系统是一个被严重低估的底层 API,它隐藏在外设驱动之下,默默为整个系统的数据传输提供高效服务。理解 DMA Engine,不仅能够写出性能更好的设备驱动,还能在网络加速、存储优化、多媒体处理等关键场景找到性能瓶颈的根源。
核心要点回顾:
- DMA Engine 提供统一的控制器驱动框架,通过 virt-dma 抽象层解耦硬件差异
- Synchronous API 简单易用,Asynchronous + callback 才是高性能的正确选择
- Scatter-Gather 不连续内存 + 描述符链 在硬件层面实现零 CPU 干预的批量传输
- Cyclic DMA 是音频/实时场景的最优解,一次配置、循环运行
- 缓存一致性是 DMA 编程中最容易出错的地方,Coherent 与 Streaming 两种使用模式需严格区分
- IOMMU + VFIO 是用户态访问 DMA Engine 的安全通道
- 未来方向:eBPF + DMA 融合,构建可编程硬件加速器
推荐学习路线:
- 阅读 virt-dma.c + pl330.c,理解"虚拟通道→硬件通道"的映射机制
- 编写一个简单的 DMA 驱动(基于 QEMU 的 SSE DMA 或 Raspberry Pi PL330)
- 使用 dmaengine_prep_slave_sg 实现 MCU 到 Linux 的零拷贝数据传输
- 分析 Xilinx XDMA 驱动,理解 PCIe Multi-Queue DMA 架构
- 阅读 io_uring 的 fixed buffer 绑定机制(io_uring + VFIO + DMA 三位一体)

发表评论 取消回复