本文是《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_NONE | IRQ_NONE | 0 | 进入拒认计数,十万次后禁用 |
IRQ_NONE | IRQ_HANDLED | 1 | 认为"有人管了",你的拒认消失 |
IRQ_NONE | IRQ_WAKE_THREAD | 2 | 同上 |
判据是确定的,不是"有时候":共享线上有 N 个 handler,只要其中 N−1 个正常,剩下那 1 个的持续拒认永远进不了统计判定。共享线在真实系统里并不罕见——PCI 传统 INTx 线、ISA 线,以及大量"内核自带驱动已经先占了这条线"的场景都算。
把这条判定链完整画出来——返回值走出 handler 之后,会经过什么、被谁看见:
图里最容易被忽略的是右下那格:它不是"报得慢",而是没有暴露通道——日志加不出来,你不能给内核的统计层加计数,判定只能前移到代码审查。底部那条出口与线型无关:返回值落在三个枚举之外(最常见的来源是忘了写return),会被识别为bogus return value,最多报 100 次后限流静默(kernel/irq/spurious.c:227)。
把视角沉到源码层——上一张图里那句"位或聚合",在分发链上的位置是这样:
链上每一环都有行号可查。值得停一停的是倒数第二环:handler 执行完后,内核先做逐项判定(越界、WAKE_THREAD 检查),然后才是那一行位或——你在 handler 里返回的值,只是聚合的一个输入。
可操作:今天就能查的四条
- 先数线上有几个 handler:
grep -E "^ *[0-9]+:" /proc/interrupts,看目标 IRQ 那一行出现了几个名字。有邻居,就不要再等日志。 - 检查返回值有没有依据:handler 里若没有任何寄存器读取、或无条件返回固定值,直接标红——
if (!(status & MASK)) return IRQ_NONE;与if (status & MASK) return IRQ_NONE;一字之差、语义相反,编译器不会提醒你。 - 给拒认分支自建计数:内核不提供"某个 handler 拒认了多少次"这个计数器,要让它在运行期可见,就得在模块里自己建。做法是一个
atomic_t,配一个 debugfs 或/proc入口——不要写成每次拒认都pr_info,高频线上会把 dmesg 刷爆。 - 只用
/proc/interrupts判断 handler 是否工作,是错的:它的计数归整条线所有。
前三条查完,你大概会想知道:那套"十万次才报"的统计到底怎么算的?为什么是十万次?
没展开的部分
虚警统计本身还有一整套阈值与两个计数:为什么是十万次、99.9%占比、100ms窗口;/proc/irq/<N>/spurious里irq_count与irqs_unhandled两条计数的读法正好相反,混用会算出完全错误的结论。这些本文没有展开。
下一篇
下一篇就聊这三个数怎么来的:nobody cared为什么非等十万次,以及那条容易读反的计数——很多人以为"早就该禁用了",其实那个计数一直在被悄悄重置。
回复「交流」,我很乐意分享更精彩的内容。
感谢阅读。转载请注明出处与作者。