DMA完成通知机制深度解析:从中断到轮询,AI Infra性能优化关键
2026/9/14 2:40:02 网站建设 项目流程

1. DMA 完成通知:设备“交作业”前,CPU 还蒙在鼓里

DMA 做完了,设备怎么告诉 CPU“我干完了”?这个问题,我几乎每天都会在 AI Infra 相关的调试里碰到。不管是在调 NVMe 驱动的 IO 路径,还是排查网卡收包延迟,抑或只是把一块 RK3588 开发板的以太网大流量压上去,最终都会落到同一个问题上:DMA 引擎把数据搬完了,CPU 到底靠什么感知到“搬运结束”。

DMA 的全称是 Direct Memory Access,设备绕过 CPU 直接读写内存。它的好处是省 CPU,但也带来一个副作用:DMA 完成这件事,CPU 默认是不知道的。设备不会在你面前举手,也不会有个喇叭喊“快递已放入柜中”。所以整个 DMA 完成通知机制,本质上是在回答一个问题:设备怎么用最小的代价,让 CPU 知道“数据已经在内存里,你可以用了”。

常见答案无非三类:轮询状态位、触发硬件中断、写一个完成标志再附带轻量级通知。而实际系统里,这三者往往是叠加使用的。理解这个机制,不只是为了应付“AI Infra 八股”面试题,而是因为你真的会在某天遇到一个诡异问题:数据没到、延迟暴涨、CPU 飙到 100%、或者设备驱动报出 failed to reset the dma 这类错误。所有这些问题,最后都会追溯到“CPU 是什么时候、通过什么方式知道 DMA 完成了”这一环。

1.1 什么叫“DMA 做完”?三个层面缺一不可

“DMA 做完了”这句话,乍一听很简单,实际上至少包含三个层面。

第一个是硬件层面:DMA 引擎完成了源地址到目的地址的搬运。设备内部的状态机从 BUSY 回到 IDLE,或者已经开始取下一条描述符。这个是设备自己知道的,CPU 如果不主动去读寄存器是看不到的。

第二个是总线层面:数据真的写到了内存里,并且对其他观察者可见。也就是说,当 CPU 去读那块内存时,读到的一定是新数据,而不是 CPU 缓存里的旧数据。PCIe 体系下通常靠 hardware coherency 保证,但在一些嵌入式 SoC 上,这需要驱动调用 DMA API 来维护缓存一致性。

第三个是软件层面:设备把本次传输涉及的描述符、完成队列条目、状态寄存器都更新完毕,并且这些更新以正确的顺序展示给 CPU。这是最容易出问题的一层,因为涉及乱序问题。

举一个具体例子。设备处理完一组 DMA 描述符后,先往内存地址 0x1000 写了个完成标志,然后才发起中断。CPU 在中断处理程序里读 0x1000,如果 CPU 或者总线做了重排,它可能读到 0,以为 DMA 还没完成。反过来,设备如果先发中断再更新完成标志,CPU 中断处理时也可能读到旧的描述符状态。解决这类问题要靠内存屏障,DMA 完成路径里常用的是 dma_rmb()、dma_wmb()、dma_sync_single_for_cpu 这类 API。说得直白一点,DMA 完成通知不只是一个“中断有没有触发”的问题,而是一个“中断触发后,相关内存看起来是否一致”的问题。

1.2 为什么不能靠简单轮询寄存器搞定一切?

看到这里你可能会想:既然 DMA API 这么多讲究,那 CPU 干脆每隔一小段时间去读设备的状态寄存器,看看 DMA 引擎是不是回到 IDLE 了,不就行了?

行是行,但代价很高。设备寄存器一般在 MMIO 空间,读一次 MMIO 寄存器的开销远大于读内存,经常是几百纳秒到微秒级别。如果系统里有大量 DMA 完成事件,CPU 就会一直在“查快递”这件事上空转。假设每秒钟有 100 万次 DMA 完成,一次轮询花 500ns,光轮询这一件事就能吃掉 50% 的 CPU。DMA 本来是为了帮 CPU 减负的,结果 CPU 又全耗回去了,得不偿失。

所以完整的高性能方案,一定是“事件驱动”的:要么设备主动发中断“喊”CPU,要么 CPU 在一个确定的高频路径里,用轮询的方式检查内存中的完成队列头尾指针。后面这种轮询并不低效,因为它轮询的是设备已经被 DMA 写成普通内存的完成队列,而不是 MMIO 寄存器,延迟一下低了一个数量级。接下来,我分别把这两条路线讲透。先说中断这条线。

2. 中断路线:设备如何“举手喊话”

中断是 DMA 完成通知里最经典的机制。设备干完活之后,主动打断 CPU,让 CPU 放下手头的事情去处理“DMA 已完成”的后续逻辑。但中断不是只有一种实现,从最古老的 INTx 到现代 PCIe 设备普遍支持的 MSI-X,再到 Linux 内核里那套 hardirq/softirq/threaded IRQ 的复杂路径,里面每一个环节都决定了 DMA 完成通知的延迟和开销。

2.1 传统 INTx 中断线的烦恼

传统 PCI/PCIe 设备可以通过 INTx 管脚拉低或者拉高来触发中断。这就像房间里有人举手喊“我干完了”,CPU 听到了声音,但不知道是谁喊的,只能挨个问。因为多个设备可能共享同一条中断线,中断来了之后,CPU 需要逐个调用挂在同一条 IRQ 上的每个驱动的中断处理函数,看看到底是哪个设备触发的。

在 AI Infra 服务器里,PCIe 设备一抓一大把:NVMe SSD、GPU、各种网卡和加速卡。如果大家都挤在几条 INTx 中断线上,中断风暴几乎是必然的。而且 INTx 还要求硬件上存在实际的中断线路,在虚拟化场景里也可能引入额外开销。所以现代 PCIe 设备基本都不把 INTx 当主力了,取而代之的是 MSI/MSI-X。

2.2 MSI-X:把“我干完了”写成一条消息

MSI(Message Signaled Interrupt)和 MSI-X 的核心思路非常优雅:设备不再拉一根物理线,而是向 CPU 的本地 APIC 写入一个预先配置好的地址和数据,这个写操作本身就是中断消息。换句话说,“我干完了”这句话变成了一条内存映射写入指令,从设备侧发送出去。

MSI 和 MSI-X 的区别在于,MSI 支持的中断向量数量有限,而且通常是连续编号的;MSI-X 则通过一张中断消息表,允许每个 DMA 队列都拥有独立的中断向量。这对现代多队列设备非常重要。

举个具体场景。NVMe SSD 通常支持多个 IO 队列,每个队列可以分配独立的 MSI-X 中断。驱动可以把队列 0 的中断绑定到 CPU0,队列 1 的中断绑定到 CPU1,以此类推。这样某个队列的 DMA 完成,只会打断对应的 CPU 核,其他核完全不受影响。在 AI Infra 里调存储和网络性能时,这是最先要检查的事之一:cat /proc/interrupts,看看各个中断是不是均衡地分散在多个核上,而不是全挤在 CPU0。

2.3 Linux 内核里的中断处理路径

设备触发 MSI-X 之后,中断并不会直接变成驱动里的一个回调函数,它要经过一套完整的内核路径。

首先是 hardirq 阶段。CPU 进入中断上下文,执行驱动注册的中断处理函数。这个阶段 CPU 处于原子上下文,不能调用可能睡眠的函数,也不能做太耗时的事情。所以大部分驱动在 hardirq 里只做“快速检查 + 清中断 + 调度后续工作”,然后立刻返回。

然后是 softirq 阶段。例如网卡的 NET_RX_SOFTIRQ,在这一阶段处理实际的收包逻辑。softirq 仍然运行在中断上下文中,但允许更多的处理逻辑,比如把 skb 交给协议栈。

第三种是 threaded IRQ。有些驱动用 request_threaded_irq 注册一个线程化的中断处理函数,这样中断处理的大部分工作可以放到内核线程里执行,允许睡眠,也避免长时间关中断。块设备驱动、串口 DMA 驱动经常会用到这种模式。

如果你只是使用设备,不开发驱动,这些细节看起来有点遥远。但一旦你要排查“为什么 DMA 完成后回调延迟那么大”时,就需要确认回调到底是在 hardirq 里被调用,还是经过 workqueue 延迟执行了。有些回调被驱动放到了 workqueue 里,延迟可能达到几十微秒甚至更多,这在 AI Infra 的高吞吐路径上是不可接受的。

2.4 中断合并与 NAPI:别让 CPU 被“喊”到崩溃

中断虽然高效,但也不是免费的。每个中断都会带来一次上下文切换和 cache 抖动。如果设备每完成一次 DMA 就发一个中断,在每秒几十万次请求的负载下,CPU 可能把大量时间都花在处理中断上,而不是真正处理数据。

于是就有了中断合并(interrupt coalescing)机制。设备不是完成一次 DMA 就立刻发中断,而是积累一批完成事件后再发,或者等一个超时窗口再发。代价是平均延迟上升,但 CPU 开销大幅下降。NVMe 驱动里可以通过配置控制中断合并阈值,网卡驱动里也有类似参数。

网卡收包路径还有一个更复杂的优化,叫 NAPI。它的工作模式很有意思:一开始网卡收到第一个包,DMA 到内存后触发中断,驱动中断处理函数发现收包量大,就进入轮询模式,关闭中断,持续从 DMA ring 里取包;等到连续一段时间没有新包,再重新开启中断。这套“中断转轮询”的机制很好地兼顾了空闲时低 CPU 占用和繁忙时高吞吐低延迟两个目标。DMA 完成通知在 NAPI 模型里,其实已经不再是单纯的中断,而是和轮询深度结合了。

3. 轮询路线:CPU 如何“盯着看”

中断不是银弹。很多场景下,设备高频完成 DMA,频繁触发中断导致的 overhead 反而成为瓶颈。这时很多人会转向轮询。但轮询有讲究:不是让你去死读 MMIO 寄存器,而是轮询内存里的完成队列。理解这一点,才算真正懂得 DMA 完成通知的设计。

3.1 什么时候该放弃中断、改成轮询

判断依据很简单:单位时间内 DMA 完成事件的频率有多高。如果每秒钟几千个请求,中断完全没问题,来了就处理,空闲时 CPU 可以干别的。但如果是每秒钟几十万甚至上百万个请求,中断的开销就会被放大。

我见过一个典型案例。某个 AI 推理服务里,GPU 和 CPU 之间做张量搬运,底层驱动用的是中断通知。结果在模型并行通信加大的时候,CPU 的 softirq 占用直接飙到 80%,业务代码反而抢不到时间片。后来把驱动改成轮询模式,CPU 在数据搬运时保持一个核满负荷轮询,但整体吞吐上去了,业务侧延迟也更稳定。原因很简单:中断通知在高频场景下,把“本来可以连续处理”的工作打碎成了一块块带 overhead 的小任务。

3.2 完成队列和 Doorbell:轮询应该在内存里做

轮询的目标不应该是一个设备状态寄存器,而是一个由设备主动写到普通内存里的结构:完成队列(Completion Queue)。

以 NVMe 为例。CPU 要下发一个 IO 请求,会先写一个命令到 Submission Queue,然后写一次门铃(doorbell)寄存器,告诉设备“SQ 里有新命令了”。设备拿到命令,执行完 DMA 传输,把一个完成条目写到 Completion Queue,然后根据配置决定发不发中断。

在整个模型里,CPU 侧有很多手段感知完成。传统路径是设备发 MSI-X 中断,驱动在中断处理里更新 CQ head 指针;轮询路径则是某个内核线程或用户态线程,不停地检查 CQ head 和 CQ tail 是否一致,不一致就说明有新完成条目。

Doorbell 这个词很形象。它不是用来传输数据的,而是用来“按门铃通知对方有货到了”。这套机制把“CPU 告诉设备我发了新命令”和“设备告诉 CPU 我完成了任务”双向的通知都统一成非常轻量的内存写操作,避免了复杂的锁交互。

3.3 用户态驱动为什么偏爱轮询

DPDK、SPDK 这类用户态驱动,几乎清一色选择轮询模式,而且是很极端的 busy polling。它们把网卡或 NVMe 的 DMA ring 映射到用户态,应用进程占住一个物理核,死循环地轮询完成队列,绕过内核进程调度和系统调用。

这样做的好处是延迟极低而且非常稳定。没有中断上下文切换,没有锁竞争,没有调度抖动,CPU 把全部时间都花在检查队列和处理数据上。代价是即使设备完全空闲,这个核也会被轮询循环占满,CPU 使用率始终是 100%。所以用户态驱动的部署策略一般要求机器有富余的核,或者接受“用 CPU 换性能”。

在 AI Infra 里,像高性能存储网关、网络加速层,很多时候会倾向这种方案。但要注意,不是所有业务都适合全轮询:如果流量稀疏且 CPU 核紧张,中断模式可能更经济。

3.4 内核驱动里的 DMA 完成回调实现

内核也提供了类似轮询或“中断+完成”的异步模型。常见的是 struct dma_async_tx_descriptor 里的 callback 机制。驱动把 DMA 描述符交给 DMA engine 驱动,然后提交任务;DMA 引擎完成传输后,在中断处理或轮询逻辑里调用 callback。

下面是典型的 DMA 完成回调注册。

struct dma_async_tx_descriptor *desc; desc = dmaengine_prep_slave_single(chan, buf, len, DMA_MEM_TO_DEV, 0); desc->callback = my_dma_done_callback; desc->callback_param = &priv_data; dmaengine_submit(desc); dma_async_issue_pending(chan);

在 DMA 完成回调里,你可以用一个 completion 变量唤醒等待的进程。

static void my_dma_done_callback(void *param) { struct my_priv *priv = param; complete(&priv->done); }

调用方进程则可以在等待时选择睡眠还是自旋。如果对延迟要求高,就用 wait_for_completion_timeout;如果可接受做点别的,就把进程塞进队列等回调唤醒。这里面有个细节需要注意:callback 可能在中断上下文里执行,也可能在 tasklet 或内核线程里执行,所以回调函数里不能随便睡眠,也不能拿普通互斥锁。

4. AI Infra 场景里的 DMA 完成通知实践

前面讲的是通用机制,现在回到 AI Infra 本身。无论是大规模训练还是推理服务,底层存储、网络和异构计算之间的数据搬运全靠 DMA,而完成通知的效率和稳定性,直接影响端到端性能。这一章我结合几个常见场景展开。

4.1 存储链路:NVMe 的 SQ/CQ 和中断配置

AI 训练必然涉及 checkpoint 读写和数据集加载。这些 IO 大部分落在 NVMe SSD 上。NVMe 原生就是多队列模型,每个队列有独立的 Submission Queue 和 Completion Queue,并且支持独立的 MSI-X 中断。

实际调优时,我建议先检查几个点。第一是队列数量和 CPU 核数的对应关系,最好一个物理核对应一个 IO 队列;第二是中断亲和性,把每个队列的 IRQ 绑定到对应核;第三是中断合并策略,如果目标负载是大量小 IO,中断合并阈值高了会明显增加延迟,阈值低了又可能造成中断风暴。这块没有标准答案,需要通过压测来权衡。

在 Linux 里可以用 nvme cli 查看队列信息和中断信息。cat /proc/interrupts里能看到每个 NVMe 队列对应的中断号、中断次数所在的 CPU 列。如果发现所有中断都在同一个 CPU 上,说明 IRQ affinity 配置没生效,DMA 完成通知的性能会受限于单核处理能力。

4.2 网络收包路径:从 DMA 完成到协议栈

分布式训练里,网络收包路径的 DMA 完成通知直接影响通信库的性能。网卡收到数据后,通过 DMA 把数据放到 host 内存,再通过中断通知驱动。传统的网卡驱动每个包都会触发一次中断,这在大流量下会产生严重的中断风暴。所以现在主流方案都是 NAPI 加中断合并。

调优网卡 DMА 完成通知时,可以关注 ethtool 的相关参数。比如 rx-usecs 控制设备在收到包后延迟多久产生中断,rx-frames 控制积累多少个包后产生中断。AI 分布式训练里的消息往往比较大,适当提高 rx-frames 可以减少中断次数,但如果消息频率不高,过高的 rx-frames 反而会让小包等很久才被处理,延迟暴涨。

4.3 GPU/NPU 和 CPU 之间的事件同步

异构加速卡内部同样有一堆 DMA 引擎。以 GPU 为例,显存和主机内存之间做拷贝(H2D/D2H),通常由 GPU 内部的 copy engine 或者支持 P2P 的 DMA 控制器完成。拷贝结束后,CPU 端应用怎么知道数据已经可用了?

熟悉 CUDA 的人应该知道 cudaMemcpyAsync 是异步的,后面要配合 cudaStreamSynchronize 或者 cudaEventQuery 等机制同步。这些同步最终都落在“DMA 完成事件”上。底层会把一个完成事件写到显存或系统内存的某个位置,CPU 通过轮询或者驱动事件机制等待。在 NCCL 等通信库里,同样有类似的同步机制,保证跨设备数据搬运完成后才继续下一步计算。

这里能给我们什么启发?在设计自己的 AI Infra 组件时,凡是异步 DMA 搬运,一定要把“完成通知”显式地抽象出来,而不是默默 sleep 一个固定时间再去检查。否则在多设备并行时,要么无谓等待,要么过早读取未完成的数据。

4.4 多队列多中断的绑核与验证

多队列 DMA 设备(NVMe、网卡、部分加速器)都支持把不同队列的 DMA 完成中断分配到不同 CPU。绑核的目的是防止中断在核之间迁移,减少 cache miss 和调度抖动。

操作步骤很简单。先查到设备对应的 IRQ 号,然后写/proc/irq/{irq}/smp_affinity,用十六进制位图指定允许运行在哪些 CPU 上。也可以关掉 irqbalance,避免它自动迁移中断。绑完之后一定要验证,不能绑完就不管。验证方法就是跑压测的同时观察/proc/interrupts里各列中断次数的变化,哪个核的中断次数在持续增长,说明 DMA 完成通知主要落在那个核上。

4.5 从“八股”到实战:面试题到底在问什么

“AI Infra 八股”里经常会出现这个问题:DMA 完成后设备如何通知 CPU?面试官想听的其实不是“中断”两个字,而是完整的性能权衡。你最好能说出中断适合低频事件、轮询适合高频场景;能说出 MSI-X 对多队列的意义;能说出 completion queue 和 doorbell 的配合;甚至能提到 NAPI 这种“中断转轮询”的混合模式。一旦你能把这些点串起来,说明你不仅知道概念,而且理解背后的系统设计。

5. 问题排查与避坑记录

DMA 完成通知机制做得再好,线上也一定会出幺蛾子。这一章整理几个我踩过或者看到过的典型问题,给读者一个速查的思路。每一个问题我都尽量给出定位方法和解决方向。

5.1 网卡驱动报 failed to reset the dma,怎么定位

这个报错在嵌入式平台和部分 PCIe 网卡上并不少见。字面意思很好理解:驱动尝试复位 DMA 引擎,但 DMA 引擎没有在预期时间内回到复位状态。

出现这种情况,首先去dmesg里看报错上下文。如果是在系统刚启动、驱动 probe 阶段报的,常见原因是 SoC 的 DMA 时钟没有被正确使能,或者设备树里的 reset GPIO、复位控制器配置不对。如果是在运行中突然报的,更可能是 DMA 状态机被卡住,比如上一次传输没结束、描述符链异常、或者 DMA 控制器收到非法地址。

排查顺序我建议是:先查时钟和复位,再查电源域,最后查驱动里对 DMA 寄存器的初始化顺序。嵌入式 SoC 里很多外设是共用 DMA 控制器的,如果一个驱动把 DMA 通道配置错了,另一个外设也可能连带报错。这类问题有时候不是驱动本身的 bug,而是资源冲突。

5.2 中断风暴:CPU 明明没干活,中断却把核都打满了

中断风暴的典型现象是:某个 CPU 核的 sys 或 softirq 占用接近 100%,业务吞吐却很低。用cat /proc/interrupts能看到某个中断号的计数值每秒增加几十万次。

常见原因有三类。一是设备默认没有开中断合并,每个 DMA 包都产生中断;二是驱动在中断处理函数里检查队列不及时,让硬件以为 CPU 一直没处理,于是反复触发;三是虚拟化环境下宿主机中断转发逻辑异常。对应的解决方案,网卡侧用 ethtool 开 coalesce,方驱动侧检查 NAPI 是否正常工作,虚拟化侧确认 vCPU 的 local APIC 配置。

5.3 DMA 完成标志读不到或读到旧值

这是最难排查的一类问题,因为现象往往是偶发的。CPU 在中断里读 DMA 完成标志,有时读到 1,有时读到 0;或者标志已经是 1 了,但 DMA 搬运的数据还是旧内容。

原因基本都是内存序和缓存一致性问题。DMA 是异步的,设备写内存和 CPU 读内存之间没有天然的 order,必须由软件显式维护。Linux 内核提供了 DMA API 来保证一致性:使用流式映射时,CPU 在读设备写入的数据前要调用 dma_sync_single_for_cpu;在设备读 CPU 写入的数据前要调用 dma_sync_single_for_device。内核文档把这个叫 DMA 方向处理,顺序反了就会出现莫名其妙的数据损坏。

如果设备支持 PCIe relaxed ordering 或者 No-Snoop,还需要检查设备侧的配置,必要时在驱动里关掉这些特性,换取一致性。

5.4 连续 DMA 请求互相覆盖,完成处理跟不上

做 continuous requests 时,描述符或者 ring buffer 会被复用。如果上一个请求的完成状态还没被 CPU 消费,下一个请求就开始往同一个描述符位置写,就会出现数据覆盖。

我见过一个案例:某个 AI 推理框架的 preprocess 线程和 DMA 搬运线程共享一块固定内存,preprocess 线程在 DMA 完成回调还没执行前,就提前复用了这块内存,结果模型输入偶尔少掉一帧数据。最后靠的是在完成回调里递增一个 sequence number,业务侧等到 sequence number 匹配才复用内存。这个方法简单可靠,推荐在 DMA 频繁复用的代码里都加上。

还有一个和 MMIO 相关的小坑:写完 doorbell 之后,不要立刻去读设备状态寄存器来判断设备是否已经拿到命令。PCIe posted write 虽然不会阻塞 CPU,但它何时到达设备是不确定的,需要借助完成队列来确认。

5.5 常用排查工具速查表

场景用什么看重点关注
中断分布不均cat /proc/interrupts各 CPU 列的中断次数
网络收包中断风暴ethtool -S eth0mpstat -P ALL 1softirq 占比、rx 中断计数
NVMe IO 队列nvme listcat /proc/interrupts队列数与 IRQ 对应关系
DMA 映射/一致性错误dmesg、KASANIOMMU 错误、dma_debug 输出
驱动调用路径perf trace、ftrace、bpftrace中断回调延迟、workqueue 排队

调试 DMA 完成通知时,我的习惯是先看中断分布,再看软中断占比,最后才深入驱动代码。很多问题其实在/proc/interrupts这一层就已经暴露了:哪个核中断过多、哪个中断号异常增长,都能直接看到。

DMA 完成通知看起来是个小机制,但它决定了 AI Infra 里每一次存储读写、每一次网络收包、每一次 GPU 和 CPU 之间的数据交换能不能低延迟、高效率地完成。从 INTx 到 MSI-X,从中断到轮询,从 doorbell 到 completion queue,每一层设计都是在和延迟、CPU 开销做权衡。实际调优时没有银弹,先把机制吃透,再根据负载特点选择中断还是轮询、合并不合并、绑核绑几个,才不会在线上数据量一上来时被打个措手不及。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询