4. 中断注册与注销:request_irq()函数详解、devm_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:中断号。这个不用多解释,上一节讲过了。
- handler:中断处理函数指针。注意返回值类型是
irqreturn_t,一般返回IRQ_HANDLED或IRQ_NONE。 - flags:中断标志位。常用的有
IRQF_TRIGGER_RISING(上升沿触发)、IRQF_TRIGGER_FALLING(下降沿触发)、IRQF_SHARED(共享中断)等。 - name:设备名称。这个会在
/proc/interrupts里显示,方便调试。 - dev:设备标识。对于共享中断来说,这个参数至关重要,它是区分不同设备的依据。
重点来了: 对于共享中断(IRQF_SHARED),dev 参数绝对不能为 NULL。否则内核无法区分是哪个设备触发了中断,free_irq() 的时候也会出问题。
我在项目中遇到过一件事:有个同事写摄像头驱动,用了共享中断,但 dev 参数传了个 NULL。结果一跑起来,中断疯狂触发,但根本不知道是哪个设备引起的。查了两天才发现是这个问题。嗯,这种坑踩过一次就记住了。
4.2 devm_request_irq() 的使用
这个函数是 request_irq() 的“资源管理版”。它多了一个 struct device *dev 参数:
int devm_request_irq(struct device *dev,
unsigned int irq,
irq_handler_t handler,
unsigned long flags,
const char *name,
void *dev_id);
好处是什么?自动释放。当设备被卸载时,内核会自动帮你调用 free_irq()。你想想看,这能省多少事?
我个人强烈建议:能用 devm 就用 devm。除非你有特殊需求,比如中断的生命周期和设备生命周期不一致。
小技巧: 使用 devm_request_irq() 后,你的 remove() 函数里就不用写 free_irq() 了。代码更简洁,也少了一个出错点。
我曾经接手过一个项目,驱动里手动管理各种资源,probe 函数里注册了中断、申请了内存、映射了寄存器,但 remove 函数里漏掉了 free_irq()。结果每次卸载模块再加载,中断就注册失败。改成 devm 系列函数后,问题迎刃而解。
4.3 free_irq() 注意事项
注销中断看似简单,其实暗藏玄机。函数原型:
void free_irq(unsigned int irq, void *dev_id);
这里有几个坑,我一个个说:
- 必须在进程上下文调用。不能在中断上下文里调用 free_irq(),否则会死锁。为什么?因为 free_irq() 要等待正在执行的中断处理函数完成,如果在中断里调用自己,就成死循环了。
- dev_id 必须和注册时一致。对于非共享中断,可以传 NULL。但对于共享中断,必须传注册时用的那个 dev_id,否则内核找不到对应的处理函数。
- 确保中断已经禁用。在调用 free_irq() 之前,最好先调用 disable_irq() 或 disable_irq_nosync(),防止在注销过程中又有新的中断进来。
- 注意调用顺序。先释放中断,再释放中断中用到的资源。我曾经见过有人先 kfree 了某个数据结构,然后才 free_irq,结果中断处理函数里还在访问那个已经释放的内存——直接 oops。
警告: 千万不要在中断处理函数里调用 free_irq()!这会导致内核 panic。如果你确实需要在中断里注销中断,可以用 workqueue 把 free_irq() 放到进程上下文去执行。
4.4 实际使用示例
来看一个完整的 Camera 驱动中断注册示例:
static irqreturn_t camera_irq_handler(int irq, void *dev_id)
{
struct camera_device *cam = (struct camera_device *)dev_id;
// 读取中断状态寄存器
u32 status = readl(cam->base + CAM_IRQ_STATUS);
if (status & CAM_IRQ_FRAME_DONE) {
// 帧完成中断
complete(&cam->frame_done);
writel(CAM_IRQ_FRAME_DONE, cam->base + CAM_IRQ_CLEAR);
return IRQ_HANDLED;
}
return IRQ_NONE;
}
static int camera_probe(struct platform_device *pdev)
{
struct camera_device *cam;
int ret, irq;
cam = devm_kzalloc(&pdev->dev, sizeof(*cam), GFP_KERNEL);
// ... 其他初始化代码 ...
irq = platform_get_irq(pdev, 0);
if (irq < 0)
return irq;
// 使用 devm_request_irq,自动管理生命周期
ret = devm_request_irq(&pdev->dev, irq,
camera_irq_handler,
IRQF_TRIGGER_RISING,
"camera_drv",
cam);
if (ret) {
dev_err(&pdev->dev, "Failed to request IRQ\n");
return ret;
}
return 0;
}
// 注意:remove 函数里不需要 free_irq,devm 会自动处理
这个例子展示了几个要点:
- 中断处理函数里先读状态寄存器,确认是不是自己的中断
- 处理完后清除中断标志位(这个很容易忘)
- 用 devm_request_irq 简化资源管理
- dev_id 传的是设备结构体指针,方便在中断处理函数里访问设备数据
4.5 中断注册的常见错误
| 错误类型 | 现象 | 解决方法 |
|---|---|---|
| 中断号错误 | request_irq 返回 -EINVAL | 检查 platform_get_irq 返回值,或设备树中的中断描述 |
| 共享中断 dev_id 为 NULL | 中断无法正常工作,或 free_irq 失败 | 确保 dev_id 指向有效的设备结构体 |
| 中断处理函数耗时过长 | 系统响应变慢,甚至触发 watchdog | 将耗时操作移到 tasklet 或 workqueue 中 |
| 忘记清除中断标志 | 中断反复触发,CPU 占用 100% | 在中断处理函数末尾清除硬件中断标志位 |
嗯,这张表里的错误,我基本都犯过。尤其是那个忘记清除中断标志的坑,当年调试一个 sensor 驱动,中断来了就不停,CPU 直接飙到 100%。我还以为是硬件问题,查了半天 datasheet,最后发现就是少写了一行清除寄存器的代码。
好了,中断注册与注销就讲到这里。下一节我们会深入中断处理函数的编写技巧,包括上半部和下半部的拆分、以及如何在中断里安全地访问硬件。到时候见。