ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

同一句 IRQ_NONE,独占线报警,共享线静默

同一句 IRQ_NONE,独占线报警,共享线静默

本文是《request_irq 成功却收不到中断》系列第 1 篇。

一条 IRQ 线上挂着两个设备。其中一个的 handler 里,状态位的掩码抄的是旧版手册:

irqreturn_tmy_handler(intirq,void*dev_id){u32 status=readl(dev->regs+STATUS_REG);if(!(status&MY_INT_MASK))/* 掩码来自旧版手册 */returnIRQ_NONE;/* 每次都说"不是我的" */...}

这个设备从此再没处理过一次中断。奇怪的是 dmesg 干净得像没出过事:没有nobody cared,没有Disabling IRQ,/proc/interrupts里那一行的计数还在稳定上涨。

如果这条线上只有它一个设备,同样的 bug 会在十万次中断之后被内核点名:

irq 42: nobody cared (try booting with the "irqpoll" option) Disabling IRQ #42

同一份 handler,同一个 bug,换一条线,结局两样——独占线上等得到内核开口,共享线上永远不会被点名。

(场景由反复出现的典型故障模式归纳,非具体设备记录;下文机制均可核对。)

第一反应,基本都是错的

面对"设备不工作但日志干净",第一反应通常是内核总该发现没人处理吧。

这个判断在独占线上成立,在共享线上不成立——它不是一个"报得慢"的问题,而是一条根本不存在的上报通道:

线型同类 bug 的暴露形式
独占十万次后nobody cared+Disabling IRQ #N
共享(同线有别的 handler 在认领)🚨 无日志、无计数、无状态位

一句反证:/proc/interrupts里那一行的计数是整条线的、按 CPU 分列,行名只是 handler 的名字,不是它的命中次数(kernel/irq/proc.c:498-501)。所以它一直在涨,看起来"很正常"——那是邻居的贡献。

真正的原因:检测看的是聚合值,不是你的返回值

内核的虚警检测入口是note_interrupt()(kernel/irq/spurious.c:272)。它不在每个 handler 返回时被调用,而是在整条线的所有 handler 都跑完之后才被调用一次,拿到的是聚合结果(kernel/irq/handle.c:200-201)。

聚合只有一行(kernel/irq/handle.c:185):

retval|=res;/* 把每个 handler 的返回值并进来 */

三个返回值的位表示决定了这一行怎么工作(include/linux/irqreturn.h:12-14):IRQ_NONE= 0,IRQ_HANDLED= 1,IRQ_WAKE_THREAD= 2。位或的后果是——只要有一位是 1,整体就非 0。

而拒认计数的门槛写在统计层(kernel/irq/spurious.c:385):只有聚合值恰好等于IRQ_NONE(也就是全链都拒认)时才累加。把两者拼起来看:

你的返回邻居的返回聚合值统计层看到什么
IRQ_NONEIRQ_NONE0进入拒认计数,十万次后禁用
IRQ_NONEIRQ_HANDLED1认为"有人管了",你的拒认消失
IRQ_NONEIRQ_WAKE_THREAD2同上

判据是确定的,不是"有时候":共享线上有 N 个 handler,只要其中 N−1 个正常,剩下那 1 个的持续拒认永远进不了统计判定。共享线在真实系统里并不罕见——PCI 传统 INTx 线、ISA 线,以及大量"内核自带驱动已经先占了这条线"的场景都算。

把这条判定链完整画出来——返回值走出 handler 之后,会经过什么、被谁看见:

图里最容易被忽略的是右下那格:它不是"报得慢",而是没有暴露通道——日志加不出来,你不能给内核的统计层加计数,判定只能前移到代码审查。底部那条出口与线型无关:返回值落在三个枚举之外(最常见的来源是忘了写return),会被识别为bogus return value,最多报 100 次后限流静默(kernel/irq/spurious.c:227)。

把视角沉到源码层——上一张图里那句"位或聚合",在分发链上的位置是这样:

链上每一环都有行号可查。值得停一停的是倒数第二环:handler 执行完后,内核先做逐项判定(越界、WAKE_THREAD 检查),然后才是那一行位或——你在 handler 里返回的值,只是聚合的一个输入。

可操作:今天就能查的四条

  1. 先数线上有几个 handler:grep -E "^ *[0-9]+:" /proc/interrupts,看目标 IRQ 那一行出现了几个名字。有邻居,就不要再等日志。
  2. 检查返回值有没有依据:handler 里若没有任何寄存器读取、或无条件返回固定值,直接标红——if (!(status & MASK)) return IRQ_NONE;与if (status & MASK) return IRQ_NONE;一字之差、语义相反,编译器不会提醒你。
  3. 给拒认分支自建计数:内核不提供"某个 handler 拒认了多少次"这个计数器,要让它在运行期可见,就得在模块里自己建。做法是一个atomic_t,配一个 debugfs 或/proc入口——不要写成每次拒认都pr_info,高频线上会把 dmesg 刷爆。
  4. 只用/proc/interrupts判断 handler 是否工作,是错的:它的计数归整条线所有。

前三条查完,你大概会想知道:那套"十万次才报"的统计到底怎么算的?为什么是十万次?

没展开的部分

虚警统计本身还有一整套阈值与两个计数:为什么是十万次、99.9%占比、100ms窗口;/proc/irq/<N>/spurious里irq_count与irqs_unhandled两条计数的读法正好相反,混用会算出完全错误的结论。这些本文没有展开。

下一篇

下一篇就聊这三个数怎么来的:nobody cared为什么非等十万次,以及那条容易读反的计数——很多人以为"早就该禁用了",其实那个计数一直在被悄悄重置。


回复「交流」,我很乐意分享更精彩的内容。

感谢阅读。转载请注明出处与作者。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进