DMA 传输类型:Block DMA、Scatter-Gather DMA、Ring Buffer

做 PCIe 驱动面试,DMA 传输类型几乎是必考题。面试官会问你:“你们项目用的哪种 DMA?选型依据是什么?” 说白了,这块就是看你有没有真正调过硬件,踩过坑。

我个人把常见 DMA 传输方式分为三类:Block DMA、Scatter-Gather DMA、Ring Buffer DMA。每种都有自己的脾气,选错了后面能把你折腾死。

1. Block DMA(块传输)

Block DMA 是最简单粗暴的方式。一次传输要求物理内存是连续的一大块。DMA 引擎拿到起始地址和长度就开始搬,搬完中断通知 CPU。

适用场景:小数据量(比如 4KB~64KB)、实时性要求高、系统刚启动时内存碎片少。

我遇到过一类踩坑案例:客户在嵌入式系统里做视频采集,每次数据包 8KB,用 Block DMA 跑得好好的。后来升级系统,内核开了大量设备驱动,内存频繁申请释放,连续页申请成功率暴跌。DMA 请求排队等着分配连续内存,帧率直接掉到 10 fps。

避坑:Block DMA 必须在驱动初始化时就预留连续内存池(dma_alloc_coherent),不要临时分配。否则一旦系统内存碎片化,你就等着看 DMA error 吧。

Block DMA 的好处是硬件实现简单,中断次数少。坏处就不说了——内存连续性门槛太高,大块传输基本不可靠。

2. Scatter-Gather DMA(散聚传输)

Scatter-Gather DMA(简称 SG DMA)是现代 PCIe 设备的主流方案。它允许 DMA 引擎把多个不连续的物理内存块打包成一个传输任务。怎么做?靠一张 SG 表(Scatter-Gather List)——每个表项记录一块物理地址和长度,DMA 引擎按表项顺序挨个搬。

我习惯把 SG 表看作“快递配送清单”:CPU 告诉 DMA 去哪些地址取货、送到哪些地址,DMA 自己按清单跑一趟。这样 CPU 就不用等每次传输完才干下一件事。

SG 表结构(典型格式)

struct sg_entry {
    uint64_t dma_addr;  // 源或目的物理地址
    uint32_t length;    // 本次传输字节数
    uint32_t flags;     // 控制位(如最后一项标记)
};

DMA 引擎遍历这张表,直到遇到 “last” 标记。整个过程可以批量提交,减少 CPU 干预。项目中我常用 SG DMA 配合 PCIe MRd/MWr 事务,带宽能跑到线速的 95% 以上。

关键指标:SG 表最大项数由硬件决定。常见的 PCIe DMA 引擎支持 256~4096 项。超过上限就得把表拆成链式 SG(Scatter-Gather Chain),每个链节点指向下一段表。

老实说,SG DMA 也不是没坑。驱动必须管理 SG 表的生命周期——表项必须驻留在内存中,且对 DMA 引擎可见(不能 cache 污染)。我曾经在某个 SOC 平台上遇到 SG 表放在 cacheable 区域,DMA 读到的是旧数据,一传就出错。最后加了 dma_sync_single_for_device 才解决。

3. Ring Buffer DMA(环形缓冲区传输)

Ring Buffer 是另一个流派。它在主存里开辟一个固定大小的环形队列(通常叫描述符环),驱动往里写描述符、硬件从另一端取描述符去执行,完成后再把状态写回描述符。CPU 和硬件通过写指针(P)和消费指针(C)协同工作。

概念用途
Producer Index驱动写入新描述符后更新,通知硬件有活干
Consumer Index硬件处理完描述符后更新,通知驱动 DMA 完成
Ring Size描述符个数,通常是 2 的幂(方便取模)

Ring Buffer 的最大优势是无锁设计。驱动只需更新一个写指针,硬件只读另一个指针,完全不用互斥。在高速网络、NVMe、GPU 场景里,环形缓冲区几乎是标配。

我记得第一次调 Ring Buffer 时犯了个蠢错:环大小设成了 127,而不是 128。结果生产者索引回卷时取模计算错误,描述符永远写不对位置。这类问题但凡驱动写多了,都能第一时间怀疑到环大小必须是 2 的幂上。

注意:Ring Buffer 要求描述符不能跨 cache line 边界,否则硬件读到脏数据。驱动申请内存时要用 dma_alloc_coherent,或者主动做 cache 刷新。

4. 三种 DMA 传输类型对比

项目Block DMASG DMARing Buffer DMA
内存连续性必须连续支持不连续支持(描述符连续即可)
描述符管理无(只有地址+长度)SG 表(可链式)环形队列
CPU 干预频率每次传输都中断批量触发中断按水位或超时中断
典型应用SPI、I2C、小数据采集存储、网络、GPU、视频NVMe、万兆网卡、专业加速卡
驱动复杂度简单中等(需维护 SG 表)中等(需管理环指针)
带宽利用率低(CPU 频繁干预)高极高(流式处理)

5. 实际项目怎么选?

如果是面试题,很多候选人背得出定义,但问到“为什么选 Ring Buffer 而不选 SG”就卡壳。我一般在项目中按以下逻辑判断:

  • 数据流方向固定、描述符数量可预测 → 优先 Ring Buffer。比如 NVMe 的 SQ/CQ 就是天然环形。
  • 数据流随机、描述符大小不定 → 用 SG DMA。比如显卡的显存映射,物理页不连续但要批量传输。
  • 简单小系统,一个中断处理一次搬移 → Block DMA 最简单,不用折腾。

不过也别一根筋。有些硬件同时支持 SG 和 Ring Buffer的低级模式(Legacy Ring),描述符格式长得像 SG 表但通过环指针管理。碰到这种混血设备,驱动要小心——我曾经在某个 FPGA 核上把 SG 表当 Ring Buffer 用,环指针回卷时表项没更新,结果 DMA 一直在搬旧数据,排查了三天。

嗯,这三种类型本质上就是在资源(内存连续性)、延迟(中断开销)、吞吐(批量传输)三者间做权衡。没有银弹,只有最合适的。

三种 DMA 传输类型对比 三种 DMA 传输类型对比 PCIe Endpoint Block DMA 连续内存 + 单次中断 Scatter-Gather DMA SG 表 → 非连续内存 Ring Buffer DMA 环形描述符队列 🚨 典型问题 连续页申请失败 → 传输阻塞 中断频繁 → CPU 开销大 ✅ 优势 零拷贝传输,带宽利用率高 支持任意物理页组合 ⚙️ 优势 无锁设计,CPU 与硬件并行 流式处理,极低延迟 选型决策路径 连续内存 & 小数据量 → Block DMA 非连续内存 & 随机 IO → Scatter-Gather DMA 固定流式、高性能 & 低中断 → Ring Buffer DMA