Linux中断申请全解析:API选择与实战避坑指南
2026/9/16 1:52:17 网站建设 项目流程

1. 驱动视角下的"申请中断"到底在申请什么

驱动开发里最常打交道的一件事,恐怕就是中断申请了。前面几篇把 Linux 中断子系统的中断控制器、irq domain、分发流程都过了一遍,这一篇回到驱动工程师的日常,聊聊在一个驱动程序里申请中断时,内核到底做了什么、驱动应该怎么写、以及那些文档里一般不会写清楚的坑。这篇适合已经写过一些字符设备或者 platform 驱动的同学,也适合准备从裸机中断思维切换到 Linux 中断思维的人。

1.1 一次硬件中断从触发到 CPU 的基本链路

驱动里调用request_irq拿到的是一个"Linux 中断号",很多刚从单片机转过来的朋友会把这个数字直接理解为硬件引脚号,其实不是。硬件中断线先接到中断控制器(GIC、IOAPIC、GPIO controller 等),中断控制器把多个来源仲裁后告诉 CPU,CPU 跳转到内核的异常入口,内核再通过 irq domain 把硬件中断源映射成一个 Linux 虚拟中断号。

这个虚拟中断号才是驱动里用的irq参数。它对应内核里的一个struct irq_desc,描述这个中断的当前状态、硬件芯片操作函数struct irq_chip,以及最重要的:一串struct irqaction。驱动申请中断,本质上就是往这个 action 链表上挂一个回调节点。

中断触发后的典型路径是:

硬件中断线 -> 中断控制器 -> CPU 异常入口 -> generic_handle_irq() -> 分发到 irq_desc -> 遍历 action 链表 -> 调用驱动注册的 handler

可以看到,驱动注册的 handler 只是这条链路最末尾的一个环节。所以申请中断时既要管好"我这个 handler 在 action 链表里的身份",也要理解它运行时的上下文是什么。

1.2 申请中断的对象:irq_desc、action 与 handler 表

struct irq_desc是内核中断管理的核心对象,每个虚拟中断号对应一个。驱动申请成功后,内核会分配一个struct irqaction,把handlerthread_fnflagsnamedev_id等信息填进去,然后挂到desc->action链表上。

如果多个驱动共享一根中断线,这个链表上就会有多个 action 节点。中断触发时,内核会按顺序调用每个 handler,直到某个 handler 返回IRQ_HANDLEDIRQ_WAKE_THREAD。如果所有 handler 都返回IRQ_NONE,内核会认为这是伪中断,次数多了会触发 spurious interrupt 检测机制,在/proc/interrupts里能看到相关统计异常增长。

所以request_irq这个 API 真正做的事情是:

  1. 校验irq号是否有效。
  2. 如果中断被占用,检查是否允许共享,以及双方 flag 是否兼容。
  3. 分配 irqaction,并初始化。
  4. 如果携带 trigger 相关 flag,调用irq_set_irq_type配置触发方式。
  5. 在 irq_desc 上注册完成,必要时使能该中断。

注意:request_irq只是一个封装,内核里真正的入口是request_threaded_irq,这一点下面会展开讲。

2. 那几个申请 API 怎么选:request_irq 还是线程化中断

2.1 API 签名与参数细节

驱动里最常用的几个接口是:

int request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags, const char *name, void *dev); int request_threaded_irq(unsigned int irq, irq_handler_t handler, irq_handler_t thread_fn, unsigned long flags, const char *name, void *dev); int devm_request_irq(struct device *dev, unsigned int irq, irq_handler_t handler, unsigned long flags, const char *name, void *dev); int devm_request_threaded_irq(struct device *dev, unsigned int irq, irq_handler_t handler, irq_handler_t thread_fn, unsigned long flags, const char *name, void *dev);

参数拆解一下:

  • irq:Linux 虚拟中断号,不是硬件引脚号。
  • handler:硬中断回调,也叫主 handler 或 top half。它运行在 hardirq 上下文,不能睡眠。
  • thread_fn:线程化中断回调,也叫 threaded handler。它在内核线程上下文运行,可以睡眠。
  • flagsIRQF_TRIGGER_*IRQF_SHAREDIRQF_ONESHOT等。
  • name:显示在/proc/interrupts里的设备名,主要用于调试。
  • dev:设备标识符,主要用来区分共享中断的多个节点。IRQF_SHARED场景下必须传一个非 NULL 且唯一的指针,通常是struct device *struct my_dev *

request_irq展开后就是:

request_threaded_irq(irq, handler, NULL, flags, name, dev)

也就是只注册主 handler,不注册线程回调。

2.2 devm_ 前缀的优势和清理顺序

devm_系列是设备资源管理(devres)的一部分。devm_request_irq在成功申请中断后,会把释放回调挂到struct device的 devres 链表上。设备 unbind、模块卸载、probe 失败时,内核会自动调用free_irq,不需要在 remove 函数里手动处理。

这里要特别提醒清理顺序的问题。devres 是逆序释放的,也就是说后申请的资源先被释放。假设 probe 里先devm_ioremapdevm_request_irq,那么设备注销时先free_irq,再 iounmap。这个顺序刚好是我们想要的:保证中断 handler 不再运行之后,映射地址还在,避免 handler 访问已释放的 ioremap 区域。

反过来,如果你在 probe 里用devm_request_threaded_irq之后才devm_kzalloc某个结构体,并且dev_id指向这个结构体,那么释放顺序就会变成:先释放结构体内存,再 free_irq。此时一旦中断还在跑,handler 就可能访问已经释放的内存。所以申请中断最好放在依赖的资源分配之后。

2.3 什么时候必须用 request_threaded_irq

老派驱动喜欢在硬中断里只做标记,把耗时逻辑扔给 tasklet 或者 workqueue。线程化中断出来之后,很多场景可以更简单:直接在thread_fn里做事情,因为它运行在普通进程上下文中,能够调用i2c_transferspi_syncmutex_lockmsleep这些会睡眠的接口。

典型场景包括:

  • 触摸屏控制器、I2C 接口的 IO 扩展芯片、传感器等,中断处理需要读 I2C/SPI 寄存器来确认事件。
  • 中断处理逻辑本身耗时较长,比如加密计算、数据包解析。
  • 硬件中断无法在 hardirq 里清掉,必须等到线程上下文操作后才能解除。

使用方式很简单:

ret = devm_request_threaded_irq(dev, irq, hard_handler, // 可为 NULL thread_handler, IRQF_TRIGGER_RISING | IRQF_ONESHOT, "demo-irq", ddev);

如果hard_handler传 NULL,内核会用一个默认的 primary handler,它直接返回IRQ_WAKE_THREAD,也就是完全靠线程处理。此时一般配IRQF_ONESHOT,保证线程处理期间该中断线处于屏蔽状态,避免同一中断反复触发导致线程挤爆。IRQF_ONESHOTIRQF_SHARED通常是互斥的,设计共享中断时不要硬凑这两个 flag。

另一个相关的 API 是request_any_context_irq,它会自动判断这个 irq 是普通硬中断还是嵌套线程中断(比如 i2c gpio 扩展芯片上的中断),然后选择合适的方式注册。如果驱动不确定中断来源,用这个更稳。

3. 申请之前必须拿到对的 IRQ 号:三种常见来源

3.1 平台设备与设备树:platform_get_irq

platform 设备驱动里最常见的获取 irq 方式是:

int irq; irq = platform_get_irq(pdev, 0); if (irq < 0) return irq;

这个接口负责把设备树或 ACPI 里描述的中断信息翻译成 Linux 虚拟中断号。设备树里通常这样写:

demo { compatible = "vendor,demo-irq"; reg = <0x0 0x40000000 0x0 0x1000>; interrupt-parent = <&gpio0>; interrupts = <4 IRQ_TYPE_EDGE_RISING>; };

驱动只需要关心platform_get_irq的返回值,不需要关心底层用的是 GIC 的 SPI/PPI,还是某个 GPIO controller 的 line。irq domain 会在背后完成映射。返回值小于 0 时,要原样返回错误码,新内核里这个接口自己会打印错误信息,驱动别再重复打一条没有上下文的dev_err

platform_get_irqindex参数对应interrupts属性里的第几个条目。一个设备有多个中断源时,就分别用 index 0、1、2 去取。

3.2 PCI 设备的 MSI-X / INTx 分配

PCI 驱动现在不建议直接读pdev->irq了。现代内核推荐这样:

int ret; int irq; ret = pci_alloc_irq_vectors(pdev, 1, 1, PCI_IRQ_ALL_TYPES); if (ret < 0) return ret; irq = pci_irq_vector(pdev, 0); if (irq < 0) { pci_free_irq_vectors(pdev); return irq; }

pci_alloc_irq_vectors会优先尝试 MSI-X,然后 MSI,最后退回到传统 INTx。申请到的 vector 个数在min_vecsmax_vecs之间。pci_irq_vector(pdev, 0)返回第一个 vector 对应的 Linux irq 号,这个号再传给devm_request_irqdevm_request_threaded_irq

这里有个容易忽视的点:MSI/MSI-X 不再是一根共享线,每个 vector 都是独立的,所以不需要也不能用IRQF_SHARED。PCI 驱动如果错误地在 MSI 场景使用IRQF_SHARED,反而可能在某些硬件上出问题。

3.3 GPIO 中断的申请方式

现代内核推荐用 GPIO descriptor API:

struct gpio_desc *gpio; int irq; gpio = devm_gpiod_get(dev, "irq", GPIOD_IN); if (IS_ERR(gpio)) return PTR_ERR(gpio); irq = gpiod_to_irq(gpio); if (irq < 0) return irq;

gpiod_to_irq返回的也是 Linux 虚拟中断号。拿到之后同样走request_threaded_irq。要注意,如果这个 GPIO 是挂在 I2C 总线上的扩展芯片提供的,那么它背后很可能是一个嵌套中断,request_any_context_irq在这种场景下更安全,因为它会自动适配。直接用request_irq的话,handler 里就不能调用 I2C 操作,必须走线程处理,否则睡眠在原子上下文里,CONFIG_DEBUG_ATOMIC_SLEEP会直接打印警告,运气好是死锁,运气不好是随机性崩溃。

4. 硬中断上下文中的"红线"和下半部机制

4.1 hardirq 里能做什么、绝不能做什么

hardirq handler 是驱动里最敏感的代码路径。它运行时,当前 CPU 的本地中断处于关闭状态,处理器可能不会响应新的本地中断。这意味着:一切会睡眠的操作都是违法的。

绝对不能做的事包括:

  • kmalloc(..., GFP_KERNEL),改用GFP_ATOMIC
  • mutex_lock(),普通自旋锁可以,但临界区要短。
  • msleepudelay这类延时要谨慎,udelay因为忙等可能可以,但长时间忙等会拖垮整个 CPU。
  • 直接copy_to_user/copy_from_user
  • 调用 i2c、spi、mmc 等可能睡眠的子系统接口。

hardirq handler 的正确姿势是:先操作寄存器把硬件中断源清掉或者屏蔽掉,收集必要状态,然后返回IRQ_WAKE_THREAD把重活丢给线程函数,或者标记一个 flag,交给 softirq/tasklet/workqueue 去跑。

4.2 tasklet/softirq 和线程化 handler 的差异

很多老驱动仍然大量使用 tasklet,这个机制有一定历史包袱。它挂在 softirq 上下文,相比 hardirq 宽松一些,但本质上还是不允许睡眠,只是在同一个 CPU 上延后执行。线程化 handler 则完全不同,优先级也可以自己指定,是可调度的内核线程。

机制运行上下文能否睡眠典型场景
hardirq硬件中断上下文,本地中断关闭不能快速应答、清中断、唤醒下半部
softirqsoftirq 上下文不能网络收发、块层处理、tasklet 底座
taskletsoftirq 上下文不能老驱动常用,逐渐被 workqueue/threaded irq 替代
threaded irq内核线程上下文I2C/SPI 交互、耗时处理
workqueue进程上下文更灵活的工作队列,可以指定调度策略

最近几年内核社区越来越不鼓励新代码使用 tasklet,很多驱动改成了 threaded irq 或者 workqueue。原因很简单:tasklet 和 softirq 要处理并行性,还要保证不能睡眠,审查成本高,调试起来也难。而线程化中断只要你确定使用场景,代码会直白很多。

提示:如果你在写新驱动,优先考虑thread_fn,只有在确认中断处理必须在极短时间内完成、且没有总线访问需求时,才把逻辑放进 hardirq。

4.3 返回值 IRQ_NONE / IRQ_HANDLED / IRQ_WAKE_THREAD 的含义

irqreturn_t本身是 int,驱动里常见三个值:

  • IRQ_NONE:这个中断不是本设备产生的,或者本次中断条件不成立。
  • IRQ_HANDLED:本设备已经处理了这个中断。
  • IRQ_WAKE_THREAD:主 handler 处理完,唤起对应的 threaded handler。

用错返回值的问题是真实存在的。如果一个中断源实际由你的设备触发,但 handler 返回了IRQ_NONE,内核会怀疑这是杂散中断,多次之后可能关闭该中断或者打印 warning。反过来,如果别人的中断被你的 handler 误判为IRQ_HANDLED,共享中断的链式调用就会提前终止,真正该处理的驱动反而拿不到通知。

所以 handler 的第一步通常就是读取硬件状态寄存器,确认是否属于自己。确认是,做处理后返回IRQ_HANDLED;不是,直接返回IRQ_NONEIRQ_WAKE_THREAD只在request_threaded_irq时才有意义,如果你只调request_irq,返回它会拿不到线程回调,因为线程回调压根不存在。

5. 实战:一个带线程化中断的 Platform 驱动完整例子

5.1 模块代码骨架

下面是一个最小但完整的 platform 驱动,中断来自设备树,硬中断只负责清状态,线程中断负责打印信息(实际项目里这里一般是 i2c/spi 操作)。

#include <linux/module.h> #include <linux/platform_device.h> #include <linux/interrupt.h> #include <linux/of.h> #include <linux/io.h> #define REG_STATUS 0x00 struct demo_dev { void __iomem *base; struct device *dev; int irq; }; static irqreturn_t demo_hard_handler(int irq, void *dev_id) { struct demo_dev *ddev = dev_id; /* 最小化处理:清硬件中断状态 */ writel(0x1, ddev->base + REG_STATUS); return IRQ_WAKE_THREAD; } static irqreturn_t demo_thread_handler(int irq, void *dev_id) { struct demo_dev *ddev = dev_id; /* 这里可以做需要睡眠的操作 */ dev_info(ddev->dev, "thread irq: %d\n", irq); return IRQ_HANDLED; } static int demo_probe(struct platform_device *pdev) { struct device *dev = &pdev->dev; struct demo_dev *ddev; int irq, ret; ddev = devm_kzalloc(dev, sizeof(*ddev), GFP_KERNEL); if (!ddev) return -ENOMEM; ddev->dev = dev; ddev->base = devm_platform_ioremap_resource(pdev, 0); if (IS_ERR(ddev->base)) return PTR_ERR(ddev->base); irq = platform_get_irq(pdev, 0); if (irq < 0) return irq; ddev->irq = irq; ret = devm_request_threaded_irq(dev, irq, demo_hard_handler, demo_thread_handler, IRQF_TRIGGER_RISING | IRQF_ONESHOT, "demo-irq", ddev); if (ret) return ret; return 0; } static const struct of_device_id demo_of_match[] = { { .compatible = "vendor,demo-irq" }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(of, demo_of_match); static struct platform_driver demo_driver = { .probe = demo_probe, .driver = { .name = "demo-irq", .of_match_table = demo_of_match, }, }; module_platform_driver(demo_driver); MODULE_LICENSE("GPL");

这个例子里demo_hard_handler只做一件事:清硬件状态位,然后叫醒线程。thread_fn里才有真正的业务处理。用devm_系列后,remove函数理论上不需要再调free_irq,但前提是你在 probe 里保证所有依赖资源在 irq 之前分配好。

5.2 probe 中的申请顺序与清理顺序

probe 里申请中断的顺序不是随便写的。我习惯的顺序是:

  1. 分配私有结构体。
  2. ioremap 寄存器、分配 DMA buffer、注册 misc/char 设备等。
  3. 初始化硬件,包括屏蔽中断、配置寄存器。
  4. 申请中断。
  5. 最后统一使能硬件中断。

顺序的核心原则是:设备在中断 handler 被调用之前,必须处于一个可以被安全处理的状态。如果硬件还没初始化完,寄存器还不可访问,此时中断一旦触发,handler 里访问baseddev就可能崩溃。

清理时,虽然devm会自动释放,但你在模块 remove 回调里还是要注意顺序。比如你手动注册了 class、字符设备,最后再手动free_irq,就要先确保硬件已经不再产生中断,然后再释放中断,否则硬件还在不停触发,但中断已经被移除,共享线路上其他设备会被你拖累。

free_irq的行为是同步等待正在执行的 handler 跑完。如果你在 handler 执行时持有某个锁,而free_irq的路径上也尝试拿同一个锁,就会死锁。一个常见错误是在 remove 里持有mutex_lock(&ddev->lock)之后才调free_irq,而 handler 里也依赖这个锁。正确做法是先free_irq,再拿锁清理。

5.3 怎么验证中断确实被申请到

驱动加载成功后,第一件事就是看/proc/interrupts

cat /proc/interrupts

你会看到类似这样的输出:

CPU0 CPU1 16: 1236 981 GICv3 38 Level demo-irq

右边那个名字就是request_threaded_irq里的devname。如果没有这一行,说明申请失败,或者驱动根本没 probe。触发一次硬件事件后,再cat /proc/interrupts,观察计数增加。如果计数没涨,要么硬件事件没到达中断控制器,要么触发方式配置反了,要么你的设备中断压根没被使能。

更细的追踪可以看 tracepoint:

mount -t tracefs nodev /sys/kernel/tracing echo 0 > /sys/kernel/tracing/tracing_on echo irq_handler_entry > /sys/kernel/tracing/set_event echo irq_handler_exit >> /sys/kernel/tracing/set_event echo 1 > /sys/kernel/tracing/tracing_on # 触发一次中断 cat /sys/kernel/tracing/trace

通过 trace 可以看到每次中断进入时调用的 handler 地址和名字,以及返回的irqreturn值。调试IRQ_NONE/IRQ_HANDLED乱返问题的时候,这个命令比看代码快得多。

6. 踩坑笔记:申请中断时最容易翻车的几个点

6.1 IRQF_SHARED 的强制约束

共享中断线在 x86 平台和一些老式总线设备上还很常见。用IRQF_SHARED时有几个硬性约束:

  • dev_id参数必须是非 NULL,而且最好唯一。free_irq的时候必须传同一个dev_id,内核靠它精确定位要移除的 action 节点。
  • 同一个中断线上的所有驱动必须都设置IRQF_SHARED,只要有一个没设,后续申请就返回-EBUSY
  • trigger 方式必须兼容。一般情况下共享中断都是电平触发,因为边沿触发在共享线路上极易丢中断。
  • 所有共享 handler 都要有"这中断可能是别人家的"的觉悟,第一步读自己的状态寄存器,确认不是自己的就快速返回IRQ_NONE

我见过一个项目里两个驱动共享一根 GPIO 中断线,结果一个用 eudev 触发,一个用电平触发,硬件上电平信号和边沿逻辑打架,最终系统出现偶发中断风暴。排查了一周,最后发现是设备树里一个节点写了IRQ_TYPE_EDGE_RISING,另一个驱动又用IRQF_TRIGGER_LOW去申请。所以共享中断前,先坐在一起把 trigger 对齐。

6.2 触发方式与中断控制器配置不匹配

request_threaded_irq的 flags 里如果带了IRQF_TRIGGER_*,内核会尝试通过irq_set_irq_type配置中断控制器。如果控制器不支持这个配置,返回-EINVAL。如果支持但和平台定义不一致,行为就很怪:有时候 interrupt 永远不来,有时候莫名其妙连触发好几次。

设备树里已经写了触发方式,驱动里再传一个不同触发 flag,这俩到底谁说了算?实际是驱动里的 flag 会覆盖平台默认设置。为了减少混乱,我推荐设备树里写明中断触发方式,驱动里申请时要么传同样的 flag,要么干脆不传 trigger flag,保持一致性。

提示:IRQF_TRIGGER_NONE不完全是"没有触发方式",它等于 0,表示驱动不要求修改触发类型,沿用之前配置。如果设备树里配好了,驱动里可以传这个值。

6.3 free_irq 与设备正在跑的竞态

free_irq会等待正在执行的 handler 返回,也等待 thread_fn 退出。这看起来安全,但要注意 "handler 正在跑" 不代表"设备已经不会产生新中断了"。

假设你的设备硬件处于 enable 状态,中断源源不断。你在 remove 回调里直接devm_free_irq(或者依赖 devm 触发),硬件还在狂发中断。如果这是独占中断,irq 已经被移除,硬件状态寄存器里的 pending 位没人清,等下次设备 resurrect 时可能跳过一次事件。如果这是共享中断,别家驱动会被你连累。

推荐流程是:先 disable 设备中断源(操作寄存器屏蔽中断),再做同步(比如synchronize_irq或者干脆在free_irq前调一次),最后释放中断。

disable_irq/enable_irq是另一个容易出问题的点。disable_irq是同步等待 handler 跑完的,如果你在硬件中断上下文里调disable_irq,自己就把自己卡死了。这时候只能用disable_irq_nosync

6.4 申请失败 errno 的含义与排查路径

request_threaded_irq返回负数时,常见几个错误码的意思要知道:

errno含义
-EINVALirq 无效、handler 和 thread_fn 都为空、trigger 类型非法
-EBUSY中断已被申请且未开共享,或者共享 flag 不一致
-ENOMEM内核分配 irqaction 失败,常见于内存压力大的时候

-EINVAL特别常见的一个原因是:从platform_get_irq拿到的 irq 已经小于 0,你还把它传给request_irq。所以拿到 irq 之后要立刻判负,别等注册时才暴露。

排查路径我总结成三句话:

  1. 先看dmesg里有没有genirq相关的报错,很多问题内核已经告诉你原因了。
  2. 再看/proc/interrupts里有没有这一行,没有说明接口就没走通,回到platform_get_irq返回值和设备树配置上。
  3. 最后还是查不到,就加 trace 看irq_handler_entry有没有进来,进来之后返回值是什么。这一步能快速区分是"中断没进来"还是"handler 跑飞了"。

驱动申请中断这事儿,表面上就是几个 API 组合一下,真正难的是理解硬件和内核之间的那层映射关系,以及 handler 运行的上下文边界。实际项目里我经常看到新手在 hardirq 里挪一段 i2c 读取代码,跑起来就 panic,回头看注释里写着 "may sleep",就只能怪自己没把这个边界吃透。多花点时间把/proc/interrupts、tracepoint 和 errno 含义吃透,调试效率会高很多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询