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_HANDLED 和 IRQ_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()
- 中断标志位要和硬件匹配: 查清楚你的设备是电平触发还是边沿触发
嗯,这一节就到这儿。中断注册看似简单,但细节决定成败。下一节咱们聊聊中断下半部,那是真正体现水平的地方。