中断注册与注销:request_irq()、free_irq()、中断标志位详解

中断注册,说白了就是告诉内核:“这个中断号归我管了,来了信号就叫我。” 注销则是反过来:“我用完了,你找别人吧。” 这两个接口是中断编程的起点和终点,看似简单,但坑不少。我见过不少新手在这上面翻车,今天就好好聊聊。

3.1 request_irq():把中断“绑定”到你的驱动

函数原型长这样:

int request_irq(unsigned int irq,
                irq_handler_t handler,
                unsigned long flags,
                const char *name,
                void *dev_id);

五个参数,一个都不能马虎。我一个个说。

参数 含义 个人经验
irq 中断号,硬件决定的 别写死,用平台设备或设备树传
handler 中断处理函数指针 注意函数签名,错了编译能过但运行崩
flags 中断标志位,控制行为 这是重点,下面细讲
name 设备名,/proc/interrupts 里能看到 起个有意义的名字,方便调试
dev_id 设备标识,共享中断时必填 很多人忽略这个,共享中断就出问题

返回值: 0 表示成功,负数表示错误。常见错误码:-EBUSY(中断已被占用)、-EINVAL(参数非法)。

3.2 中断标志位详解:flags 参数的门道

flags 是 request_irq() 的灵魂。不同的标志位决定了中断的行为方式。我整理了一张表,方便你对照。

标志位 含义 典型场景
IRQF_SHARED 允许共享中断线 多个设备共用一根中断线
IRQF_TRIGGER_RISING 上升沿触发 GPIO 按键、边沿敏感设备
IRQF_TRIGGER_FALLING 下降沿触发 某些传感器中断输出
IRQF_TRIGGER_HIGH 高电平触发 传统 PCI 设备、电平敏感外设
IRQF_TRIGGER_LOW 低电平触发 某些电源管理芯片
IRQF_ONESHOT 中断处理期间屏蔽本中断 threaded IRQ 必须搭配
IRQF_NO_SUSPEND 系统休眠时不关中断 唤醒源设备

我的习惯: 电平触发和边沿触发别混用。曾经有个项目,硬件设计改了触发方式,驱动没同步更新,结果中断风暴直接把系统打挂了。排查了一整天才找到原因。

3.3 共享中断:IRQF_SHARED 的正确用法

共享中断是嵌入式系统里的常态。一根中断线上挂多个设备,内核怎么知道该叫谁?

关键就在 dev_id 参数。共享中断时,每个注册者必须传一个唯一的 dev_id。中断到来时,内核会遍历所有注册在该中断线上的 handler,直到某个 handler 返回 IRQ_HANDLED。

/* 共享中断注册示例 */
static struct my_device dev_a, dev_b;

request_irq(irq_num, my_handler, IRQF_SHARED | IRQF_TRIGGER_FALLING,
            "my_device", &dev_a);

request_irq(irq_num, my_handler, IRQF_SHARED | IRQF_TRIGGER_FALLING,
            "my_device", &dev_b);

注意: 共享中断的 handler 必须检查中断是否来自自己的设备。如果发现不是自己的中断,要返回 IRQ_NONE,让内核继续调用下一个 handler。我曾经见过一个驱动,共享中断里直接返回 IRQ_HANDLED,结果把其他设备的中断给吞了,导致那个设备完全没法用。

3.4 free_irq():优雅地释放中断资源

注销中断比注册简单,但同样有讲究。

void free_irq(unsigned int irq, void *dev_id);

两个要点:

  • 非共享中断: dev_id 可以传 NULL,但建议还是传注册时用的那个指针。
  • 共享中断: dev_id 必须 传注册时用的那个唯一标识,否则内核不知道要释放哪个。

调用时机: free_irq() 会等待当前正在执行的中断处理函数完成,然后才返回。所以不要在中断上下文里调用 free_irq(),否则会死锁。我见过有人在中断处理函数里调用 free_irq(),结果系统直接卡死。

3.5 中断注册与注销的完整流程

下面这张图展示了中断从注册到注销的完整生命周期:

中断注册与注销完整流程 驱动初始化 request_irq() 注册 中断到来,handler 执行 驱动卸载/移除 free_irq() 注销 驱动退出完成 注意:free_irq() 会等待当前 handler 执行完毕,不要在中断上下文调用

3.6 避坑指南:我踩过的那些坑

做中断开发这么多年,有些坑是反复出现的。我挑几个典型的说说。

坑一:中断号写死

我曾经在一个项目里把中断号直接 #define 成 42,结果换了一个硬件平台,中断号变成了 55,驱动完全没法用。后来改用 platform_get_irq() 从设备树获取,再也没出过问题。

坑二:共享中断不传 dev_id

有个同事在共享中断里传了 NULL 作为 dev_id,结果 free_irq() 的时候内核不知道要释放哪个,直接报错。排查了半天才发现是 dev_id 的问题。

坑三:中断处理函数里调用可能导致睡眠的函数

中断上下文不能睡眠,这是基本常识。但我见过有人在中断处理函数里调用了 kmalloc(GFP_KERNEL),结果系统直接报 "scheduling while atomic"。后来改用了 GFP_ATOMIC 才解决。

3.7 调试技巧:如何验证中断注册是否成功

注册完中断,怎么确认它真的生效了?我一般用这几个方法:

  • 查看 /proc/interrupts: 看看你的中断号有没有出现,计数有没有增加。
  • 查看 /proc/irq/{irq_num}/: 每个中断号都有一个目录,里面可以看到注册的 handler 信息。
  • 使用 ftrace: 跟踪 irq_handler_entry 和 irq_handler_exit 事件,能看到中断处理函数的执行时间。

我的调试习惯: 在 handler 里加一个计数器,每次中断进来就加 1。然后在 read() 接口里把这个计数器暴露给用户空间。这样用户态程序可以实时监控中断是否正常触发。这个方法简单有效,我一直在用。

好了,中断注册与注销的核心内容就这些。记住:注册时 flags 要选对,共享中断 dev_id 不能省,注销时要在正确的上下文调用。把这些搞明白了,中断编程就成功了一半。