中断与底半部机制优化:中断上下文与进程上下文、中断注册与性能开销、tasklet/workqueue/threaded IRQ对比与选择、中断负载均衡(IRQ affinity)
中断,是嵌入式系统响应外部事件的「神经末梢」。但很多驱动工程师写中断处理函数时,习惯把大量工作直接塞进 handler 里。我早年也这么干过,结果系统响应慢得像蜗牛,连按键都丢中断。后来才明白——中断上下文里,你碰都不能碰那些会睡眠的东西。
中断上下文 vs 进程上下文
这两个概念,说白了就是「谁在干活」的问题。
- 中断上下文:硬件中断触发后,CPU 自动跳转到中断向量表,关中断或开中断,执行 handler。此时没有进程的概念,不能睡眠,不能调度,不能拿信号量。
- 进程上下文:普通进程跑在用户态或内核态,可以睡眠、可以调度、可以等锁。
我见过有人在中断 handler 里调用 msleep(),结果系统直接死锁。为什么?因为中断上下文里调度器根本不会去切换你,你睡下去就没人叫醒你。
中断注册与性能开销
注册中断用 request_irq() 或 devm_request_irq()。参数里有个 flags,很多人不注意,其实这里藏着性能陷阱。
// 典型的中断注册
ret = request_irq(irq_num, my_handler,
IRQF_TRIGGER_RISING | IRQF_SHARED,
"my_device", dev);
if (ret) {
pr_err("Failed to request IRQ %d\n", irq_num);
return ret;
}
性能开销主要来自三方面:
- 中断触发频率:每秒几万次的中断,handler 哪怕只多花 1us,CPU 就被吃掉 10%。
- 中断共享:多个设备共享一根 IRQ 线,每次中断都要遍历所有 handler,直到找到匹配的那个。我在一个项目里遇到过 4 个设备共享一个 IRQ,每次中断都要轮询一遍,性能直接腰斩。
- 中断嵌套:如果 handler 里开了中断,新的中断可能打断当前处理,导致栈溢出或优先级反转。
IRQF_NO_SUSPEND 和 IRQF_ONESHOT 组合,尤其是 threaded IRQ 场景。前者保证系统休眠时还能响应,后者防止中断重入。
底半部机制:tasklet / workqueue / threaded IRQ
Linux 提供了三种主流底半部机制。我按自己的经验画了张对比图,帮你快速选型。
tasklet
tasklet 跑在软中断上下文,说白了就是「中断的下半截」。它不能睡眠,不能拿信号量,但执行很快。我一般用它处理那些确定能在几百微秒内完成的事,比如清 FIFO、更新统计计数。
// tasklet 示例
void my_tasklet_handler(unsigned long data)
{
struct my_dev *dev = (struct my_dev *)data;
// 快速处理,不能睡眠
writel(dev->reg_base + STATUS, 0);
}
DECLARE_TASKLET(my_tasklet, my_tasklet_handler, (unsigned long)dev);
// 中断 handler 里调度
irqreturn_t my_handler(int irq, void *dev_id)
{
tasklet_schedule(&my_tasklet);
return IRQ_HANDLED;
}
workqueue
workqueue 跑在进程上下文,可以睡眠、可以等锁。适合那些需要访问 I2C、SPI 或操作文件系统的场景。但要注意——workqueue 的调度延迟不确定,如果中断频率很高,workqueue 可能来不及处理。
// workqueue 示例
struct work_struct my_work;
void my_work_handler(struct work_struct *work)
{
// 可以睡眠,可以调用 i2c_transfer()
msleep(10);
}
// 中断 handler 里调度
irqreturn_t my_handler(int irq, void *dev_id)
{
schedule_work(&my_work);
return IRQ_HANDLED;
}
msleep(),但中断频率是 10kHz,workqueue 根本来不及消化,系统内存被 pending 的 work 撑爆。后来改用 threaded IRQ 并限制线程优先级,才稳住。
threaded IRQ
threaded IRQ 是内核 2.6.30 之后引入的机制。它把中断 handler 分成两部分:一个极简的「快速 handler」(可选),和一个内核线程。线程可以设置优先级,适合实时性要求高的场景。
// threaded IRQ 注册
ret = request_threaded_irq(irq_num,
my_fast_handler, // 快速 handler,可为 NULL
my_thread_fn, // 线程函数
IRQF_TRIGGER_RISING | IRQF_ONESHOT,
"my_device", dev);
static irqreturn_t my_thread_fn(int irq, void *dev_id)
{
// 这里可以睡眠,可以拿 mutex
struct my_dev *dev = dev_id;
mutex_lock(&dev->lock);
// 处理数据
mutex_unlock(&dev->lock);
return IRQ_HANDLED;
}
三种机制对比
| 特性 | tasklet | workqueue | threaded IRQ |
|---|---|---|---|
| 上下文 | 软中断 | 进程 | 内核线程 |
| 可睡眠 | 否 | 是 | 是 |
| 调度延迟 | 低(us 级) | 中(ms 级) | 可控(可设优先级) |
| 适用场景 | 高频、轻量 | 低频、重量 | 实时性要求高 |
| 并发限制 | 同类型串行 | 可并行 | 可并行 |
中断负载均衡(IRQ affinity)
多核处理器上,中断默认都落在 CPU0 上。如果外设中断频率很高,CPU0 会被打满,其他核却在看热闹。IRQ affinity 就是用来把中断分散到不同核上的。
设置方法很简单:
# 查看当前 IRQ 的 CPU 亲和性
cat /proc/irq/123/smp_affinity
# 设置 IRQ 123 只跑在 CPU2 上(位掩码 0x04)
echo 04 > /proc/irq/123/smp_affinity
# 或者用 irqbalance 自动均衡
systemctl start irqbalance
但要注意,不是所有中断都适合分散。比如网卡中断,如果分散到多个核,反而可能因为 cache 失效导致性能下降。我一般这样分配:
- 高频、数据量大(如网卡、NVMe):绑定到专用核,避免核间切换。
- 低频、控制类(如 GPIO、I2C):可以共享一个核,或者交给 irqbalance 自动管理。
- 实时性要求高(如音频、电机控制):绑定到隔离核(isolcpus),避免被其他进程干扰。
嗯,中断优化这件事,说白了就是「把对的事放在对的地方」。中断上下文只做最紧急的事,底半部根据场景选 tasklet、workqueue 还是 threaded IRQ,最后用 IRQ affinity 把负载摊平。这三板斧用好了,系统性能基本不会差到哪去。