第4章:中断处理函数编写:顶半部与底半部、中断上下文限制、编写规范
中断处理函数怎么写,是很多Linux驱动开发者入门时最头疼的问题。我刚开始接触这块时,也踩过不少坑——比如在中断里调了kmalloc(GFP_KERNEL),结果系统直接死给你看。嗯,今天我们就来彻底搞明白这件事。
4.1 顶半部与底半部:为什么需要拆成两半?
先问一个问题:中断来了,CPU正在处理A任务,突然被B中断打断。如果B的处理时间很长,A就得一直等着。更糟的是,如果中断里又来了中断,系统可能直接崩掉。
所以Linux把中断处理拆成了两半:
- 顶半部(Top Half):硬中断上下文,必须快,越快越好。只做最紧急的事,比如清中断标志、拷贝硬件寄存器。
- 底半部(Bottom Half):软中断/任务/workqueue,可以慢一点。做剩下的耗时操作,比如数据解析、唤醒等待队列。
核心原则:顶半部只做“必须关中断才能做的事”,其余全部扔给底半部。
我在项目中遇到过一位同事,把整个网络协议栈解析都放在顶半部里。结果呢?网卡一跑满,系统响应延迟直接飙到秒级。后来改成顶半部只收包、底半部做协议解析,问题就解决了。
4.2 中断上下文限制:哪些事不能做?
顶半部运行在中断上下文里。说白了,这个上下文里没有进程的概念,你不能睡眠、不能调度、不能拿锁等资源。我列个表,你一看就明白:
| 操作 | 顶半部 | 底半部(tasklet/workqueue) | 说明 |
|---|---|---|---|
kmalloc(GFP_KERNEL) |
❌ 禁止 | ✅ 允许 | GFP_KERNEL可能睡眠,顶半部只能用GFP_ATOMIC |
copy_from_user() |
❌ 禁止 | ✅ 允许 | 涉及缺页异常,可能睡眠 |
mutex_lock() |
❌ 禁止 | ✅ 允许 | mutex可能睡眠,顶半部只能用spin_lock() |
printk() |
⚠️ 谨慎 | ✅ 允许 | printk在顶半部可用,但太多会拖慢系统 |
| 访问用户空间内存 | ❌ 禁止 | ✅ 允许 | 用户页可能被换出,访问会触发缺页 |
| 触发调度(schedule) | ❌ 禁止 | ✅ 允许 | 中断上下文没有进程概念 |
我曾经犯过的错:在顶半部里调了wake_up_interruptible(),以为只是唤醒进程没问题。结果唤醒路径里调了try_to_wake_up(),里面拿了rq锁,和别的CPU形成死锁。从那以后,我所有唤醒操作都放到底半部去做。
4.3 底半部机制对比:选哪个?
Linux提供了三种底半部机制。我按自己的使用经验排了个序:
| 机制 | 特点 | 适合场景 | 我的建议 |
|---|---|---|---|
| 软中断(SoftIRQ) | 最快,但不可睡眠,不能重调度 | 网络收包、块设备IO | 内核子系统内部用,驱动一般不直接碰 |
| tasklet | 基于软中断,串行执行,不可睡眠 | 简单数据处理、状态更新 | 老驱动用得多,新代码推荐用workqueue |
| workqueue | 基于内核线程,可睡眠,可重调度 | 复杂操作、IO操作、锁竞争 | 我90%的场景都用它 |
你想想看,如果你的底半部需要访问I2C设备、需要等DMA完成、需要调msleep(),那workqueue是唯一选择。tasklet里不能睡眠,软中断更不行。
4.4 编写规范:一个靠谱的中断处理函数长什么样?
我总结了一套模板,照着写基本不会出问题:
// 顶半部:只做最紧急的事
static irqreturn_t my_irq_handler(int irq, void *dev_id)
{
struct my_device *dev = (struct my_device *)dev_id;
// 1. 读取硬件状态寄存器,确认中断源
u32 status = ioread32(dev->regs + INT_STATUS);
// 2. 清除中断标志(硬件要求)
iowrite32(status, dev->regs + INT_CLEAR);
// 3. 把数据从硬件FIFO搬到内存缓冲区
// 注意:只搬少量关键数据
dev->rx_data = ioread32(dev->regs + RX_FIFO);
// 4. 调度底半部
schedule_work(&dev->work);
return IRQ_HANDLED;
}
// 底半部:做剩下的所有事
static void my_work_handler(struct work_struct *work)
{
struct my_device *dev = container_of(work, struct my_device, work);
// 1. 解析数据(可以慢)
// 2. 唤醒等待队列
// 3. 通知用户空间
// 4. 如果有需要,重新使能中断
}
个人习惯:我每次写中断处理函数,都会在顶半部末尾加一行注释——/* 所有耗时操作已移交底半部 */。这样代码审查时,别人一眼就能看出你的设计意图。
4.5 避坑指南:我踩过的五个坑
- 中断里调了
kmalloc(GFP_KERNEL):系统直接panic。记住,顶半部只能用GFP_ATOMIC。 - 忘了清除中断标志:中断一直触发,CPU被锁死在中断里。我调试过一个网卡驱动,就是这个问题,查了两天才发现。
- 底半部里拿了自旋锁又睡眠:自旋锁持有期间不能睡眠,否则其他CPU等锁等到天荒地老。
- 中断共享时没检查设备状态:多个设备共享一个IRQ线,你的处理函数被调用了,但中断不是你的设备产生的。一定要先读状态寄存器确认。
- workqueue没初始化就调度:
schedule_work()前必须调INIT_WORK()。我见过有人把初始化放在probe之后,结果中断先来了,直接oops。
4.6 知识结构图:中断处理全流程
下面这张图把顶半部、底半部、中断上下文限制串在一起,方便你理解整体流程:
4.7 总结:记住这三条
- 顶半部要快:只做硬件交互和底半部调度,其他一律不放。
- 中断上下文不能睡眠:锁用spin_lock,内存用GFP_ATOMIC,用户空间别碰。
- 底半部选workqueue:除非你有特殊理由,否则workqueue是最安全、最灵活的选择。
嗯,中断处理函数写好了,你的驱动就成功了一半。剩下的就是调试和验证了——这部分我们后面章节会详细讲。