Scatter Gather DMA 概述:SG-DMA 定义、优势,与传统 DMA 区别

大家好,我是老周。这一章咱们聊聊 Scatter Gather DMA,简称 SG-DMA。很多工程师早期接触的都是传统的块 DMA(Block DMA),脑子里总觉得 DMA 就该一次搬一整块连续数据。直到项目里遇到内存碎片、大块分配失败的问题,才意识到 SG-DMA 有多香。今天我就把它的定义、优势,以及跟传统 DMA 的区别掰开揉碎了讲清楚。

1. SG-DMA 到底是什么?

SG-DMA 的全称是 Scatter Gather Direct Memory Access。说白了,它允许你在一次 DMA 传输中,把多个不连续的内存块(叫做 scatter/gather 列表)串起来,一次性搬完。你不需要让 CPU 把零散的小块拼成一个连续大缓冲区,再启动 DMA——这些零碎活全都交给描述符链表(Descriptor Chain)自动处理。

描述符链表长什么样?每个描述符里放着:

  • 源地址 / 目的地址(指向一块内存)
  • 传输长度
  • 控制标志(是否最后一个、是否产生中断等)
  • 指向下一个描述符的指针

DMA 控制器拿到链表头指针后,自动沿着链表一个一个描述符执行。每个描述符对应的搬完后,自动跳下一块,直到遇到结束标志才停下来通知 CPU。你想想看,CPU 只需要设置一次链表,后面全程不用管。

关键点: SG-DMA 把一个 DMA 请求拆成多个物理不连续的传输段,但对软件来说,它看起来就是一次完整的、连续的传输。上层驱动完全不需要关心内存是否连续。

2. 跟传统 DMA 比,SG-DMA 强在哪?

传统 DMA(也叫块 DMA)有个硬伤:源和目的地址必须物理连续,而且一次传输只能对应一个连续块。举个例子,你要从网卡收 100 KB 数据,但系统只能给你 5 个不连续的 20KB 内存块。传统 DMA 就得先让 CPU 拷贝拼接成一个连续大缓冲区,再启动 DMA——白白浪费 CPU 时间和内存带宽。

SG-DMA 直接解决了这个问题。来看张图:

传统 DMA vs SG-DMA 数据流对比 传统 DMA(块 DMA) CPU 拼连续缓冲区 连续缓冲区 (必须物理连续) DMA 引擎 SG-DMA CPU 只建描述符链表 描述符链表 (指向多个不连续块) SG-DMA 引擎 块 1 (0x1000) 块 2 (0x5000) 块 3 (0xA000) 传统 DMA 只能搬一个连续块;SG-DMA 通过描述符链表自动串联多个不连续块

从这张图能看出,传统 DMA 的 CPU 要先拼缓冲区,然后 DMA 搬走;SG-DMA 的 CPU 只建链表,然后把链表头告诉 DMA,DMA 自己就能遍历所有碎片块。嗯,这里要注意:SG-DMA 的内部硬件通常包含一个专用的描述符缓存和地址解析器,所以它读链表的时候不会抢占太多系统总线。

3. SG-DMA 的三大核心优势

  1. 减少 CPU 干预:一次配置,多次传输。我在项目中遇到过网络驱动每收一个包都要重新配置 DMA 的情况,结果 CPU 占用率飙升。换成 SG-DMA 后,把所有 sk_buff 的环形缓冲描述符链起来,一次启动,后续描述符由 DMA 自己取,CPU 负载直接降了 40%。
  2. 高效处理非连续内存:操作系统内存碎片是常态。你想想看,一个运行了 3 天的嵌入式设备,还能申请到超过 64KB 的连续物理块吗?很难。SG-DMA 自动把散落小块串起来,不要求底层物理连续,这对文件系统缓存、视频帧处理太关键了。
  3. 降低中断次数:传统 DMA 每搬完一个块就产生一次中断;SG-DMA 可以设置整个链表传输完成后才产生一个中断。我曾经调试过一个存储加速卡,原来每秒 5000 次中断,改成 SG-DMA 后降到每秒 30 次,CPU 几乎被释放出来做其他运算。

避坑指南: 我曾经因为忘记设置描述符链表中最后一个描述符的“结束标志”(通常是 chain_end 或 last_flag),导致 DMA 永远停不下来——它一直跑到最后一个描述符后,读下一个无效指针,直接访问非法地址引发总线错误。所以,链表初始化时,一定记得把最后一个描述符的结束位置 1。

4. 与传统 DMA 的详细对比

对比维度 传统 DMA(块 DMA) Scatter Gather DMA
内存连续性要求 源 / 目的地址必须物理连续 不要求连续,可跨多个碎片块
配置复杂度 一次一配置,简单 需构建描述符链表,稍复杂
CPU 干预次数 每块一次中断 / 轮询 整条链表完成一次中断
对内存碎片容错 差,大块分配失败则传输失败 好,自动拼接碎片
典型应用场景 小数据量、连续缓冲、UART 等 网络、存储、视频、大数据块传输
硬件成本 低,通用 DMA 控制器 需额外链表引擎和地址解析逻辑

5. 什么时候该用 SG-DMA?

我个人判断标准很简单:如果数据源或目的地址是动态分配的、大小不确定、或者应用层需要零拷贝,优先考虑 SG-DMA。反过来说,如果你只是搬几百字节的 SPI 数据,或者 CPU 资源非常空闲且内存连续性有保障,用传统 DMA 也不差。

举个实际例子:我曾经做过一个高速 NVMe 磁盘控制器,命令队列里的每个请求可能对应多个物理分散的 PRP(Physical Region Page)。这时候用 SG-DMA 把 PRP 列表直接挂到描述符链上,DMA 引擎就能自己完成整个 IO 请求的数据搬运,CPU 只需要提交命令并等待完成通知。换成传统 DMA 的话,你可能要把所有数据先拷到一个大缓冲区,再给 NVMe 发命令——多了一层拷贝,延迟和带宽都吃亏。

特别注意: SG-DMA 的描述符链表本身需要内存来存放。如果你的系统内存极其紧张(比如只有几十 KB RAM),描述符链表可能占掉不少空间。另外,链表遍历会引入额外的总线读操作,在某些低端总线(如 AMBA AHB-Lite)上可能因为等待周期导致传输效率略低于传统块 DMA。所以“SG-DMA 一定更快”这个观点不成立,需要具体场景具体分析。

好了,这一章我花了不少时间把 SG-DMA 的核心概念、优势以及与传统 DMA 的差异讲透了。希望你在设计 DMA 方案时能多一个选择。下一期咱们深入描述符链表的构建细节,到时候我会带上实际代码示例。嗯,今天就到这儿。