news 2026/9/17 6:05:43

DMA完成通知CPU的机制:从硬件链路到驱动工程实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
DMA完成通知CPU的机制:从硬件链路到驱动工程实践

1. 先说结论:DMA干完活,靠“敲门”通知CPU

DMA(Direct Memory Access,直接内存访问)这个技术,在 AI Infra 里几乎是躲不开的。GPU要从主机内存拉权重、NVMe SSD要把模型数据读进内存、网卡要把远端数据搬进本地缓冲区——这些高速数据搬运,如果每一步都让CPU亲自来做,CPU早就被IO操作拖垮了。所以DMA控制器的角色,就是替CPU打工的“搬运工”:你告诉它数据从哪里来、到哪里去、搬多少字节,它就自己吭哧吭哧搬完,搬的时候CPU该干嘛干嘛。

但这里有个特别实际的问题:搬运工干完了活,怎么告诉老板“我搞定了”?如果靠CPU用轮询的方式不停地去问“搬完了没”,那DMA解放CPU的意义就大打折扣——CPU还是被绑在等待上了。真正的答案是:DMA控制器在传输完成后,会通过中断(Interrupt)通知CPU。这就像你点了个外卖,外卖员到了不会站在楼下干等你主动探头去问,而是直接按门铃,门铃响了你就知道“饭到了”。中断就是硬件给CPU按的那个门铃。

这篇东西我打算从硬件链路一路讲到驱动代码,把“DMA完成后如何通知CPU”这个机制的里里外外掰开揉碎,再结合AI Infra场景说说为什么这个机制选择直接影响系统吞吐。适合正在搞AI基础设施、做驱动开发、或者对高性能计算IO路径感兴趣的同学,即使你暂时不写驱动,理解了这套机制,对排查“CPU占用高但性能上不去”这类问题也特别有帮助。

提示:轮询不是完全被淘汰了。在极低延迟、单设备、数据量极小的场景下,轮询甚至比中断更高效。但绝大多数AI Infra的批量数据搬运场景,中断机制是绝对的王者。下面我细讲。

2. 从DMA控制器到CPU的完整通知链路

2.1 DMA控制器的三个关键角色:搬运工、状态旗、门铃按钮

DMA控制器本身就是一个小的状态机。我们以最常见的存储器到存储器搬运为例,看看一次完整的过程:

  1. CPU配置DMA控制器的源地址寄存器、目的地址寄存器、传输长度寄存器,然后往控制寄存器里写一个“启动”位。
  2. DMA控制器开始逐字节或者逐块地从源地址读数据、写到目的地址。这个过程里CPU完全不用参与。
  3. 搬运完成后,DMA控制器内部的状态寄存器中,会有一个“传输完成(Transfer Complete)”标志位被硬件自动置1。
  4. 同时,DMA控制器的中断状态寄存器里,对应的中断挂起位也会被置位。如果之前配置了“中断使能”,那么DMA控制器就会在它的中断请求引脚上拉出一个有效的电平或者边沿信号——这就是触发中断的“电信号敲门”。

你可以把DMA控制器的中断请求引脚理解成门铃按钮。DMA搬完了,按钮被按下,然后门铃线路上就会有一个电信号往CPU方向传。关键点是:这个电信号不是直接进CPU的,它要先经过一个中断控制器。

2.2 中断控制器:扮演“总机接线员”的GIC/APIC

在x86平台,这个总机叫做APIC(Advanced Programmable Interrupt Controller);在ARM平台,它叫GIC(Generic Interrupt Controller)。名称不同,职责高度一致:

  • 收集各个外设发来的中断请求。
  • 根据优先级仲裁,决定先给CPU报哪个。
  • 记录中断号,方便CPU在响应时快速识别是哪个设备在敲门。
  • 在ARM GIC的语境下,还要把中断分发给具体哪个CPU核。

这里有一个容易出现理解偏差的地方:CPU并不是“中断一来就放下手里所有事直接跑过去”。实际情况是,中断控制器把信号送到CPU的某个核上,CPU执行完当前正在执行的指令之后,先去保存现场(把当前任务的寄存器状态压栈),然后根据中断向量表跳转到对应的中断处理程序(ISR)去执行。ISR读取DMA控制器的中断状态寄存器,发现是“传输完成”,就做相应的善后工作——标记传输完成、唤醒等待的进程、清中断标志位。

这个“总机接线员”角色的存在,解决了一个很实际的问题:当一个系统里有几十个设备都可能产生中断时,CPU不可能给每个设备都留一个直接的引脚,那样CPU的引脚根本不够用。中断控制器帮CPU做了一个“先统一收线、再转接”的抽象,让CPU只需要面对一个中断入口。

2.3 中断亲和性:多核CPU时代,谁去开门?

在AI Infra的服务器里,一颗CPU有几十个核,GIC/APIC把中断分发给哪个核,直接影响性能走向。默认情况下,中断可能会被分发给任意一个核,但这样会导致缓存局部性变差——上次处理这个设备中断的核是CPU0,这次变成了CPU3,那CPU0缓存里关于这个设备的状态全部失效了。

所以实际工程里,我们通常会给特定的DMA相关中断绑定固定的CPU核,也就是“中断亲和性(IRQ Affinity)”。比如在Linux下,可以通过修改/proc/irq/<irq_number>/smp_affinity文件,用十六进制掩码指定这个中断只发给CPU2和CPU3。对AI训练服务器来说,如果GPU数据搬运的中断始终落在同一个核上,那么该核在处理完中断后,相关的页表、描述符缓存都会保持在热状态,下一次中断处理就非常快。

3. AI Infra场景下,这个“通知”为什么格外关键

3.1 GPU训练场景的DMA中断:一次训练迭代搬多少数据?

先说个数据量级的概念。以训练一个大型语言模型为例,假设模型权重有70GB,每次迭代都要从主机内存搬运一批数据到GPU显存,哪怕一次只搬几个GB,也是在百万毫秒级别内必须完成的。这个搬运过程的终点是DMA完成中断告诉CPU“数据已经进显存了”,CPU才会去通知GPU“你可以开始算了”。如果这个中断通知链路慢了,GPU就会在那里空转等数据——GPU的计算单元时间非常宝贵,空转一毫秒都是浪费。

另外还有一个常被忽视的点:训练过程中的梯度同步。多卡训练时,每张卡算完梯度后要通过NVLink或者PCIe做梯度聚合,这个过程中DMA完成的确认通知同样起着“发令枪”的作用。后续算子在等待梯度就绪时,靠的就是DMA完成中断去唤醒。可以说,DMA中断延迟直接叠加到每次迭代的耗时上,积累下来就是几个百分点的训练吞吐差距。

3.2 NVMe SSD与网络卡:高IOPS下的中断风暴风险

AI Infra不只有GPU,还有存储和网络。NVMe SSD的IOPS动辄百万级别,网卡在分布式训练里也是每秒钟几十万个小包进进出出。每个IO完成都要来一次DMA中断,如果处理不当,CPU会被“门铃”按到崩溃——这就是工程师们常说的“中断风暴(interrupt storm)”。

为了解决这个问题,硬件和软件都在做优化,这里简单列几个关键思路:

  • 中断聚合(Interrupt Coalescing):DMA控制器或者设备允许攒一批传输完成后再统一报一次中断。延迟稍微增加一点,但CPU中断处理次数大幅下降。
  • 多队列(Multi-Queue):网卡和NVMe控制器支持多个DMA队列,每个队列可以路由到不同的CPU核上处理,把“按门铃”的压力分散到多颗核头上。
  • MSI/MSI-X中断:相比传统的中断线,MSI-X允许设备直接往特定CPU核写一个内存消息来触发中断,定向能力更强,避免了传统共享中断线的排队问题。

这些都建立在“DMA完成会通知CPU”这个基本机制之上。理解了这个根本机制,再去看厂商的各种调优参数,就不会觉得玄学了。

3.3 异构计算下的等待机制:事件同步与信号量

在CUDA编程里,cudaMemcpyAsync是异步的——它发起DMA传输后立即返回,CPU可以继续做别的事。但当你调用cudaStreamSynchronize时,CPU就会进入等待状态,直到DMA传输完成事件到来。这个事件的背后,本质上就是DMA完成中断触发了驱动里的回调逻辑,进而唤醒等待队列里的CPU线程。

这里我想强调一个经验之谈:对AI框架做性能分析时,如果发现CPU的等待占比很高,不要只盯着算子本身,用perf或者py-spy看看线程是不是阻塞在wait_event这类调用上。如果阻塞时间很长,大概率是DMA完成中断没有及时送达,而不是计算太慢。

4. 落到代码:驱动里怎么实现“等待干完”的逻辑

4.1 从轮询到中断:写代码的视角转换

我们先从代码的视角感受一下两种方式的区别。早期或者极简场景下的DMA轮询写法大概是这样:

// 设置DMA寄存器... dma_start(); // 轮询状态寄存器,直到完成位被置位 while (!(readl(dma_base + DMA_STATUS) & DMA_STATUS_COMPLETE)) { cpu_relax(); } // 搬运完成,开始处理数据 process_data();

这种方法简单粗暴,但CPU在等待期间什么事都干不了。虽然cpu_relax()会让出流水线,但线程还是占着这个核,整体的CPU利用率看起来不高,但系统吞吐也被拖住了。

换成中断驱动模式之后,逻辑被拆成了两半:发起者不傻等,而是“我先睡,你干完了叫我”;中断处理函数干“叫醒”的活:

// 发起DMA传输 static irqreturn_t dma_done_handler(int irq, void *dev_id) { struct my_dma_device *dev = dev_id; // 读状态寄存器,确认是这个传输完成的中断 u32 status = readl(dev->base + DMA_STATUS); if (status & DMA_STATUS_COMPLETE) { // 清中断标志位,避免中断反复触发 writel(DMA_STATUS_COMPLETE, dev->base + DMA_STATUS_CLEAR); // 唤醒等待队列中的进程 dev->done = 1; wake_up_interruptible(&dev->wait_queue); } return IRQ_HANDLED; }

发起者那边的逻辑就解脱了:

struct my_dma_device *dev = ...; // 配置源、目的、长度 writel(src_phys_addr, dev->base + DMA_SRC); writel(dst_phys_addr, dev->base + DMA_DST); writel(len, dev->base + DMA_LEN); // 使能完成中断,启动传输 writel(DMA_CTRL_START, dev->base + DMA_CTRL); // 睡眠等待,中断来了wake_up就继续往下走 wait_event_interruptible(dev->wait_queue, dev->done); dev->done = 0;

这样,CPU在线程里等待时可以被调度器切走,去运行别的任务,等DMA干完了,中断一来,这个线程再被唤醒。对整个系统来说,CPU资源的利用率高了一个级别。

注意:wait_event_interruptible 返回后,最好重新检查一下条件是否真的成立了。因为可能被信号打断,返回时dev->done还是0。实际驱动里通常用循环包一层,或者用wait_event_interruptible_timeout带一个超时保护,防止硬件异常时进程永远睡死。

4.2 中断处理函数为什么不能做“重活”

这里有个很多新手容易踩的坑:在中断处理函数里做了太多事情。比如读出来的数据很大,直接在ISR里做内存拷贝;或者在ISR里调用了一个可能睡眠的函数。这都是大忌。

中断处理函数运行在原子上下文中,期间当前CPU核上高优先级的中断被屏蔽,任何调度、睡眠、动态内存分配(GFP_KERNEL)都是不被允许的。正确做法是ISR里只做最必要的事:确认中断来源、清中断标志、唤醒等待者。如果确实需要做复杂善后,就用Linux的tasklet或者workqueue把重活推到下半部去执行。

我们这个“DMA完成通知CPU”的场景,因为通知完的主要动作就是唤醒一个进程,所以在上半部直接做掉没有任何问题。但如果你的驱动在DMA完成后还需要对数据做校验和、再做一次较小的搬运,务必考虑下半部机制,免得让中断路径过长,影响其他时间敏感的设备。

4.3 request_irq 的正确姿势

注册中断处理函数是初始化阶段必须做的。这里有几个我实践下来觉得特别值得注意的点:

int ret; // 拿到DMA设备对应的IRQ号,可能是设备树里指定的,也可能是PCIe 的MSI-X申请来的 int irq = platform_get_irq(pdev, 0); ret = request_irq(irq, dma_done_handler, IRQF_TRIGGER_HIGH, // 由硬件触发方式决定 "my_dma_device", dev); if (ret) { dev_err(&pdev->dev, "failed to request irq %d, ret=%d\n", irq, ret); return ret; }

第一个坑是handlerdev_id的匹配。如果你的驱动注册了多个设备,或者同一个中断号上挂多个设备,dev_id是用来区分的,所以通常传递设备结构体指针。中断处理函数的第一个参数irq只是告诉你哪个中断号发生了,具体是哪个设备、哪个DMA通道,都要靠dev_id指向的结构体里的寄存器去判。

第二个坑是共享中断。有些设备不支持独立的MSI-X中断,只能和别的设备共用一个中断号。这种情况下,request_irq必须带IRQF_SHARED标志,且中断处理函数里如果发现不是自己的设备产生的中断,必须返回IRQ_NONE。如果忘了带IRQF_SHARED,申请就会失败。

第三个坑是中断的触发方式。边沿触发和电平触发对中断处理的要求完全不一样。边沿触发有锁存机制,如果一个边沿没被及时响应,可能会丢。电平触发只要不解除电平,中断就会反复进入处理函数,容易导致风暴。用DMA设备时,通常芯片手册会给明确的推荐配置,照着配就好;如果是FPGA自己做的DMA设备,和硬件工程师对齐触发方式格外重要,软件上配置错了,表现出来的症状可能非常诡异。

4.4 DMA描述符与完成回调:硬件抽象的进阶视角

现代高性能DMA设备(比如NVMe控制器、GPU、智能网卡)早已不是CPU直接配置一堆寄存器这么简单了。它们普遍采用描述符环(Descriptor Ring)的方式:驱动在内存里维护一个环形队列,每个描述符里写清楚源地址、目的地址、长度、标志位;硬件自动从队列里取任务,一个个执行,完成后写回描述符里的状态字段,然后产生一个完成中断。

在这个模型下,“DMA干完了”的通知就更加高级了。驱动不再为每个传输逐一注册回调,而是在中断处理函数里遍历完成队列,批量处理所有已经完成的请求。这大大提高了高IOPS场景的效率。以Linux的io_uring或者NVMe驱动为例,一次中断过来,可能同时有几百个请求完成了,驱动轮一遍完成队列,挨个唤醒对应的等待者或者回调函数。

这个思路在AI Infra里也用得淋漓尽致。GPU驱动、RDMA网卡驱动、DPU(数据处理单元)驱动,几乎都是这种描述符加完成队列的架构。理解了这个模型,你就能明白为什么中断处理函数里不建议做重活——因为一次中断可能要处理海量的完成事件,再把耗时的数据不做规划地丢进去,整个系统的IO路径就会变得又长又慢。

5. 实际调优经验与排查锦囊

5.1 中断风暴:DMA完成中断频率过高怎么办

一个典型的症状是系统负载不高,单看CPU占用率也不满,但你用top看一眼会发现某个进程CPU时间明明不多,系统整体却卡卡的;用cat /proc/interrupts看一下,某个中断号的计数在疯狂上涨。这就是典型的DMA完成中断频率过高。

我遇到过一个真实案例:某次在调一块FPGA加速卡的驱动时,因为配置错了DMA控制器的中断合并阈值,原本应该攒够64个描述符才报一次中断,结果一个描述符完成就报一次,导致中断频率直接高了几十倍。处理方法是修改阈值寄存器,把中断聚合打开,让中断频率降到合理的范围。**实战中判断“合理范围”的一个简单标准:中断带来的CPU开销不要超过设备吞吐带来收益的1%左右。**如果网卡跑50Gbps流量时,单核的软中断处理已经吃满了一整颗核,那一定是要做中断聚合或者多队列分发的。

5.2 中断迟迟不来:问题排查链路顺序

假设DMA搬运没有完成,中断也一直没来,代码卡在wait_event里不动了。我建议按这个顺序查:

  1. 先看硬件寄存器:DMA控制器的状态寄存器里,搬运是否已经完成?传输完成位是不是已经置1了?如果硬件层面已经完成了,那大概率是“中断没传出来”,检查中断使能位和中断状态寄存器。
  2. 确认中断支配:在中断处理函数入口加一行printk或者trace_printk,看看到底有没有进中断。如果没进,检查中断控制器层面的使能和屏蔽配置(GIC/APIC的相应寄存器),以及设备树或者ACPI表里中断号的映射对不对。
  3. 确认中断号和设备匹配:用cat /proc/interrupts看各个中断号的计数,对比设备到底是哪个IRQ。如果设备申请的IRQ号和实际硬件连接的中断线不一致,也会出现“谁来敲门都敲不到我家”的窘境。
  4. 确认等待条件没被错误消费wait_event条件变量是共享的,可能在中断来之前就被别的地方改成了1,导致进程提前醒来却以为传输完成,然后继续等待,或者反过来。查代码里有没有别的地方写这个标志位。

另外,irqdev_id那块的坑也要复盘。共享中断下如果自己的ISR返回了IRQ_HANDLED导致别的设备中断没识别,也可能让某个设备的完成信号被吞掉。这时把ISR里返回IRQ_NONE的态度要严谨。

5.3 多队列设备与中断亲和性配置实战

对AI服务器来说,多队列设备的配置是绕不开的一环。以主流网卡为例,每个队列都会被分配一个MSI-X中断,每个中断可以绑定到不同的CPU核上。手动配置时需要注意几个点:

  • lspci -vvv查看设备支持的MSI-X中断向量数量,一般会对应队列数。
  • 找到每个队列对应的IRQ号之后,通过写smp_affinity掩码来绑定CPU。比如掩码是0x30,代表IRQ只发给CPU4和CPU5。
  • 配置完记得用cat /proc/interrupts验证一下,确认中断处理计数是否持续增长,而且增长的那一栏确实是绑定的那个核。

这里有个很容易被忽略的细节:NUMA架构下,中断处理核最好和DMA设备所在的PCIe控制器在同一个NUMA节点。跨NUMA访问内存会有额外的延迟和带宽损失,对AI训练这种对延迟敏感的场景影响尤其明显。可以通过lstopo命令看看设备挂在哪颗CPU的PCIe root上,然后把该中断绑到这颗CPU的本地核上。

5.4 中断处理函数的“慢性子”排查法

当某次DMA完成中断处理时间很长时,系统的其他中断响应也会被拖慢。Linux里排查中断处理耗时常用的手段是ftrace里的irqsoffpreemptirqsoff追踪器:

# 开启中断关闭追踪 echo 0 > /sys/kernel/debug/tracing/tracing_on echo irqsoff > /sys/kernel/debug/tracing/current_tracer echo 1 > /sys/kernel/debug/tracing/tracing_on # 等一段时间或者复现问题后 cat /sys/kernel/debug/tracing/trace

看到最大中断关闭时间的调用栈后,基本就能锁定位高耗时的ISR。老实说,这种问题一般是驱动写得糙导致的,比如在ISR里加锁、做耗时循环、或者调用了会睡眠的接口。把重活挪到下半部之后,中断响应时间会立刻好看很多。

6. 几个我踩过的坑,写成给你们避雷

关于“DMA完成后怎么通知CPU”这个话题,我最后聊聊自己实际调试过程中的几个体会。

第一个坑是清中断标志的时机。有些DMA控制器要求在中断处理函数里,先读取数据、再清标志;有些则要求先清标志、再读数据。顺序搞反了,轻则多触发几次误中断,重则直接丢中断。我建议拿到芯片手册后,先把这个时序弄清楚,最好在写ISR之前,用硬件的loopback模式单独测一下中断路径,不要等到系统联调时再排查,那样变量太多了。

第二个坑是共享中断和自定义的MSI中断不要混在一起处理。共享中断时无法直接从IRQ号反推设备,必须读自己的设备寄存器来判定;MSI中断则通常和设备一一对应,可以放心用IRQ号做查表。这两种处理逻辑写在一起,代码容易绕死,而且最后出bug时特别难查。

第三个坑是wait_event和超时的配合。AI加速卡在异常时有时会出现DMA控制器挂死的情况,如果驱动的等待没有超时保护,系统会一直卡在等待队列里,有些看门狗机制根本救不回来。我的习惯是任何DMA等待都加上超时,超时后至少打印一条寄存器快照日志,方便事后分析是在哪个环节卡的。

最后还有个心得:排查DMA中断问题,最有效的工具不是示波器,而是/proc/interrupts加上trace-cmd。先看中断计数有没有涨,再看源头的驱动有没有进ISR,最后再怀疑硬件。**这个从统计到代码、再到电气信号的排查顺序,能帮你在毫无头绪时稳住阵脚。**调试过程中记得多留打印,哪怕临时加的printk显得代码很丑,也比两眼一抹黑强得多。等问题定位清楚了,再把调试代码删除也来得及。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/17 6:05:07

iOS大文件音视频导入原理与实战指南

1. 项目概述&#xff1a;为什么iOS上导入大容量音视频会让人抓狂&#xff1f;“iOS导入大容量音视频用什么APP&#xff1f;文件处理能力横评”——这句话背后&#xff0c;藏着成千上万普通用户、内容创作者、自媒体剪辑者、教育工作者甚至小型影视团队的真实困境。不是他们不会…

作者头像 李华
网站建设 2026/9/17 6:04:56

100G FPGA UDP上板测试:系统级压力验证实战指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/17 6:04:16

ESP32双模选型避坑指南:Wi-Fi与蓝牙共存的真实边界

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/17 6:02:13

OSPF高级配置实战:从邻居状态机到DR选举与多区域设计

简介&#xff1a;一份面向网络工程师及网络技术学习者的OSPF高级配置PPT学习教案&#xff0c;围绕动态路由协议OSPF在实际网络中的进阶应用展开&#xff0c;重点讲解NSSA区域&#xff08;含no-summary参数与LSA 7处理&#xff09;、地址汇总命令、辅助地址的配置规则与应用场景…

作者头像 李华
网站建设 2026/9/17 6:01:08

木鸟短租网爬虫课设:requests+BeautifulSoup+pandas全流程实战

简介&#xff1a;面向大学生数据采集与预处理课程设计的完整项目资料&#xff0c;以木鸟短租网为实战对象&#xff0c;内含爬虫源码和课程设计报告&#xff0c;重点展示requests、BeautifulSoup、Scrapy、Selenium等技术的应用&#xff0c;覆盖数据采集、反爬应对、清洗与预处理…

作者头像 李华
网站建设 2026/9/17 6:00:16

中望CAD机械制图实战:法兰图贯穿国标图层、构造逻辑与参数化图库

简介&#xff1a;本资源是一份面向CAD初学者与机械制图从业者的中望CAD系统入门教程&#xff0c;聚焦工程制图核心能力培养&#xff0c;尤其适用于法兰类标准件的规范绘制与标注实践。教程内容结构清晰、步骤详实&#xff0c;覆盖图框设置与信息栏填充、法兰主视图轮廓构建&…

作者头像 李华