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 知识结构总览

下面这张图把中断申请与释放的核心流程串起来了:

中断申请与释放知识结构 request_irq() irq 中断号 handler 函数 flags 标志 name 名称 dev 设备标识 触发方式 共享属性 其他标志 上升沿 下降沿 高电平 低电平 free_irq() 必须在进程上下文调用 dev_id 必须与申请时一致 共享中断会同步等待完成 核心原则:申请与释放成对出现,生命周期与设备绑定

4.5 实战建议

最后,分享几个我多年积累的经验:

  • 检查返回值:request_irq() 返回 0 才表示成功。非零值要打印错误码,方便定位。
  • 中断号有效性:申请前可以用 irq_can_request() 检查中断号是否可用。
  • 调试利器:/proc/interrupts 和 /proc/irq/<irq>/ 目录能帮你确认中断是否注册成功。
  • 线程化中断:如果处理函数需要睡眠,用 request_threaded_irq() 代替。

小技巧:调试阶段可以在 flags 里加上 IRQF_NO_AUTOEN,手动控制中断使能时机,避免刚注册就触发意外中断。

嗯,中断申请与释放就聊到这儿。记住:request_irq() 是入口,free_irq() 是出口,中间是你的处理逻辑。把这套流程理清楚,中断驱动就成功了一半。