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_HANDLEDIRQ_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);

这里有几个坑,我一个个说:

  1. 必须在进程上下文调用。不能在中断上下文里调用 free_irq(),否则会死锁。为什么?因为 free_irq() 要等待正在执行的中断处理函数完成,如果在中断里调用自己,就成死循环了。
  2. dev_id 必须和注册时一致。对于非共享中断,可以传 NULL。但对于共享中断,必须传注册时用的那个 dev_id,否则内核找不到对应的处理函数。
  3. 确保中断已经禁用。在调用 free_irq() 之前,最好先调用 disable_irq() 或 disable_irq_nosync(),防止在注销过程中又有新的中断进来。
  4. 注意调用顺序。先释放中断,再释放中断中用到的资源。我曾经见过有人先 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,最后发现就是少写了一行清除寄存器的代码。

好了,中断注册与注销就讲到这里。下一节我们会深入中断处理函数的编写技巧,包括上半部和下半部的拆分、以及如何在中断里安全地访问硬件。到时候见。