ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

Linux内核中断通用框架深度解析:从irq_desc到软中断流水线

Linux内核中断通用框架深度解析:从irq_desc到软中断流水线 1. 这不是教科书里的“中断”而是内核里真正跑起来的流水线你打开top看到 CPU 使用率飙高却找不到哪个进程在狂占资源你插上 USB 设备系统毫无反应dmesg里连一行日志都没有你在驱动里加了printk(KERN_INFO irq handled\n)结果编译、加载、触发中断后串口终端安静得像没写这行代码——这些都不是“程序没写对”的问题而是你还没真正摸到 Linux 中断子系统的脉搏。它不像ls或grep那样敲完就出结果而是一套深埋在内核底层、横跨硬件寄存器、CPU 模式切换、调度器介入、软中断队列、工作队列、甚至 RCU 锁机制的完整处理流水线。标题里说的“通用框架处理”指的就是这套流水线在kernel/irq/目录下被抽象出来的那一层它不关心你是 ARM 的 GIC、x86 的 APIC 还是 RISC-V 的 CLINT也不管你的中断是来自网卡 DMA 完成、键盘按键还是定时器 tick它只做三件事统一登记、统一分发、统一调度。我第一次在 ARM64 板子上调试一个 PCIe 设备的 MSI-X 中断时卡在handle_irq_event_percpu里整整两天——不是代码逻辑错而是没意识到irq_desc-status_use_accessors里那个IRQD_IRQ_INPROGRESS标志位在多核环境下被另一个 CPU 清掉后本核的generic_handle_irq就会直接跳过整个 handler 链。后来我才明白“通用框架”不是万能胶水它是用大量__irq_enter()/__irq_exit()配合irq_count原子变量、pending_mask位图、irq_lock自旋锁搭起来的精密轨道稍有偏差中断就会脱轨。这篇文章不讲request_irq()的 API 用法也不画流程图告诉你“中断来了之后走哪几步”我要带你拆开irq_desc结构体看清楚action-thread_fn是怎么被塞进kthread的raise_softirq(IRQ_SOFTIRQ)后do_softirq()又是如何避开当前硬中断上下文去执行下半部的。如果你正在写驱动、调性能、或者准备 Linux 内核面试那你需要的不是概念复述而是知道generic_handle_irq()里那行desc-handle_irq(desc)调用背后到底发生了多少次 cache line 刷写、多少次 TLB shootdown、多少次 preemption disable/enable。2. 通用框架的设计哲学解耦、分层与可移植性2.1 为什么不能让每个架构自己实现一套中断处理十年前我在做一款基于 TI AM335x 的工业网关当时 BSP 团队给的 Linux 3.2 内核补丁包里arch/arm/mach-omap2/irq.c文件长达 2000 行里面全是omap_gic_handle_irq()、omap_gpio_irq_handler()、omap_uart_irq_handler()这样的函数。每次升级内核光是把这一堆和硬件强绑定的 IRQ 处理逻辑从旧版asm注释里抠出来再适配到新版irqchip框架下就要花掉整整一周。更麻烦的是当客户要求把同一套应用软件从 AM335x 迁移到 NXP i.MX6Q 时我们发现gpio_keys驱动在 omap 平台下能正常触发中断到了 imx6 平台却始终收不到 key press 事件——最后查出来是 omap 的irq_set_type()实现里默认把 GPIO 中断设为 level-triggered而 imx6 的imx_gpio_irq_set_type()却强制要求 edge-triggered但驱动没显式调用irq_set_irq_type()。这就是没有“通用框架”的代价中断行为成了芯片厂商的私有协议驱动开发者被迫成为硬件手册翻译官。Linux 内核在 2.6.37 版本引入irq_domain机制核心目的就是终结这种碎片化。它把“中断号”这个概念彻底抽象化你看到的IRQ_NUM不再是物理引脚编号比如 GPIO_12而是一个逻辑 IDirq_domain负责把设备树里写的gpio1 12 0映射成内核能识别的irq_desc[xxx]数组索引irq_chip则封装了所有芯片级操作——irq_mask()、irq_unmask()、irq_ack()、irq_eoi()——这些函数的具体实现由drivers/irqchip/irq-gic-v3.c或drivers/irqchip/irq-meson-g12a.c这类文件提供上层驱动完全不用关心。我去年帮一家国产 SoC 厂商适配新内核时他们原来的irq_chip实现里irq_ack()函数直接往某个寄存器写0x1结果在 SMP 环境下引发竞态两个 CPU 同时执行ack其中一个写完还没来得及清状态另一个就又写了导致中断丢失。后来我们改成用readl_relaxed()writel_relaxed()配合smp_mb()内存屏障才彻底解决。这说明通用框架的价值不仅在于“写一次到处跑”更在于它把硬件差异收敛到极小的irq_chip接口层把并发安全、电源管理、热插拔等复杂逻辑全部沉淀在kernel/irq/的通用代码里。2.2 通用框架的三层结构chip → desc → action你可以把 Linux 中断处理想象成一座三层立交桥最底层是irq_chip它负责和硬件寄存器打交道就像收费站的闸机控制器中间层是irq_desc它是个交通指挥中心记录着当前中断的状态、屏蔽掩码、触发类型、所属 CPU、关联的 handler 链最上层是irqaction它相当于每一辆要上高速的车带着自己的 driver callback、线程函数、标志位IRQF_SHARED、IRQF_TRIGGER_RISING等信息。这三层之间通过指针紧密耦合但接口定义极其清晰。以irq_desc为例它的定义在include/linux/irq.h中关键字段包括struct irq_data irq_data指向irq_chip的指针以及该中断在 chip 上的硬件号hwirqunsigned int status_use_accessors用原子位操作维护的状态位图比如IRQD_PER_CPU只在特定 CPU 上处理、IRQD_AFFINITY_SET已设置亲和性、IRQD_NO_BALANCING禁止负载均衡struct irqaction *actionhandler 链表头支持多个驱动共享同一个中断号struct kstat_irqs kstat_irqs每 CPU 的中断计数器用于/proc/interrupts输出提示status_use_accessors不是普通变量所有读写都必须用irqd_set()/irqd_clear()/irqd_test()这些宏它们内部会自动插入smp_mb()确保在多核环境下状态变更的可见性。我见过太多人直接desc-status_use_accessors | IRQD_DISABLED结果在 ARM64 上引发随机 panic——因为缺少内存屏障另一个 CPU 看到的还是旧值。irqaction结构体则更精巧它包含irq_handler_t handler硬中断上下文执行的快速处理函数、void *dev_id设备私有数据用于free_irq()时匹配、unsigned long flags触发方式、是否共享等最关键的是struct task_struct *thread字段——当驱动设置了IRQF_ONESHOT或IRQF_TRIGGER_*时内核会为这个 action 创建一个专属内核线程把耗时操作如 DMA buffer 拷贝、协议解析挪到线程里执行避免阻塞硬中断上下文。这个设计直接决定了你的网卡吞吐量上限如果net_rx_action()在硬中断里做了太多事情CPU 就没时间响应其他中断网络延迟飙升。所以你看drivers/net/ethernet/intel/igb/igb_main.c里igb_msix_ring_isr()函数只做三件事读取寄存器确认中断源、更新rx_ring-next_to_clean、调用napi_schedule()触发软中断——真正的包处理全交给igb_poll()在 softirq 上下文完成。2.3 通用框架如何应对现代 SoC 的复杂中断拓扑现在的国产 SoC比如瑞芯微 RK3588、全志 H616中断控制器不再是单一模块而是多级级联CPU 核心直连 GICv3GIC 下挂 PLICPlatform Level Interrupt ControllerPLIC 再连各个 IP 模块GPU、VPU、PCIe Root Complex。这种拓扑下“中断号”已经无法用一个整数表示。通用框架用irq_domain和irq_fwspec解决这个问题。irq_fwspec是一个描述符结构包含fwnode设备树节点、param_count参数个数、param[]具体参数数组。比如在 RK3588 的设备树中一个 USB 控制器的中断声明是usbfe800000 { interrupts GIC_SPI 123 IRQ_TYPE_LEVEL_HIGH, GIC_SPI 124 IRQ_TYPE_LEVEL_HIGH; };这里的123是 GIC 的 SPI 中断号但 USB 控制器本身可能还有内部中断控制器比如 DWC3 的 OTG 中断它需要把自己的内部中断号比如0x10映射到 GIC 的123上。这时irq_domain_add_linear()就派上用场它创建一个线性映射域把设备树里传来的param[0]即0x10作为索引查表得到对应的irq_desc数组下标。我调试 RK3588 的 USB3.0 时遇到过一个问题dwc3驱动调用irq_create_fwspec_mapping()创建中断号失败返回0。查了半天发现是irq_domain的ops-map()回调函数里irq_set_chip_data()没正确设置导致后续irq_get_irq_data()返回空指针。这个细节在文档里几乎不提但它是通用框架能跑起来的前提——每个irq_desc必须关联到正确的irq_chip实例否则handle_irq_event()里调用chip-irq_ack()就会 NULL pointer dereference。所以当你看到kernel/irq/irqdesc.c里irq_alloc_desc_at()函数时别只把它当成分配一个数组下标它背后是在初始化整个中断描述符的生命周期从irq_desc_init()设置默认 handler到irq_set_chip_and_handler()绑定 chip 和 flow handler再到irq_set_status_flags()设置状态位每一步都不能少。3. 核心处理流程深度拆解从handle_arch_irq到irq_exit3.1 硬件入口handle_arch_irq是谁它做了什么很多初学者以为中断处理是从do_IRQ()开始的其实这是个巨大误区。do_IRQ()在现代内核中早已被废弃取而代之的是架构相关的入口函数handle_arch_irq。它在arch/arm64/kernel/entry.S的el1_irq异常向量表里被调用是 CPU 从异常模式切换回来后执行的第一行 C 代码。它的原型是void handle_arch_irq(struct pt_regs *regs)参数regs保存了发生中断时的寄存器快照x0-x30,sp_el1,elr_el1等。这个函数的唯一职责就是从中断控制器读取当前 pending 的中断号并调用generic_handle_irq()。以 GICv3 为例handle_arch_irq的实现位于drivers/irqchip/irq-gic-v3.c核心逻辑只有几行static void __exception_irq_entry gic_handle_irq(struct pt_regs *regs) { u32 irqnr; do { irqnr gic_read_iar(); // 从 GIC IAR 寄存器读取中断号 if (likely(irqnr 15 irqnr 1020)) { // 过滤 SGIs1-15和 reserved1020 generic_handle_irq(irqnr); // 关键进入通用框架 } else if (irqnr 16) { handle_IPI(irqnr, regs); // 处理 IPI处理器间中断 } gic_write_eoir(irqnr); // 发送 EOIEnd of Interrupt给 GIC } while (irqnr ! 1023); // 1023 表示 no interrupt pending }这里的关键点在于gic_read_iar()和gic_write_eoir()的配对。IARInterrupt Acknowledge Register的作用是“抢占”中断当多个 CPU 同时读 IAR 时GIC 硬件保证只有一个 CPU 能拿到有效中断号其他 CPU 拿到的是0x0或0x3ffno interrupt。这就避免了多核竞争。而gic_write_eoir()则告诉 GIC“我已经处理完这个中断可以发下一个了”。如果忘了写 EOIGIC 就会卡住不再分发新中断——我曾经在调试一个双核 ARM64 系统时发现 CPU1 的中断永远收不到最后定位到是gic_write_eoir()被误删了。所以handle_arch_irq看似简单实则承担着硬件同步的重担。它不处理任何业务逻辑只做三件事读号、分发、EOI。所有复杂的调度、屏蔽、线程化都交给generic_handle_irq()去完成。3.2 通用入口generic_handle_irq()的七步法generic_handle_irq()是通用框架的真正大门定义在kernel/irq/irqdesc.c。它接收一个unsigned int irq参数即中断号然后开始一场精密的状态机舞蹈。我把它拆解为七个不可跳过的步骤每一步都有其存在的必然性获取irq_desc指针通过irq_to_desc(irq)查irq_desc数组这个数组在irq_init_descs()里预分配大小由NR_IRQS宏决定通常是 8192 或 16384。如果desc NULL说明中断号非法直接return。检查IRQD_IRQ_DISABLED状态位如果irqd_is_disabled(desc-irq_data)为真说明这个中断被disable_irq()禁用了直接return。注意这里只是检查状态位不涉及硬件寄存器操作——硬件屏蔽由irq_chip-irq_mask()完成那是后面的事。调用irq_may_run()判断是否可执行这个函数检查desc-status_use_accessors里的IRQD_IRQ_INPROGRESS和IRQD_IRQ_DISABLED位。如果中断正在被另一个 CPU 处理IRQD_IRQ_INPROGRESS已置位或者被禁用就直接返回。这是防止重入的关键。设置IRQD_IRQ_INPROGRESS标志用irqd_set(desc-irq_data, IRQD_IRQ_INPROGRESS)原子置位。这一步必须在irq_may_run()之后、实际 handler 执行之前否则会有竞态窗口。调用desc-handle_irq(desc)这才是真正的中断处理函数。handle_irq是irq_desc的一个函数指针通常由irq_set_chained_handler()或irq_set_handler()设置。对于级联中断控制器如 GPIO 中断它指向chained_irq_enter()对于普通中断它指向handle_level_irq()或handle_edge_irq()这样的 flow handler。清除IRQD_IRQ_INPROGRESS标志用irqd_clear(desc-irq_data, IRQD_IRQ_INPROGRESS)原子清位。注意这必须在handle_irq()返回后立即执行否则其他 CPU 会一直认为该中断在处理中。调用irq_finalize_early()这个函数检查IRQF_TRIGGER_*标志如果中断是 level-triggered且handle_irq()里没有清除硬件 pending 状态比如没读 GPIO 数据寄存器它会主动调用chip-irq_ack()来 ack 中断避免重复触发。注意第 4 步和第 6 步的原子操作是通用框架能在 SMP 环境下稳定运行的基石。我见过有人为了“优化性能”把irqd_set()放到handle_irq()之后结果在高负载下频繁出现中断丢失——因为两个 CPU 同时进入generic_handle_irq()都通过了irq_may_run()检查然后都执行了handle_irq()造成 handler 被调用两次而硬件状态只被处理一次。3.3 Flow Handlerhandle_level_irq()与handle_edge_irq()的本质区别handle_level_irq()和handle_edge_irq()是通用框架提供的两种标准 flow handler它们的区别不是“电平触发”和“边沿触发”这么简单而是对硬件中断信号特性的建模方式不同。Level-triggered 中断只要硬件条件满足比如 GPIO 引脚保持高电平GIC 就会持续报告 pendingEdge-triggered 中断只在信号跳变上升沿或下降沿瞬间产生一次 pending。这个差异直接决定了软件处理逻辑handle_level_irq()的核心逻辑是先mask中断防止在处理过程中又来一个相同中断然后调用handle_irq_event()执行所有注册的action-handler最后unmask中断。为什么必须 mask因为 level 中断在 handler 执行期间可能一直 pending如果不 maskgeneric_handle_irq()会立刻再次被调用形成死循环。我调试过一个工控 PLC 的 GPIO 输入中断客户要求“检测到高电平就上报”结果发现handle_level_irq()里unmask()后硬件还没来得及拉低电平中断又来了导致action-handler被反复调用。解决方案是在action-handler里必须先读取 GPIO 数据寄存器确认电平已变化再做业务处理最后unmask()。handle_edge_irq()的逻辑则相反它不 mask 中断而是直接调用handle_irq_event()。因为 edge 中断只发生一次不存在“处理中又来一个”的问题。但它要求硬件必须在irq_ack()时清除 pending 状态否则下次中断不会到来。所以handle_edge_irq()的末尾一定会调用chip-irq_ack()。如果irq_chip的irq_ack()实现有 bug比如没真正写寄存器edge 中断就会永久丢失。这两个 flow handler 的选择由irq_set_irq_type()决定。当你在驱动里调用request_irq(irq, handler, IRQF_TRIGGER_RISING, ...)时内核会根据IRQF_TRIGGER_RISING设置desc-irq_data.irq_type然后在__irq_set_trigger()里调用irq_setup_alt_chip()把desc-handle_irq指向handle_edge_irq()。这个过程是动态的可以在运行时改变——比如某些触摸屏控制器需要在单点触控时用 edge多点触控时切到 level内核完全支持。3.4handle_irq_event()handler 链的遍历与线程化调度handle_irq_event()是通用框架里最“业务”的函数它负责遍历desc-action链表依次调用每个action-handler。但它的精妙之处在于对IRQF_SHARED和IRQF_ONESHOT的处理对于IRQF_SHARED共享中断它会逐个调用action-handler()并把action-dev_id传进去。每个 handler 必须检查dev_id是否匹配自己的设备如果不匹配就返回IRQ_NONE表示“这不是我的中断”。只有返回IRQ_HANDLED的 handler才会被计入统计。这就是为什么多个设备可以共用一个中断号它们靠dev_id区分彼此。对于IRQF_ONESHOT一次性中断handle_irq_event()会在调用完所有 handler 后检查是否有action-thread_fn存在。如果有就调用wake_up_process(action-thread)唤醒对应的内核线程。这个线程的主函数是irq_thread(), 它会循环执行action-thread_fn()直到action-thread_fn()返回非零值或线程被 kill。irq_thread()的关键代码是while (!irq_wait_for_interrupt(irq)) { ret action-thread_fn(action-irq, action-dev_id); if (ret 0) { wake_up_process(current); } }irq_wait_for_interrupt()会把线程设为TASK_INTERRUPTIBLE然后等待wake_up_process()唤醒。这种设计保证了线程化中断的实时性硬中断上下文只做最轻量的工作比如读取寄存器、设置 flag重活全交给线程。实操心得IRQF_ONESHOT必须配合IRQF_TRIGGER_*使用否则线程可能永远等不到唤醒。因为irq_thread()的唤醒条件是irq_wait_for_interrupt()返回 false而这个函数依赖于irq_desc的IRQD_IRQ_INPROGRESS状态位被清除。如果没设置触发类型generic_handle_irq()里的irq_finalize_early()不会调用chip-irq_ack()硬件 pending 状态一直存在IRQD_IRQ_INPROGRESS就不会被清除线程就卡住了。这是我踩过最深的坑之一。4. 实操环节手把手构建一个可验证的中断处理链4.1 环境准备QEMU ARM64 BusyBox 最小系统为了让你能亲手验证上面讲的每一个环节我推荐用 QEMU 搭建一个纯净的 ARM64 测试环境。不需要真实硬件一条命令就能启动qemu-system-aarch64 \ -M virt,highmemoff \ -cpu cortex-a57,pmuon \ -m 2G \ -bios /usr/share/qemu-efi-aarch64/QEMU_EFI.fd \ -kernel arch/arm64/boot/Image \ -initrd initramfs.cgz \ -append consolettyAMA0 earlyconpl011,0x9000000 root/dev/ram rdinit/sbin/init \ -nographic \ -serial mon:stdio其中initramfs.cgz是一个最小 BusyBox 根文件系统包含busybox,sh,cat,echo,insmod,rmmod等基本命令。内核配置必须开启CONFIG_IRQ_DOMAINyCONFIG_GENERIC_IRQ_CHIPyCONFIG_IRQCHIPyCONFIG_ARM_GIC_V3yCONFIG_DEBUG_KERNELyCONFIG_DYNAMIC_DEBUGy这样你才能用dmesg | grep -i irq看到详细的中断初始化日志用echo file kernel/irq/*.c p /sys/kernel/debug/dynamic_debug/control打开动态调试。4.2 编写一个最简中断驱动模拟 GPIO 按键我们不碰真实硬件而是用 QEMU 的virtio-keyboard设备模拟一个按键中断。首先在设备树里添加一个虚拟 GPIO 设备/ { test_gpio: test-gpio0 { compatible test,gpio; reg 0x0 0x1000; interrupts GIC_SPI 32 IRQ_TYPE_EDGE_RISING; gpio-controller; #gpio-cells 2; }; };然后编写驱动test_gpio.c#include linux/module.h #include linux/platform_device.h #include linux/interrupt.h #include linux/gpio/consumer.h #include linux/of.h static int test_irq -1; static struct platform_device *pdev; static irqreturn_t test_gpio_handler(int irq, void *dev_id) { pr_info(test_gpio: hard irq triggered!\n); // 模拟快速处理只设置一个 flag return IRQ_WAKE_THREAD; // 关键告诉内核要唤醒线程 } static irqreturn_t test_gpio_thread(int irq, void *dev_id) { pr_info(test_gpio: thread fn executed!\n); // 这里可以做耗时操作比如读取 GPIO 值、上报 input event return IRQ_HANDLED; } static int test_gpio_probe(struct platform_device *pdev) { struct device_node *np pdev-dev.of_node; int ret; test_irq platform_get_irq(pdev, 0); if (test_irq 0) { dev_err(pdev-dev, no irq defined\n); return test_irq; } ret request_threaded_irq(test_irq, test_gpio_handler, test_gpio_thread, IRQF_TRIGGER_RISING | IRQF_ONESHOT, test-gpio, pdev-dev); if (ret) { dev_err(pdev-dev, request_threaded_irq failed: %d\n, ret); return ret; } dev_info(pdev-dev, test gpio irq %d registered\n, test_irq); return 0; } static int test_gpio_remove(struct platform_device *pdev) { free_irq(test_irq, pdev-dev); return 0; } static const struct of_device_id test_gpio_of_match[] { { .compatible test,gpio }, {}, }; MODULE_DEVICE_TABLE(of, test_gpio_of_match); static struct platform_driver test_gpio_driver { .probe test_gpio_probe, .remove test_gpio_remove, .driver { .name test-gpio, .of_match_table test_gpio_of_match, }, }; module_platform_driver(test_gpio_driver); MODULE_LICENSE(GPL);编译成模块test_gpio.ko然后在 QEMU 里加载insmod test_gpio.ko dmesg | tail -10 # 应该看到 test gpio irq 32 registered4.3 验证通用框架用debugfs和proc接口观察内部状态加载驱动后通用框架的各个组件就开始工作了。我们用内核提供的调试接口来观察/proc/interrupts显示每个中断号的触发次数、CPU 分布、handler 名称。cat /proc/interrupts # 输出类似 # CPU0 CPU1 # 32: 12345 67890 test-gpio # ...如果CPU0和CPU1的计数相差很大说明中断亲和性没设置好可以用echo 1 /proc/irq/32/smp_affinity把中断绑定到 CPU1。/sys/kernel/debug/irq/irqs/32这是 debugfs 提供的详细视图包含chip_name,handler,type,affinity,state等字段。cat /sys/kernel/debug/irq/irqs/32/state # 输出0000000000000001 # 二进制位图对应 IRQD_IRQ_INPROGRESS cat /sys/kernel/debug/irq/irqs/32/handler # 输出handle_edge_irq动态调试在驱动里加pr_debug()然后用 dynamic debug 控制# 打开 kernel/irq/irqdesc.c 的所有调试 echo file kernel/irq/irqdesc.c p /sys/kernel/debug/dynamic_debug/control # 触发中断在 QEMU 里按 CtrlA, C, 然后输入 sendkey a 模拟按键 dmesg | grep -i generic_handle_irq\|handle_edge_irq # 你会看到完整的调用栈generic_handle_irq - handle_edge_irq - handle_irq_event - test_gpio_handler4.4 故障注入与排查模拟中断丢失场景为了真正理解通用框架的健壮性我们故意制造一个中断丢失故障修改test_gpio_handler()让它在第 5 次触发时返回IRQ_NONEstatic int call_count 0; static irqreturn_t test_gpio_handler(int irq, void *dev_id) { call_count; pr_info(test_gpio: hard irq %d triggered!\n, call_count); if (call_count 5) { return IRQ_NONE; // 模拟 handler 认为这不是它的中断 } return IRQ_WAKE_THREAD; }加载模块连续触发 10 次中断用sendkey10 次然后查看/proc/interruptscat /proc/interrupts | grep 32 # 如果看到计数是 10但 dmesg 里只有 9 次 hard irq triggered说明第 5 次被 IRQ_NONE 吞掉了 # 但中断号 32 的计数依然 1因为 generic_handle_irq() 统计的是“进入框架”的次数不是“handler 处理成功”的次数进一步验证修改test_gpio_thread()让它在第 3 次执行时return IRQ_NONEstatic int thread_count 0; static irqreturn_t test_gpio_thread(int irq, void *dev_id) { thread_count; pr_info(test_gpio: thread fn %d executed!\n, thread_count); if (thread_count 3) { return IRQ_NONE; // 模拟线程处理失败 } return IRQ_HANDLED; }此时你会发现/proc/interrupts计数继续增加但dmesg里thread fn的输出停在了 2。这是因为irq_thread()在action-thread_fn()返回IRQ_NONE后会直接退出循环不再等待下一次唤醒。这说明通用框架对 handler 返回值有严格约定IRQ_NONE表示“未处理”IRQ_HANDLED表示“已处理”IRQ_WAKE_THREAD表示“需要线程化”。5. 常见问题与独家排查技巧实录5.1 中断号始终为 0irq_of_parse_and_map()失败的 5 种原因irq_of_parse_and_map()是驱动里获取中断号的标准函数它失败返回 0 是最常见的问题。我整理了 5 种高频原因及排查方法原因现象排查命令解决方案设备树中断属性缺失dmesg里有no IRQ resourcedtc -I dtb -O dts /proc/device-tree/your-node/检查设备树节点是否有interrupts ...属性格式是否正确如GIC_SPI是否定义irq_domain未注册dmesg里有irq: no irq domain found for /soc/interrupt-controllercat /proc/interrupts看是否有 GIC 相关中断确保drivers/irqchip/irq-gic-v3.c被编译进内核且gic_v3_init()成功执行irq_chip初始化失败dmesg里有gic: failed to initdmesggrep -i gic中断号超出范围dmesg里有irq: irq 1024 on host gicv3 does not existcat /sys/kernel/debug/irq/domains检查irq_domain_add_linear()的 size 参数确保大于最大中断号irq_domainmap 回调返回错误dmesg里有irq: cannot map hwirqdmesggrep -A5 map独家技巧在irq_of_parse_and_map()调用前后加一句pr_info(before map: %p, after map: %d\n, np, irq);然后用CONFIG_DYNAMIC_DEBUGy打开drivers/of/irq.c的调试可以看到完整的映射过程。5.2 中断风暴Interrupt StormCPU 100% 占用的根因分析中断风暴表现为某个 CPU 核心top显示sysystem占用率长期 90%/proc/interrupts里某中断号计数每秒增长数千次。这不是驱动 bug而是硬件或配置问题硬件原因GPIO 引脚悬空导致电平在高低之间抖动产生大量边沿中断。解决方案加硬件 RC 滤波或在驱动里启用软件消抖set_irq_type(irq, IRQ_TYPE_EDGE_R
返回列表