4. 中断处理函数编写:中断上下文限制、快速响应原则、共享中断处理
好,咱们今天聊聊中断处理函数的编写。这玩意儿,说简单也简单,说难也难。我见过不少新手,甚至有些老手,都在这里栽过跟头。说白了,中断处理函数就是在“刀尖上跳舞”,你得时刻清楚自己身在何处,能做什么,不能做什么。
4.1 中断上下文:一个“不自由”的世界
先说说“中断上下文”这个概念。你想想看,当CPU正在高高兴兴地跑你的应用程序,突然一个中断来了,CPU就得立刻停下手中的活,跳去执行你的中断处理函数。这时候,CPU处于一种特殊的状态——中断上下文。
在这个状态下,有哪些限制呢?我列一下:
- 不能睡眠:这是最核心的一条。在中断上下文里,你不能调用任何可能导致睡眠的函数。比如
kmalloc(GFP_KERNEL)、mutex_lock()、copy_from_user()这些,统统不能用。为什么?因为中断上下文没有进程调度,你一睡,就没人能把你唤醒了。 - 不能访问用户空间:中断处理函数运行在内核空间,没有进程的页表上下文。你没法直接访问用户空间的地址。我曾经有个同事,在中断里直接解引用用户空间指针,结果系统直接oops了。
- 不能持有信号量:信号量可能导致睡眠,所以也不行。但自旋锁可以用,不过要小心,持有自旋锁的时间一定要短。
- 不能做复杂运算:中断处理函数应该像“闪电战”一样,快进快出。浮点运算、大循环、复杂的算法,都不适合在这里做。
核心原则:中断上下文里,你唯一能信任的就是当前CPU的寄存器。其他一切,都要小心再小心。
4.2 快速响应原则:别让CPU等你
中断处理函数的核心要求就一个字:快。为什么?因为中断是异步事件,CPU响应中断时,会屏蔽同级和更低级的中断。如果你在中断处理函数里磨磨蹭蹭,其他中断就进不来,系统的实时性就完蛋了。
我个人习惯把中断处理函数分成两部分:
- 上半部(Top Half):就是注册的那个中断处理函数。只做最紧急的事,比如:
- 清除硬件中断标志
- 从硬件寄存器读取数据
- 唤醒一个内核线程(下半部)
- 下半部(Bottom Half):把耗时的、不紧急的工作放到这里做。比如:
- 处理读取到的数据
- 唤醒等待队列中的进程
- 执行协议栈处理
嗯,这里要注意:上半部里绝对不要做任何可能阻塞的操作。我见过有人在上半部里打印调试信息,结果串口输出太慢,直接把系统卡死了。
我的经验:一个合格的中断处理函数,执行时间应该控制在微秒级别。如果你发现中断处理函数执行时间超过了几十微秒,就该考虑用下半部了。
4.3 共享中断处理:大家一起用
在嵌入式系统里,硬件资源有限,多个设备共享一个中断线的情况很常见。比如GPIO中断,多个引脚可能共用一个中断号。这时候,你的中断处理函数就得学会“认人”。
注册共享中断时,要加上 IRQF_SHARED 标志。内核在调用你的处理函数前,会先检查中断状态寄存器,确认是不是你的设备触发了中断。如果不是,赶紧返回 IRQ_NONE,让内核去调用下一个处理函数。
看个例子:
static irqreturn_t my_shared_handler(int irq, void *dev_id)
{
struct my_device *dev = (struct my_device *)dev_id;
u32 status;
// 读取硬件状态寄存器,确认是不是我的设备
status = readl(dev->base + STATUS_REG);
if (!(status & MY_DEVICE_IRQ_PENDING)) {
// 不是我的中断,赶紧走人
return IRQ_NONE;
}
// 处理中断
// ... 快速处理 ...
// 清除中断标志
writel(MY_DEVICE_IRQ_CLEAR, dev->base + CLEAR_REG);
return IRQ_HANDLED;
}
// 注册时
ret = request_irq(irq_num, my_shared_handler, IRQF_SHARED,
"my_device", my_device);
这里有个坑,我曾经踩过:共享中断里,dev_id 参数不能为NULL。因为内核要靠这个参数来区分不同的设备。如果你传了NULL,卸载驱动时 free_irq() 就找不到对应的处理函数了。
警告:共享中断的处理函数,一定要先检查中断源。如果直接返回 IRQ_HANDLED,会导致其他设备的中断丢失。我曾经因为这个bug,排查了整整两天。
4.4 实战中的避坑指南
最后,分享几个我这些年积累的经验:
- 中断嵌套要小心:虽然Linux内核支持中断嵌套,但我不建议你用。嵌套中断容易导致栈溢出,而且调试起来非常痛苦。
- 用
local_irq_save()保护临界区:如果你在进程上下文和中断上下文共享数据,记得在进程上下文里关中断再访问。否则,数据竞争会让你欲哭无泪。 - 中断处理函数里别用
printk():除非你是在调试初期,而且知道自己在做什么。printk()太慢了,会严重影响中断响应时间。 - 善用
dev_id:把设备结构体指针传给中断处理函数,这样你就能在中断里访问设备寄存器、私有数据了。比用全局变量优雅得多。
我曾经在一个项目中,因为中断处理函数里调用了 msleep(),导致系统在特定条件下死锁。嗯,从那以后,我每次写中断处理函数,都会在心里默念三遍:“不能睡眠,不能睡眠,不能睡眠。”
好了,中断处理函数的编写,核心就是这些。记住:快、准、稳。快是响应要快,准是中断源要认准,稳是代码要稳定可靠。做到这三点,你的中断处理函数就合格了。
总结一下:中断上下文里,别睡觉,别磨蹭,别认错人。做到这三点,你就是中断处理的高手了。