4. 中断申请与释放:request_irq() 与 free_irq() 深度解析
中断处理,说白了就是硬件告诉 CPU:「嘿,有事儿找你!」。但 CPU 怎么知道该找谁处理?这就需要驱动开发者把中断号和对应的处理函数「注册」到内核里。今天咱们就聊聊这个注册过程——request_irq() 和 free_irq() 这对好兄弟。
核心要点:中断申请是驱动与硬件建立「紧急联络通道」的关键一步。申请错了,轻则功能异常,重则系统崩溃。
4.1 request_irq():把中断「绑定」到你的函数
先看原型,这是 Linux 内核中最常用的中断注册接口:
int request_irq(unsigned int irq,
irq_handler_t handler,
unsigned long flags,
const char *name,
void *dev);
五个参数,各有各的讲究。我一个个说。
| 参数 | 含义 | 注意事项 |
|---|---|---|
irq | 中断号 | 硬件决定,可通过设备树或 ACPI 获取 |
handler | 中断处理函数 | 上半部,运行在中断上下文 |
flags | 中断标志 | 触发方式、共享属性等 |
name | 设备名称 | 用于 /proc/interrupts 显示 |
dev | 设备标识 | 共享中断时用于区分,通常传设备结构体 |
irq 参数——中断号。这个号不是随便写的。我记得刚入行时,有个同事直接写了个数字 17,结果怎么都触发不了。后来发现板子上的 GPIO 中断号是动态分配的。所以,我建议你从设备树里读,或者用 platform_get_irq() 这类接口获取。
handler 参数——你的中断处理函数。原型长这样:
irqreturn_t my_handler(int irq, void *dev_id);
返回值只有两种:IRQ_HANDLED(我处理了)和 IRQ_NONE(不关我事)。共享中断时,内核会遍历所有注册的处理函数,直到有人返回 IRQ_HANDLED。
name 参数——纯粹为了调试。你可以在 /proc/interrupts 里看到它。我习惯用驱动名加设备实例,比如 "my_uart.0",排查问题时一目了然。
dev 参数——这个容易被忽略,但很重要。共享中断时,内核靠它来区分不同的注册者。free_irq() 也需要传同样的值才能正确释放。我通常直接传设备的私有数据结构指针。
我的习惯:在 probe 函数里申请中断,在 remove 函数里释放。这样生命周期清晰,不容易漏掉。
4.2 free_irq():用完记得还回去
有借有还,再借不难。原型:
void free_irq(unsigned int irq, void *dev_id);
注意两点:
- 必须在进程上下文调用(不能在中断处理函数里释放中断)
- 共享中断时,
dev_id必须和request_irq()传的值一致
我曾经犯过一个错:在模块卸载时忘了调 free_irq()。结果下次 insmod 时,request_irq() 返回 -EBUSY,驱动死活加载不上。查了半天才发现是上次没释放干净。
注意:如果中断是共享的,free_irq() 会同步等待所有正在执行的中断处理函数完成。所以不要在自旋锁里调它,否则可能死锁。
4.3 中断标志:控制中断行为的「开关」
flags 参数是个位掩码,可以组合使用。最常用的几类:
4.3.1 触发方式标志
| 标志 | 含义 | 适用场景 |
|---|---|---|
IRQF_TRIGGER_RISING | 上升沿触发 | 按键、脉冲信号 |
IRQF_TRIGGER_FALLING | 下降沿触发 | 同上,看硬件设计 |
IRQF_TRIGGER_HIGH | 高电平触发 | 电平敏感设备 |
IRQF_TRIGGER_LOW | 低电平触发 | 同上 |
IRQF_TRIGGER_NONE | 不指定 | 由硬件或固件配置 |
触发方式必须和硬件匹配。我遇到过一块板子,GPIO 控制器默认是低电平触发,但设备树里配成了上升沿。结果中断要么不触发,要么疯狂触发。嗯,电平触发和边沿触发搞混了,后果很严重。
4.3.2 共享中断标志
IRQF_SHARED——允许多个设备共享同一个中断号。使用条件:
- 硬件必须支持中断线共享(通常是电平触发)
- 所有共享者都必须设置
IRQF_SHARED - 每个处理函数必须检查自己的设备是否真的产生了中断
共享中断的典型场景是 PCI 设备。我记得调试一个网卡驱动时,发现中断处理函数被调用了但设备没产生中断,就是因为没检查 dev_id,误处理了别人的中断。
4.3.3 其他常用标志
| 标志 | 作用 |
|---|---|
IRQF_NO_SUSPEND | 系统休眠时不关闭该中断 |
IRQF_NO_THREAD | 禁止将中断线程化 |
IRQF_ONESHOT | 中断处理期间屏蔽该中断线 |
IRQF_NO_AUTOEN | 申请后不自动使能中断 |
IRQF_ONESHOT 我经常在线程化中断里用。它保证中断处理期间不会再次触发,省得自己在处理函数里手动屏蔽。
避坑指南:我曾经在电池驱动的休眠路径里用了 IRQF_NO_SUSPEND,结果系统休眠时电池电量突变,中断唤醒系统,导致休眠失败。后来改成在 suspend 回调里手动控制中断使能,才解决问题。
4.4 知识结构总览
下面这张图把中断申请与释放的核心流程串起来了:
4.5 实战建议
最后,分享几个我多年积累的经验:
- 检查返回值:
request_irq()返回 0 才表示成功。非零值要打印错误码,方便定位。 - 中断号有效性:申请前可以用
irq_can_request()检查中断号是否可用。 - 调试利器:
/proc/interrupts和/proc/irq/<irq>/目录能帮你确认中断是否注册成功。 - 线程化中断:如果处理函数需要睡眠,用
request_threaded_irq()代替。
小技巧:调试阶段可以在 flags 里加上 IRQF_NO_AUTOEN,手动控制中断使能时机,避免刚注册就触发意外中断。
嗯,中断申请与释放就聊到这儿。记住:request_irq() 是入口,free_irq() 是出口,中间是你的处理逻辑。把这套流程理清楚,中断驱动就成功了一半。