2. 中断注册与释放:request_irq()、free_irq()、中断标志位

好,咱们接着聊中断。上一节我讲了中断的基本概念,这一节咱们直接上手——怎么注册一个中断,怎么释放它,以及那些让人眼花缭乱的中断标志位到底该怎么选。

说实话,我刚入行那会儿,第一次写中断处理程序,就踩了个大坑。我注册了一个共享中断,结果没处理好标志位,导致两个驱动互相打架,系统直接死机。从那以后,我对 request_irq() 的每个参数都格外小心。

2.1 request_irq():中断注册的核心

先看原型,这个函数你以后会天天见:

int request_irq(unsigned int irq, 
                irq_handler_t handler,
                unsigned long flags,
                const char *name, 
                void *dev);

五个参数,一个都不能马虎。我一个个说。

2.1.1 irq:中断号

这个好理解,就是你要注册的中断线编号。但注意,在设备树时代,中断号往往是动态分配的。我个人习惯用 platform_get_irq() 来获取,而不是硬编码。你想想看,换个板子中断号就变了,硬编码不是给自己挖坑吗?

2.1.2 handler:中断处理函数

这是你的核心逻辑。原型长这样:

irqreturn_t (*irq_handler_t)(int irq, void *dev_id);

返回值只有两个:IRQ_HANDLEDIRQ_NONE。前者表示你处理了这个中断,后者表示这不是你的设备触发的中断(共享中断时有用)。

重要:处理函数里不能做耗时操作!我见过有人直接在中断里做 msleep(),结果系统卡成狗。记住,中断上下文不能睡眠,不能获取信号量,不能做任何可能阻塞的事。

2.1.3 flags:中断标志位

这部分是重点,也是容易出问题的地方。我整理了一张表:

标志位 含义 使用场景
IRQF_TRIGGER_RISING 上升沿触发 边沿敏感型设备,如按键
IRQF_TRIGGER_FALLING 下降沿触发 同上,但触发边沿不同
IRQF_TRIGGER_HIGH 高电平触发 电平敏感型设备
IRQF_TRIGGER_LOW 低电平触发 同上
IRQF_SHARED 共享中断 多个设备共用一根中断线
IRQF_ONESHOT 一次性触发 中断处理期间屏蔽该中断
IRQF_NO_SUSPEND 不休眠 系统休眠时仍能响应中断

这里我重点说两个:

IRQF_SHARED: 共享中断时,你的 dev_id 参数必须唯一,不能传 NULL。内核靠这个来区分是哪个设备触发了中断。我曾经犯过这个错,两个驱动都用 NULL,结果中断来了,两个处理函数都跑一遍,还互相抢资源。

IRQF_ONESHOT: 这个标志位很有意思。它保证在中断处理函数返回之前,不会再次触发同一个中断。对于边沿触发的中断,这能有效防止中断风暴。我建议你在处理耗时较长的中断时加上它。

2.1.4 name 和 dev

name 是设备名,会在 /proc/interrupts 里显示,方便调试。dev 是设备标识,共享中断时必须唯一。

2.2 free_irq():优雅地释放

有注册就有释放。原型很简单:

void free_irq(unsigned int irq, void *dev_id);

注意两点:

  • 必须在模块卸载或设备移除时调用
  • 共享中断时,dev_id 必须和注册时一致

警告: 不要在中断处理函数里调用 free_irq()!这会导致死锁。我曾经在调试时这么干过,结果内核直接 panic。正确的做法是在进程上下文中释放,比如在驱动的 remove 回调里。

2.3 一个完整的例子

说了这么多,咱们看个实际例子。这是我写的一个 GPIO 按键驱动片段:

static irqreturn_t button_irq_handler(int irq, void *dev_id)
{
    struct button_dev *bdev = (struct button_dev *)dev_id;
    
    /* 这里只做最少的处理,实际工作交给工作队列 */
    schedule_work(&bdev->work);
    
    return IRQ_HANDLED;
}

static int button_probe(struct platform_device *pdev)
{
    struct button_dev *bdev;
    int irq, ret;
    
    bdev = devm_kzalloc(&pdev->dev, sizeof(*bdev), GFP_KERNEL);
    if (!bdev)
        return -ENOMEM;
    
    irq = platform_get_irq(pdev, 0);
    if (irq < 0)
        return irq;
    
    /* 注册中断,使用上升沿触发 */
    ret = request_irq(irq, button_irq_handler,
                      IRQF_TRIGGER_RISING | IRQF_ONESHOT,
                      "my-button", bdev);
    if (ret) {
        dev_err(&pdev->dev, "Failed to request IRQ\n");
        return ret;
    }
    
    bdev->irq = irq;
    platform_set_drvdata(pdev, bdev);
    
    return 0;
}

static int button_remove(struct platform_device *pdev)
{
    struct button_dev *bdev = platform_get_drvdata(pdev);
    
    free_irq(bdev->irq, bdev);
    return 0;
}

你看,我用了 IRQF_ONESHOT,防止按键抖动导致的中断风暴。而且 dev_id 传的是设备结构体指针,不是 NULL。这样在共享中断时,内核能准确找到对应的设备。

2.4 调试技巧

注册完中断后,怎么确认它工作正常?我一般用这几招:

  • 查看 /proc/interrupts: 看看你的中断号有没有注册,触发次数对不对
  • 查看 /proc/irq/<irq_num>/: 每个中断的详细信息,包括 CPU 亲和性
  • 用 ftrace: 跟踪中断处理函数的执行时间

小技巧: 如果你发现中断触发次数异常多,很可能是电平触发和边沿触发搞混了。电平触发在电平持续期间会一直触发,而边沿触发只触发一次。我遇到过有人把高电平触发当成上升沿触发,结果中断每秒触发几万次,CPU 直接 100%。

2.5 避坑指南

最后,总结几个我踩过的坑:

  • 中断上下文不能睡眠: 别用 kmalloc(GFP_KERNEL),用 GFP_ATOMIC
  • 共享中断的 dev_id 必须唯一: 传 NULL 等于给自己挖坑
  • 释放中断要在进程上下文: 别在中断处理函数里调 free_irq()
  • 中断标志位要和硬件匹配: 查清楚你的设备是电平触发还是边沿触发

嗯,这一节就到这儿。中断注册看似简单,但细节决定成败。下一节咱们聊聊中断下半部,那是真正体现水平的地方。