3. Linux中断子系统:中断注册API、中断处理函数与中断上下文
中断处理,说白了就是CPU被硬件“打断”后怎么响应。很多初学者觉得中断就是写个函数挂上去,其实里面的门道不少。我刚开始做驱动时,就因为没搞懂上半部和下半部的区别,导致系统卡死,最后查了三天才找到原因。
这一节,我们重点讲三个东西:怎么注册中断、中断处理函数怎么写、以及中断上下文里能干什么不能干什么。
3.1 中断注册API:request_irq 与 devm_request_irq
注册中断,最常用的就是 request_irq。它的原型长这样:
int request_irq(unsigned int irq,
irq_handler_t handler,
unsigned long flags,
const char *name,
void *dev);
参数不多,但每个都很关键:
| 参数 | 说明 | 注意事项 |
|---|---|---|
| irq | 中断号 | 通常从设备树或平台数据获取 |
| handler | 中断处理函数指针 | 上半部,在中断上下文中执行 |
| flags | 中断标志 | 如 IRQF_TRIGGER_RISING、IRQF_SHARED |
| name | 设备名称 | 用于 /proc/interrupts 显示 |
| dev | 设备标识 | 共享中断时用于区分 |
我的习惯:能用 devm_request_irq 就别用 request_irq。前者是资源管理版本,驱动卸载或设备移除时会自动释放中断,省得你忘了调用 free_irq。我在一个项目里就因为忘记释放中断,导致第二次加载驱动时直接崩溃。
devm_request_irq 的用法几乎一样:
int devm_request_irq(struct device *dev,
unsigned int irq,
irq_handler_t handler,
unsigned long flags,
const char *name,
void *dev_id);
唯一的区别是多了一个 struct device *dev 参数。内核通过这个设备指针来管理中断的生命周期。
3.2 中断处理函数:上半部与下半部
中断处理函数分两部分,这是Linux中断子系统的核心设计思想。为什么要分?因为中断上下文里不能睡眠,不能做耗时操作。你想想看,如果中断处理函数里做个复杂的计算或者等待I/O,整个系统都会被拖死。
上半部(Top Half):在中断上下文中立即执行,要求快速完成。通常只做最紧急的事:
- 清除中断标志
- 保存硬件状态
- 调度下半部
下半部(Bottom Half):稍后执行,可以做耗时操作。Linux提供了三种下半部机制:
| 机制 | 特点 | 适用场景 |
|---|---|---|
| 软中断(Softirq) | 性能最高,但使用复杂 | 网络、块设备等高性能场景 |
| Tasklet | 基于软中断,使用简单 | 大多数驱动场景 |
| 工作队列(Workqueue) | 在进程上下文执行,可以睡眠 | 需要睡眠或大量计算的场景 |
核心原则:上半部越快越好,下半部能放就放。我见过一个驱动工程师把整个数据处理都放在上半部,结果系统响应延迟从微秒级变成了毫秒级。
来看一个典型的例子:
/* 上半部:中断处理函数 */
static irqreturn_t my_irq_handler(int irq, void *dev_id)
{
struct my_device *dev = (struct my_device *)dev_id;
/* 1. 清除中断标志 */
writel(0x01, dev->regs + INT_CLEAR);
/* 2. 保存关键状态 */
dev->status = readl(dev->regs + INT_STATUS);
/* 3. 调度下半部 */
tasklet_schedule(&dev->tasklet);
return IRQ_HANDLED;
}
/* 下半部:Tasklet处理函数 */
static void my_tasklet_handler(unsigned long data)
{
struct my_device *dev = (struct my_device *)data;
/* 这里可以做耗时操作 */
process_data(dev);
wake_up_interruptible(&dev->wait_queue);
}
3.3 中断上下文:能做什么,不能做什么
中断上下文,说白了就是CPU正在执行中断处理函数时的状态。这个状态下,内核有很多限制。我刚开始做驱动时,在中断处理函数里调用了 msleep,结果系统直接死锁。
中断上下文中的“禁区”:
- 不能睡眠:不能调用
msleep、wait_event、mutex_lock等可能导致睡眠的函数 - 不能访问用户空间:不能使用
copy_from_user、copy_to_user - 不能持有互斥锁:只能使用自旋锁(spinlock)
- 不能做复杂的计算:时间太长会影响系统响应
我曾经踩过的坑:在中断处理函数里调用了
printk 打印调试信息。你以为只是打印几个字符?实际上 printk 在控制台繁忙时可能会睡眠,直接导致内核崩溃。调试时可以用 printk,但正式代码里一定要去掉。
中断上下文中“可以”做的事:
- 读写硬件寄存器
- 操作自旋锁保护的共享数据
- 调度下半部(tasklet、工作队列)
- 唤醒等待队列(
wake_up)
3.4 知识结构总览
下面这张图把中断子系统的核心脉络画出来了,建议你保存下来对照学习:
3.5 实战建议
最后,分享几个我在项目中积累的经验:
- 注册中断前先确认中断号:用
cat /proc/interrupts查看系统中已注册的中断,避免冲突。 - 共享中断要小心:使用
IRQF_SHARED标志时,dev_id参数不能为 NULL,否则无法区分是哪个设备触发了中断。 - 上半部越短越好:我一般要求上半部不超过50条指令,超过的就扔到下半部去。
- 调试时用 tracepoint:别在中断处理函数里加
printk,用trace_irq_handler_entry和trace_irq_handler_exit更安全。
一个小技巧:如果你不确定某个函数能否在中断上下文中调用,可以查内核文档或者看函数注释。大部分内核API都会明确标注“This function may sleep”或“This function must be called with interrupts disabled”。