顶半部与底半部:那次中断风暴让我彻底搞懂了

张开发
2026/4/11 18:58:13 15 分钟阅读

分享文章

顶半部与底半部:那次中断风暴让我彻底搞懂了
那天晚上产线测试机突然卡死屏幕上的数据刷新停滞在23:47:15。重启后查看内核日志满屏的“IRQ handler took too long”警告。问题定位到我们新加的传感器驱动——中断频率从设计的100Hz变成了实际跑起来的2kHzISR里那个浮点运算和I2C读写直接把系统拖垮了。中断处理的现实困境中断服务程序要求快进快出这是教科书上的铁律。但现实场景总是打脸数据要处理、协议要解析、有时还得做点计算。那个出问题的驱动最初长这样staticirqreturn_tsensor_isr(intirq,void*dev_id){structsensor_data*datadev_id;// 读取原始数据i2c_read_bytes(data-client,REG_DATA,raw_buf,12);// 浮点校准运算问题就在这里floatcalibrated(raw_buf[0]*1.23fraw_buf[1]*0.45f)/1.68f;// 写入结果缓冲区data-buffer[data-index]calibrated;// 通知应用层wake_up_interruptible(data-wait_queue);returnIRQ_HANDLED;}看起来挺合理对吧但在2kHz中断频率下每次ISR执行时间超过200微秒CPU很快被中断占满其他任务根本抢不到时间片。顶半部的生存法则顶半部Top Half就是我们的ISR它的设计原则就三条第一只做最紧急的硬件操作。读状态寄存器、清中断标志、把数据从硬件FIFO搬到内存缓冲区——这些必须在顶半部完成否则可能丢数据。第二绝对不要睡眠。在ISR里调用kmalloc带GFP_KERNEL标志想都别想。等锁更不行。这里一旦睡眠整个系统基本就挂了。第三耗时操作统统丢出去。我们的修复方案把ISR改成了这样staticirqreturn_tsensor_isr(intirq,void*dev_id){structsensor_data*datadev_id;// 只做最必要的读取硬件数据i2c_read_bytes(data-client,REG_DATA,data-raw_buffer,12);data-raw_ready1;// 标记需要处理启动底半部tasklet_schedule(data-process_tasklet);returnIRQ_HANDLED;}ISR执行时间从200微秒降到了15微秒中断风暴瞬间平息。底半部的三种武器Linux给了我们三种主要的底半部机制选哪个得看场景tasklet——适合中等延迟要求的场景。我们的传感器驱动最终选了它voidprocess_sensor_data(unsignedlongarg){structsensor_data*data(structsensor_data*)arg;if(!data-raw_ready)return;// 放心做浮点运算floatcalibrated(data-raw_buffer[0]*1.23fdata-raw_buffer[1]*0.45f)/1.68f;// 甚至可以睡眠如果需要mutex_lock(data-buffer_lock);data-buffer[data-index]calibrated;mutex_unlock(data-buffer_lock);data-raw_ready0;}// 初始化时声明tasklet_init(data-process_tasklet,process_sensor_data,(unsignedlong)data);tasklet在软中断上下文执行还是不能睡眠但至少不占用硬中断时间。工作队列workqueue——需要睡眠时的选择。上周调试的另一个驱动需要在中断后等待GPIO稳定就用到了工作队列structwork_structprocess_work;voiddeferred_processing(structwork_struct*work){// 这里可以调用msleep、mutex_lock等可能睡眠的函数msleep(2);// 等待硬件稳定process_data();}// ISR中提交工作schedule_work(process_work);软中断softirq——内核级的高性能选择。网络栈、块设备层这些对性能敏感的核心子系统在用。普通驱动不建议自己注册软中断内核提供的tasklet其实就是在软中断基础上封装的。那些年踩过的坑内存分配陷阱在tasklet里用GFP_KERNEL分配内存理论上tasklet还是软中断上下文不能睡眠。但实际测试发现在某些内核版本下小内存分配可能不会触发睡眠但这种依赖版本的行为很危险。稳妥做法是预分配或者用GFP_ATOMIC。共享数据保护ISR和底半部共享数据时记得用原子操作或者关本地中断// 在ISR中data-raw_indexnew_index;smp_wmb();// 写内存屏障确保数据先于标志位更新// 在tasklet中读取if(data-raw_ready){smp_rmb();// 读内存屏障process_data(data-raw_buffer);}嵌套与重入tasklet在同一个CPU上不会重入但不同CPU可能并行执行同一个tasklet。如果数据是per-CPU的没问题全局数据就得加锁。实战建议性能敏感场景先测量用ktime_get_ns()在ISR入口和出口打时间戳超过10微秒的操作就要考虑挪到底半部。那次调试我们就是靠这个发现浮点运算太耗时。选型简单原则不需要睡眠→用tasklet需要睡眠或延迟执行→用工作队列超高频率中断比如网络收包→考虑NAPI或者自己注册软中断。调试技巧/proc/interrupts看中断计数/proc/softirqs看软中断分布。如果某个软中断计数异常增长很可能对应的tasklet处理太慢。新的选择内核现在有threaded IRQ算是官方推荐的另一种方案特别适合那些既想简单又需要睡眠的中断处理。用request_threaded_irq就行内核会自动帮你管理线程。那次中断风暴后我们定了个规矩所有新驱动的中断处理代码必须经过“顶半部时间审计”。ISR超过20微秒的review不通过。这个规矩让我们避开了很多性能坑。记住中断处理不是越快越好而是“该快的部分要快该慢的部分要挪出去”。平衡好顶半部和底半部驱动才能既稳定又高效。

更多文章