ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

一文讲透Linux中断机制:从硬件信号到handler的完整链路与实战

2026/9/8 0:55:11 拓冰建站 浏览量
一文讲透Linux中断机制:从硬件信号到handler的完整链路与实战 第一次调通的Linux中断我才算摸到内核的门槛如果你问我在嵌入式Linux开发里什么东西最让人又爱又恨我一定首选中断机制。说爱是因为几乎所有的外设都靠中断来通知CPU“我这里有活干了”——网络包到了、按键按下了、串口来数据了、DMA搬运完了全是中断。只要跑Linux中断就无处不在。说恨是因为它真的很绕一个中断从硬件引脚拉起来到你的驱动回调函数被调用中间隔着GIC、irq domain、irq descriptor、中断线程化、下半部机制这么多层随便卡在哪一环现象就是“设备不干活”或者“CPU傻高”。我第一次调一个触摸屏驱动时中断丢了三天最后发现是设备树里interrupts属性写错了一位数。从那以后我就明白Linux中断机制不是看几篇博客就能拿下的需要把整个链条上的每个环节都摸清。这篇文章把我在内核源码和实际项目里积累的东西整理出来从硬件中断信号产生开始一路讲到你的handler跑起来中间每一层是干什么的、为什么这么设计、有哪些坑尽量写透。先说清楚这篇文章面向的是想真正搞懂Linux中断、正在写驱动或者被中断问题折磨的人。如果你只是背面试题可能用不上这么细但如果你要调板子、调驱动、排查CPU占用异常这篇能帮你省下很多瞎折腾的时间。1. 中断机制的整体设计思路为什么这么绕刚开始看内核中断代码的人基本都会有一个疑惑一个中断来了就来了CPU直接跳到处理函数跑不就完了为什么要搞出 hardirq、softirq、tasklet、workqueue、threaded irq 这么一堆概念答案其实是一句话中断处理上下文里不能睡眠而真实世界的外设处理往往需要睡眠。先把这个核心矛盾讲清楚。1.1 中断上下文为什么不能随便睡当一个中断信号到达CPU时处理器会跳到异常向量表保存现场然后进入内核的中断处理流程。这个过程里CPU 处在中断上下文此时系统处于一个非常时刻——当前被打断的可能是用户程序也可能是内核自己的临界区。如果在中断处理函数里调用了一个会睡眠的函数比如 mutex_lock、msleep、kmalloc(GFP_KERNEL)会发生什么睡眠的本质是让出CPU等条件满足再被唤醒。在正常进程上下文里这没问题调度器会找别的进程跑。但在中断上下文里没有别的进程这个概念——你都不知道该把CPU让给谁而且这个中断本身就是用来打断别人的结果自己先睡了整个系统的节奏就乱了。所以内核有一堆检测机制专门对付这种事最常见的就是BUG: sleeping function called from invalid context at kernel/mutex.c这行log一出现基本就是有人在中断里睡觉了内核直接判死刑。1.2 上半部和下半部的分工逻辑既然中断处理函数里啥都不能干那怎么办延迟处理。这就是经典的上半部top half和下半部bottom half设计。上半部就是中断处理函数本身要求快进快出只做最紧急的事情关中断、读状态、清标志、把数据搬到内存、然后触发下半部。下半部在更宽松的环境里处理真正的业务逻辑串口数据解析、网络协议处理、按键消抖这些累活都在这里完成。为什么要分这么细因为中断处理期间CPU往往处于关闭本地中断的状态具体要看中断标志位和内核配置也就是说这段代码执行的时候其他的中断全部进不来。要是你在中断处理函数里磨蹭几百微秒那感觉就像高峰期的高速路上突然有人停车拍照——后面全堵死了串口丢数据、网络超时、实时任务错过deadline各种诡异的问题都会冒出来。所以下半部的核心价值就是让中断处理函数尽快结束把剩下的活放到可以被打断的环境里慢慢做。而且某些下半部机制允许睡眠这意味着你可以在里面用各种方便的内核API。用个生活化的类比你在厨房做菜火警响了。上半部就是立刻关火、冲出厨房、按下报警器——这是救命的必须立刻做。下半部是呼救、疏散、检查是什么烧了、清理现场——这些事情可以慢慢来甚至可以让消防员内核线程来做。你要是非在警报响的时候把所有菜都炒完再跑那房子早烧没了。1.3 一个中断要对应一套策略不是一成不变这里要提个关键认知不同中断源的应急程度是完全不同的。网络收包中断数据到了你不赶紧收网卡FIFO就溢出了包直接丢所以要在最短时间把数据搬到内存最好连下半部都不用直接在中断里把预算内的包收完。按键中断本身就不急但因为按压会产生抖动反而需要在延迟处理里做滤波经常就用线程化中断配合msleep。GPIO电平触发的中断如果外部信号不稳定最容易产生中断风暴处理策略又不一样。所以内核才同时保留了好几种下半部机制软中断、tasklet、工作队列、线程化中断。它们各有各的适用场景选错了轻则浪费CPU重则系统卡死。后面我会专门用一节来对比怎么选。2. 从硬件中断信号到Linux中断号irq domain 的原理很多人写驱动时对中断的理解就停在 request_irq 这一步在设备树里配置 interrupts 属性驱动里用 platform_get_irq 拿中断号然后 request_irq 注册回调完事。但这中间其实藏着整个中断机制最容易出问题的一环——中断号是怎么从一个硬件引脚变成一个内核中断号的。2.1 硬件中断号、Linux中断号与irq domain先理清概念。一个中断控制器比如GIC有若干中断输入线每条线有一个编号这个编号叫硬件中断号hwirq。在老的x86系统里硬件中断号和Linux中断号是一一对应的简单粗暴直接用16个IRQ就完了。到了ARM平台GIC可能有好几百个中断源再加上多个中断控制器级联情况就复杂了同一张板子上GIC的SPI 34号中断到底是哪个设备发出来的设备树里写的那个32位数字映射到内核的哪个irq号解决这个问题的方案就是irq domain。它的作用简单说就是一张映射表把硬件中断源映射为Linux中断号。在设备树时代每个中断控制器会在系统初始化时注册一个 irq_domain里面实现三个关键回调map把hwirq映射为Linux irq号、translate解析设备树interrupts属性里的参数、xlate把devicetree中的中断描述翻译成hwirq。当驱动调用 platform_get_irq 时内核会沿着设备树里的 interrupt-parent 找到对应的中断控制器再通过它的 irq_domain 完成从硬件描述到Linux irq number 的翻译。这个机制被引入的另一个重要原因是让中断控制器驱动的编写和使用中断的设备驱动的编写解耦。设备驱动只需要说我需要这个中断源这是我的回调函数具体这个中断源在硬件上怎么配置、优先级是多少、是上升沿还是下降沿触发全交给中断控制器驱动和irq domain去处理。2.2 设备树里中断属性的解析与常见错误设备树里每个用到中断的设备节点通常会这样写gpio1 { key_int: key-int { compatible my-company,key; interrupt-parent gpio1; interrupts 5 IRQ_TYPE_EDGE_FALLING; ... }; };这里的5 IRQ_TYPE_EDGE_FALLING两个参数含义由 interrupt-parent 指向的中断控制器的 irq_domain 来解释。GPIO 中断控制器一般解释为第5号GPIO、下降沿触发。我踩过一个非常典型的坑在 GPIO 控制器上配置中断时没用 interrupts 属性而是用了gpios gpio1 5 GPIO_ACTIVE_LOW然后在驱动里用gpiod_to_irq去拿中断号。这个路子也能通但和直接用 interrupts 有一个本质区别——前者是通用GPIO子系统帮你动态分配的中断后者是设备树静态描述的中断。两种方式别混着用否则容易出现驱动加载顺序导致的中断号不一致问题。另外一个常见错误是 interrupt-parent 写错了。ARM平台经常有多个中断控制器GIC负责SoC内部外设中断GPIO控制器负责外部引脚中断。设备树默认的 interrupt-parent 在根节点上但具体设备节点可以覆盖。如果你给一个挂在GPIO下的设备写了 interrupt-parent gic中断号翻译出来的结果基本是垃圾request_irq 可能不报错但中断触发时你这个驱动永远收不到。排查这类问题最快的办法是看 /proc/interrupts 里的中断计数如果某个中断号对应的计数永远不涨而且你的设备确实在工作九成是中断映射出了问题。此时用 perf 的 tracepoint 看 irq 相关事件或者 ftrace 的 irq_handler_entry比瞎猜高效得多。2.3 中断描述符 irq_desc 与 irq_chip每个Linux中断号背后都有一个struct irq_desc结构体它是中断机制的档案袋。里面记录了这个中断当前的状态是否被禁用、是否在处理中挂在它上面的所有动作action链表处理函数所属的中断控制器的操作函数集irq_chip线程化中断对应的内核线程各种统计信息、调试信息irq_chip是中断控制器驱动的核心结构体它封装了对中断控制器的底层操作irq_mask屏蔽、irq_unmask打开、irq_set_type设置触发方式、irq_set_wake配置唤醒源、irq_ack应答中断。设备驱动打交道最多的其实是上面的 action 回调而 irq_chip 全是中断控制器驱动开发者的事。你写普通外设驱动时不用直接碰 irq_chip但理解这层存在对后面排查问题很有帮助——比如你发现中断触发后 handler 没被调用但irq_mask里的屏蔽位被置上了那就可能是中断控制器层面的问题而不是你驱动的逻辑问题。3. 中断处理的完整流程与API实战现在把视角切到写驱动的人最关心的部分我如何在驱动里注册一个中断处理函数不同场景应该选哪个注册函数各有什么讲究3.1 注册中断的API选择你还在无脑request_irq吗很多教材讲中断就是request_irq这没错但它不是唯一选择甚至在很多场景下不是最优选择。内核里注册中断主要有这几个接口request_irq(irq, handler, flags, name, dev)基础版handler在中断上下文执行不能用任何会睡眠的函数。request_threaded_irq(irq, handler, thread_fn, flags, name, dev)进阶版handler仍然在中断上文执行 thread_fn在内核线程中执行可以睡眠。request_irq其实是对request_threaded_irq的封装传入的handler对应thread_fn的位置同时还额外传了一个不可睡眠的handler。// 普通中断handler在中断上下文执行 static irqreturn_t my_irq_handler(int irq, void *dev_id) { /* 这里不能睡眠 */ ... return IRQ_HANDLED; } // 线程化中断thread_fn在进程上下文执行可以睡眠 static irqreturn_t my_irq_thread_fn(int irq, void *dev_id) { /* 这里可以睡眠可以上mutex可以kmalloc GFP_KERNEL */ ... return IRQ_HANDLED; }你可能要问了那我都用线程化中断不就行了反正能睡眠多省心。这里要纠正一个误区线程化中断不是银弹它有一个显著的代价——延迟。线程化中断的中断处理函数是在一个内核线程里运行的。这个线程优先级虽然比普通进程高但它仍然要走调度器、可能要等其他CPU上的线程让出资源所以从中断触发到 thread_fn 真正跑起来中间可能多出几十微秒到几毫秒的延迟。对实时性要求高的场景比如运动控制、高速数据采集这种抖动是没法接受的。所以选择的逻辑应该是中断处理逻辑很短只需要读写寄存器、置个标志位、唤醒等待队列用request_irq在中断上下文里快速搞定。中断处理逻辑需要做耗时操作且实时性要求不高用线程化中断或者用request_irq 下半部机制。处理逻辑里必须要用mutex、msleep这类会睡眠的API只能线程化中断或者工作队列。用一张表总结就是场景推荐方案原因处理极短只需读状态清中断request_irq开销最小处理中等需要一定延迟容忍度request_irq tasklet/软中断兼具低延迟和吞吐处理较慢可接受较大延迟request_thired_irq可睡眠代码简单处理很慢且涉及IO/锁workqueue进程上下文灵活3.2 IRQF_ 系列标志位每个都值得细看注册中断时的flags参数很多人只是照抄例子实际上里面的门道很深。我挑几个最常用的展开讲。IRQF_SHARED用来声明中断线可以共享。共享中断在硬件上很常见多个设备共用一根中断线。这时每个设备的handler都会注册到同一个irq_desc的action链表上。关键点是所有使用共享中断的驱动在handler里必须先判断中断是不是自己设备的。如果不判断直接返回IRQ_HANDLED会造成中断被抢走别的设备永远收不到。正确做法是读取自己设备的寄存器状态确认如果确实有中断挂起处理掉如果没有返回IRQ_NONE让中断核心继续调用链表里的下一个handler。IRQF_TRIGGER_RISING、IRQF_TRIGGER_FALLING、IRQF_TRIGGER_HIGH、IRQF_TRIGGER_LOW这四个是触发方式。这里有个很值得注意的点触发方式的配置除了通过request函数的flags还可以在设备树interrupts属性里指定。如果两边都指定了以哪边为准答案是最终生效的是 irq_chip-irq_set_type 通过协商后的结果但设备树方式更推荐因为设备树描述了硬件本身而不是某个驱动的偏好。IRQF_DISABLED这个标志以前很有名意思是handler执行期间保持关中断。现代内核里这个标志基本被废了因为内核现在的行为默认就是 handler在中断上下文执行期间当前CPU的本地中断已经被关闭没必要再显式指定。你如果在老代码里看到它可以放心去掉。还有一个容易忽略的是IRQF_NO_THREAD。这个标志意思简单即使系统开启了强制线程化threadirqs内核参数这个中断也不能被线程化。一般用在少数对延迟极度敏感、必须直接在中断上下文执行的中断源上。我看到有实时项目直接给所有关键中断打上这个标志配合CPU隔离用效果确实好但前提是你得确信handler真的极其短小。3.3 handler的返回值语义与常见误解irqreturn_t有四个值IRQ_NONE、IRQ_HANDLED、IRQ_WAKE_THREAD、IRQ_HANDLED | IRQ_WAKE_THREAD后者等价于IRQ_WAKE_THREAD表示中断已处理且需要唤醒thread_fn。很多驱动里直接写return IRQ_HANDLED这在大多数场景没什么问题但如果涉及共享中断这个返回值就很重要。IRQ_NONE 表示这个中断不是我这个设备产生的请求内核继续让下一个handler尝试IRQ_HANDLED 表示我认识这个中断而且处理完了。如果所有共享中断handler都返回IRQ_NONE内核会认为发生了一个未处理的中断在 /proc/interrupts 里那个中断的 Unhandled 列计数会增加长时间未处理甚至会触发内核的spurious interrupt检测机制自动禁用该中断。这个行为在实际调板时很坑你代码看着没问题但中断就是偶尔不好使查半天才发现是spurious interrupt太多内核直接把中断禁了。遇到这种情况去dmesg里搜disabling IRQ一搜一个准。3.4 一个完整的按键中断驱动示例为了把上面的API串起来给一个工作时用过的按键中断例子。这个例子故意不用简单的request_irq而是用线程化中断因为按键消抖天然需要延时#include linux/module.h #include linux/platform_device.h #include linux/interrupt.h #include linux/of.h #include linux/gpio/consumer.h #include linux/delay.h struct key_dev { struct gpio_desc *gpio; int irq; }; static irqreturn_t key_thread_fn(int irq, void *dev_id) { struct key_dev *key dev_id; /* 消抖等100ms再看电平线程上下文可以安定使用msleep */ msleep(100); if (gpiod_get_value(key-gpio) 0) { /* 确认是低电平说明真的按下了 */ pr_info(key pressed, irq%d\n, irq); } return IRQ_HANDLED; } static int key_probe(struct platform_device *pdev) { struct key_dev *key; int ret; key devm_kzalloc(pdev-dev, sizeof(*key), GFP_KERNEL); if (!key) return -ENOMEM; key-gpio devm_gpiod_get(pdev-dev, NULL, GPIOD_IN); if (IS_ERR(key-gpio)) return PTR_ERR(key-gpio); key-irq gpiod_to_irq(key-gpio); if (key-irq 0) return key-irq; /* 这里把gpio作为dev_id传给handler共享中断下也可以用来区分设备 */ ret request_threaded_irq(key-irq, NULL, key_thread_fn, IRQF_TRIGGER_FALLING | IRQF_SHARED, my-key, key); if (ret) return ret; platform_set_drvdata(pdev, key); return 0; } static int key_remove(struct platform_device *pdev) { struct key_dev *key platform_get_drvdata(pdev); free_irq(key-irq, key); return 0; } static const struct of_device_id key_of_match[] { { .compatible my-company,key }, { } }; static struct platform_driver key_driver { .probe key_probe, .remove key_remove, .driver { .name my-key, .of_match_table key_of_match, }, }; module_platform_driver(key_driver); MODULE_LICENSE(GPL);这里有几个实操细节我觉得比那些教科书示例重要第一个request_threaded_irq的第一个handler参数传NULL含义是我不需要硬中断上文的特殊处理中断一进来就直接唤醒线程。这样写最干净处理逻辑全在线程里代码也好维护。第二个dev_id参数一般传设备结构体指针。这个指针在共享中断里是唯一的身份标识free_irq的时候必须传同一个否则free_irq会返回-EINVAL。我见过有人在free_irq里传NULL结果内核直接报Trying to free already-free IRQ排查了很久才发现是这里没对齐。第三个这个例子里用IRQF_SHARED是因为我在某个项目里几个按键确实共用了一根中断线。如果你的硬件上每个按键单独占用GPIO中断其实不一定要共享。但加上共享标志不会损害什么反而让代码更通用。4. 下半部机制实测软中断、tasklet、工作队列怎么选前面反复说到下半部这一节把它讲透。内核里能用来做下半部延迟处理的主要有这几个软中断softirq、tasklet基于软中断实现、工作队列workqueue、线程化中断上一节已经讲了。它们之间的区别我认为可以用两个维度衡量执行上下文和并发性。4.1 软中断性能最好但你自己写要小心软中断是目前内核里最高性能的下半部机制网络收包、块设备IO这类热点路径都用它。软中断执行在哪个上下文严格说它既可以在中断返回路径上执行ksoftirqd被唤醒之前也可以在专门的软中断守护进程 ksoftirqd/N 里执行。这意味着它运行的时候可能抢占普通进程也可能是在中断上下文收尾阶段运行反正不是普通进程上下文。软中断最大的特点是可以并发同一种软中断可以在多个CPU上同时执行。这个特性让它的吞吐量极高但也带来一个麻烦——你自己实现的软中断处理函数必须考虑多CPU并发访问共享数据得加锁或者用per-cpu变量不然极易出竞态。这正是tasklet诞生的原因tasklet是在软中断基础上做的一个简化版它保证同一种tasklet在任何时刻只能在一个CPU上执行。写tasklet处理函数时你不用考虑和其他CPU上的同一个tasklet并发的问题省了一大堆心智负担。但你得注意tasklet这个串行化是有代价的。在有多核CPU的板子上如果你的tasklet处理逻辑很重其他CPU上的相同tasklet只能等着吞吐量就上不去了。这也是现在内核社区越来越不推荐tasklet、转而推荐线程化中断和workqueue的原因之一——新代码能不用tasklet就不用。4.2 工作队列把下半部当内核进程来跑工作队列的本质是你扔一个函数进一个队列系统会有内核线程worker挨个取出来执行。它运行在进程上下文所以想睡就睡想拿mutex就拿想调kmalloc(GFP_KERNEL)就调自由度最高。代价是执行时机不确定。它是一个普通内核线程在干活要参与调度可能被其他高优先级任务抢走CPU。中断触发到你的工作函数真正开始执行这个延迟可能高达毫秒级甚至更高。所以工作队列适合那些晚做一会没关系但要做的事比较复杂的场景比如热插拔事件处理、设备状态上报、协议解析等。用简单代码量表示优先级和环境软中断/tasklet: 延迟低(us级)但上下文受限(不能睡眠) 线程化中断: 延迟中等(ms级以下)可睡眠 工作队列: 延迟相对较高但灵活完全进程上下文我实际的选型习惯是处理逻辑必须极快几十微秒内选软中断或者tasklet驱动里处理自己的中断事务优先选线程化中断事务需要和其他内核模块交互、排队、可能长时间等待用工作队列。千万别在驱动里为了性能硬上软中断你没那个必要还容易写出并发问题。4.3 强制线程化一个被忽略的全局开关内核启动参数里有一个threadirqs加上之后系统会强制把所有非关键中断的处理函数丢到内核线程里执行。这么做的好处很明显中断处理不再占用中断上下文不会因为长时间关中断导致其他中断饥饿系统的响应延迟更平滑。坏处同前面说的线程化中断——中断处理的实时性变差。这个参数在生产环境里我用过一次。当时一个嵌入式板子频繁出现软中断导致的网络延迟抖动排查下来是某个网卡的接收中断处理里做了太多工作把其他中断堵住了。加了这个参数之后网络延迟的方差立刻好看了不少。但它治标不治本真正的问题还是那个网卡驱动在中断里干太多活了后来换用 NAPI 机制才彻底解决。如果你在调实时性要求高的系统建议把threadirqs和各中断的IRQF_NO_THREAD标志配合使用可以做到整体平滑、个别关键中断保证实时。5. 中断上下文的内存分配与锁这些雷你别踩很多驱动崩溃不是中断逻辑本身错而是在中断上下文里用了不该用的内存分配API和锁。这一部分单独拿出来讲因为面试官爱问调试时更常见。5.1 中断上下文能不能用kmalloc能但要注意标志位。kmalloc(..., GFP_KERNEL)在中断上下文里用必死因为GFP_KERNEL允许睡眠内核会尝试回收内存页、可能阻塞等待。中断里一睡就触发那个BUG: sleeping function called from invalid context。中断上下文只能使用GFP_ATOMIC。这个标志告诉内存管理系统我现在在原子上下文不能睡你必须在不动用需要睡眠的路径的情况下给我内存。代价是分配成功率低一些内存不足时直接返回NULL不会等。所以代码里必须判断返回值/* 中断上下文中正确的内存分配 */ struct my_data *data kmalloc(sizeof(*data), GFP_ATOMIC); if (!data) { /* 处理失败别硬来 */ return IRQ_NONE; }还有个容易被忽视的点GFP_ATOMIC分配不能太大。它依赖的紧急内存池是有限的大概几十KB级别你要是每次都分配几KB还没有及时释放内存池耗尽了后续所有原子分配都会失败。所以我一般建议中断里尽量不分配内存最好在初始化时预分配好环形缓冲区或固定大小的内存池。5.2 锁的选择自旋锁还是互斥锁锁的问题和睡眠问题紧密相关。中断上下文里想保护共享数据只能用自旋锁spinlock不能用互斥锁mutex。自旋锁的本质是忙等拿不到锁就在原地转圈不睡眠。虽然浪费CPU但能保证在原子上下文里不会睡过去。更精确的说自旋锁在单核SMP系统中配合关本地中断能构成一个有效的临界区保护。在多核系统中自旋锁保证的是其他CPU也不会同时进入临界区。还有一个进阶话题如果你在软中断或者tasklet里需要保护数据通常用spin_lock_bh它会同时关闭本地CPU的软中断防止死锁。这是宁可在刚开始就记住的规则在tasklet里如果用普通spin_lock然后又有一个硬中断打断了你并在同一个锁上自旋就死锁了。时序上稍微一巧合内核直接hang死连log都不出相当难查。另外提一个很少人注意到的点在中断上下文里用local_irq_save/local_irq_restore手动关闭中断时必须保证这两个函数配对使用而且不能在save之后提前return。很多新手写临界区代码中间一个return就把中断关在关闭状态了后续所有中断全被屏蔽系统表现为像死机一样但CPU占用不高。6. 排查中断问题的实战工具箱写驱动的人都知道一个中断相关的bug表面现象可能五花八门设备突然不响应了、CPU某一个核占用率100%、系统偶尔卡顿一下、网络延迟忽高忽低。这一节我给出实际排查用的工具和套路比起看源码遇到线上问题先用这些手段定位效率高得多。6.1 /proc/interrupts 是第一现场排查任何中断问题第一步永远是看/proc/interrupts。这个文件列出了系统里所有中断号、每个CPU上该中断触发次数、中断控制器的名字、以及注册了handler的设备名。怎么从里面读信息先看触发次数增长情况。用两个命令间隔几秒各看一次对比同一行中断计数是否有变化cat /proc/interrupts sleep 3 cat /proc/interrupts如果某个中断计数暴涨每秒上万次基本可以断定是中断风暴。常见原因外部设备信号毛刺导致反复触发、中断处理函数没能清除硬件中断挂起位、GPIO中断配置成了电平触发但电平一直不恢复。中断风暴会让CPU长时间被中断处理占据用户进程饿死系统看起来像卡死一样。如果某个中断计数一直不变而设备确实在工作那问题出在中断根本没走到这个handler上也可能是设备根本没产生中断信号。顺着设备树、interrupt-parent、irq_domain映射一步步查重点看是不是中断号对不上。/proc/irq/[irq_num]/spurious这个文件记录了该中断被误判为spurious的次数。前面说的内核自动禁用中断的根源就在这。数值如果一直在涨说明设备驱动在共享中断链路上没有正确返回IRQ_NONE。6.2 中断延迟与耗时观测ftrace和perf想量化某个中断处理函数到底花了多久最快的方式是perf看 tracepointperf record -e irq:irq_handler_entry -e irq:irq_handler_exit -a sleep 5 perf report这样能看到每次中断处理从进入到退出消耗的时间分布。如果大部分时间都在几个us以内说明处理很快如果经常有几百us甚至ms级的说明handler里有重活得考虑移到下半部或线程化。更细的观测可以用trace-cmd配合function_graph追踪具体的处理函数调用链。有一次我排查一个触摸屏中断导致的卡顿就是用函数图追踪发现它每次中断都在I2C总线上读寄存器而I2C的访问延时被一个慢设备拖到了几十毫秒级。这种问题光看代码很难定位但看trace一目了然——中断处理函数不能碰慢速外设这条经验真的很贵。6.3 中断线程的优先级与调度策略如果用了线程化中断还有个平时看不到的坑线程化中断对应的内核线程名称里会包含irq/[中断号]-[设备名]。比如root 1234 0.0 0.0 0 0 ? S 09:00 0:00 [irq/47-my-key]这个线程的调度策略默认是SCHED_FIFO优先级通常是50。如果你的系统里有其他RT任务而且碰到了中断线程抢不到CPU的情况可以用chrt调整它的优先级chrt -p -f 60 1234但要注意优先级改太高可能反过来饿死关键应用任务。嵌入式系统里中断线程优先级和实时任务的优先级怎么配是需要系统级全盘考虑的不能只盯着中断看。6.4 典型问题速查表现象可能原因排查手段中断计数暴涨CPU占用高中断风暴、硬件毛刺、ack不清/proc/interrupts对比、示波器查引脚中断计数不变设备不工作中断映射错误、设备树属性问题检查interrupt-parent、irq_domain映射dmesg出现disabling IRQ共享中断handler不认领spurious过多检查handler返回值、设备状态判断逻辑系统卡死且无法响应中断里死锁、关中断后未恢复查看是否使用spin_lock_bh、local_irq_save配对中断处理延迟大handler里访问慢速外设、I2C/SPI操作ftrace函数图、perf观测线程化中断不执行内核线程被饿死、优先级太低查看线程状态、chrt调整优先级7. 深入中断源码的几个必看入口如果你真想把中断吃透光用API是不够的还是得看源码。我建议按下面这个顺序读不要上来就啃GIC驱动会很懵。7.1 源码阅读路径第一个看kernel/irq/manage.c这是核心逻辑它就是中断的调度中心request_irq、free_irq、setup_irq都在这读完你就知道一个handler是怎么被挂到action链表上的函数返回值和线程化唤醒是怎么串联的。第二个看kernel/irq/chip.c这是中断控制器的通用封装层mask/unmask/ack这些操作怎么通过irq_chip到达具体的中断控制器。第三个看drivers/irqchip/irq-gic-v3.cARM平台最常见的GIC V3中断控制器驱动读完你能明白一个硬件中断线是怎么在GIC里被配置、使能、发出信号的。第四个看kernel/softirq.c了解软中断的触发、调度和执行时机其中__do_softirq函数和 ksoftirqd 的配合是理解中断延迟的关键。7.2 用ftrace跟踪一次完整的中断处理流程一个很好的学习方法是实战跟踪用ftrace把一次完整的中断处理流程抓下来从硬件中断到handler到下半部执行完。echo 0 /sys/kernel/debug/tracing/tracing_on echo function_graph /sys/kernel/debug/tracing/current_tracer echo irq_handler_entry /sys/kernel/debug/tracing/set_event echo 1 /sys/kernel/debug/tracing/tracing_on # 触发一次中断比如按一下按键 echo 0 /sys/kernel/debug/tracing/tracing_on cat /sys/kernel/debug/tracing/trace这样你能看到从gic_handle_irq进入到handle_domain_irq再到你的具体handler被调用的完整调用链。看几次中断机制从抽象概念变成具象路径以后再遇到问题就不慌了。写在最后中断机制是驱动开发的试金石我个人的体会是Linux中断机制就像一面镜子把内核设计里关于时间、并发、调度的各种考量全映射出来了。你搞懂了它写驱动时那些为什么不能在这里sleep为什么用这个锁的问题都会迎刃而解。最后分享一个调板子时的小技巧新板子第一次点亮时先别急着跑业务写一个简单的中断测试驱动每个可能的中断源都接上用 /proc/interrupts 确认计数变化把中断这条路先走通。这一步做扎实了后面各种外设驱动的调试速度会快很多。我见过太多人在应用功能上折腾半天最后才发现是最底层的中断根本没有正确打通。中断通了整个嵌入式系统才算是真正活了。