中断注册与注销: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 中断注册与注销的完整流程
下面这张图展示了中断从注册到注销的完整生命周期:
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 不能省,注销时要在正确的上下文调用。把这些搞明白了,中断编程就成功了一半。