4. 中断子系统:Linux中断处理机制,顶半部与底半部(tasklet、workqueue、threaded IRQ)的设计与选择

中断处理,是嵌入式系统里最核心、也最容易出问题的地方之一。我见过不少工程师,写驱动时把整个中断处理搞得又长又重,结果系统响应一塌糊涂。说白了,中断处理就像接电话——你不可能在电话里把整件事都办完,先记下来,回头再处理。

Linux内核把中断处理分成了两段:顶半部(Top Half)底半部(Bottom Half)。这个设计,我当年刚接触时也觉得多此一举,直到被一个死锁问题折磨了三天……嗯,从那以后我就老老实实按规矩来了。

4.1 顶半部:快进快出

顶半部就是中断服务程序(ISR)。它的原则只有一个:越快越好

为什么?因为顶半部执行时,当前中断线是被屏蔽的。你拖得越久,其他中断就进不来。我有个项目,网卡驱动里在顶半部做了个复杂的链表遍历,结果一秒钟丢了几百个包。后来改成只记录标志位,问题立刻解决。

顶半部里能做什么?

  • 读取硬件状态寄存器
  • 清除中断标志位
  • 把数据拷贝到内存缓冲区
  • 触发底半部处理

绝对不能做什么?

  • 调用可能导致睡眠的函数(比如 kmalloc(GFP_KERNEL)
  • 获取信号量(mutex)
  • 进行大量数据拷贝或复杂计算
注意:顶半部里用 printk 也要小心。我见过有人调试时在ISR里加了一堆打印,结果系统直接卡死。因为 printk 可能触发控制台驱动的中断,形成递归。

4.2 底半部:三种武器

顶半部干完最紧急的活,剩下的就交给底半部。Linux提供了三种主流机制:taskletworkqueuethreaded IRQ。怎么选?我个人的经验是:看你的底半部要不要睡觉。

4.2.1 tasklet:轻量级,但不能睡

Tasklet 是基于软中断实现的。它运行在中断上下文中,所以不能睡眠,也不能调用任何可能阻塞的函数。

什么时候用 tasklet?

  • 处理时间很短(微秒级)
  • 不需要等待任何资源
  • 对实时性要求较高

代码示例:

// 定义 tasklet 处理函数
void my_tasklet_handler(unsigned long data)
{
    // 这里不能睡眠
    // 做一些轻量级的数据处理
    printk(KERN_INFO "tasklet: data = %lu\n", data);
}

// 声明 tasklet 结构体
DECLARE_TASKLET(my_tasklet, my_tasklet_handler, 0);

// 在顶半部中调度
irqreturn_t my_isr(int irq, void *dev_id)
{
    // 清除中断标志
    // ...
    
    // 调度 tasklet
    tasklet_schedule(&my_tasklet);
    
    return IRQ_HANDLED;
}
小技巧:Tasklet 默认是串行执行的——同一个 tasklet 不会在多个 CPU 上同时运行。但不同 tasklet 可以并行。所以如果你有多个不相关的底半部任务,可以拆成多个 tasklet 来提高效率。

4.2.2 workqueue:能睡觉,但慢一点

Workqueue 运行在进程上下文。这意味着它可以睡眠、可以获取信号量、可以调用 kmalloc(GFP_KERNEL)。说白了,它就是一个内核线程。

什么时候用 workqueue?

  • 需要访问 I2C/SPI 等慢速总线
  • 需要分配大量内存
  • 需要等待硬件操作完成

代码示例:

// 定义 workqueue 处理函数
void my_work_handler(struct work_struct *work)
{
    // 这里可以睡眠
    // 比如读取 I2C 设备
    msleep(10);
    printk(KERN_INFO "work: done\n");
}

// 声明 work 结构体
DECLARE_WORK(my_work, my_work_handler);

// 在顶半部中调度
irqreturn_t my_isr(int irq, void *dev_id)
{
    // 清除中断标志
    // ...
    
    // 调度 work
    schedule_work(&my_work);
    
    return IRQ_HANDLED;
}
核心区别:Tasklet 不能睡,workqueue 能睡。就这么简单。但 workqueue 的调度延迟比 tasklet 大,因为要经过内核线程调度。

4.2.3 threaded IRQ:把顶半部和底半部合二为一

Threaded IRQ 是内核 2.6.30 之后引入的。它把整个中断处理都放到一个内核线程里。顶半部只做最必要的事(比如确认中断来自本设备),然后唤醒线程,剩下的全在线程里做。

什么时候用 threaded IRQ?

  • 你的顶半部几乎没什么事可做
  • 底半部需要睡眠,但又不想用 workqueue 的额外开销
  • 希望代码更简洁,不用分开写顶半部和底半部

代码示例:

// threaded IRQ 处理函数
irqreturn_t my_threaded_handler(int irq, void *dev_id)
{
    // 这里运行在进程上下文,可以睡眠
    // 比如读取硬件寄存器
    u32 val = ioread32(my_base + REG_STATUS);
    printk(KERN_INFO "threaded: val = %u\n", val);
    
    return IRQ_HANDLED;
}

// 顶半部(快速检查)
irqreturn_t my_isr(int irq, void *dev_id)
{
    // 快速确认中断是否来自本设备
    if (!(ioread32(my_base + REG_INT) & MY_INT_BIT))
        return IRQ_NONE;
    
    // 清除中断标志
    iowrite32(MY_INT_BIT, my_base + REG_INT);
    
    // 返回 IRQ_WAKE_THREAD 唤醒线程
    return IRQ_WAKE_THREAD;
}

// 注册中断
request_threaded_irq(irq, my_isr, my_threaded_handler,
                     IRQF_TRIGGER_RISING, "my_device", dev_id);
我曾经踩过的坑:Threaded IRQ 的线程默认优先级不高。如果你的系统对实时性要求很严,记得用 sched_setscheduler 把线程优先级调高。否则中断响应可能被其他进程拖慢。

4.3 如何选择?一张表说清楚

特性 tasklet workqueue threaded IRQ
运行上下文 中断上下文 进程上下文 进程上下文
能否睡眠 不能
调度延迟 低(微秒级) 中(毫秒级) 中(毫秒级)
并行性 同类型串行 可并行 每个IRQ一个线程
代码复杂度 低(合二为一)
适用场景 轻量、快速处理 需要睡眠、慢速设备 顶半部极简、需要睡眠

4.4 我的选择原则

做了这么多年驱动,我总结了一套简单的选择方法:

  1. 先问自己:底半部需要睡眠吗?
    • 不需要 → 用 tasklet
    • 需要 → 看下一步
  2. 再问:顶半部除了确认中断还有别的事吗?
    • 几乎没有 → 用 threaded IRQ,代码最干净
    • 还有别的事(比如拷贝少量数据)→ 用 workqueue
  3. 最后问:对实时性要求有多高?
    • 极高(微秒级)→ 考虑 tasklet 甚至直接在顶半部处理
    • 一般(毫秒级)→ workqueue 或 threaded IRQ 都行
我个人习惯:新项目我优先用 threaded IRQ。因为它把顶半部和底半部写在一起,逻辑清晰,不容易出错。只有当我确认底半部不需要睡眠,而且对性能有极致要求时,才会换成 tasklet。

4.5 避坑指南

最后分享几个我踩过的坑:

  • 不要在 tasklet 里用 msleep 这会导致内核崩溃。我见过有人从 workqueue 代码直接复制到 tasklet 里,忘了删 sleep,结果系统直接 panic。
  • 注意 workqueue 的并发: 默认的 system_wq 是共享的。如果你的底半部任务很重,建议自己创建专用的 workqueue,避免影响其他驱动。
  • Threaded IRQ 的 IRQF_ONESHOT 标志: 如果你的中断是电平触发,记得加上这个标志。否则中断线会一直被触发,导致线程反复唤醒。我当年调一个 GPIO 中断,没加这个标志,结果 CPU 占用率直接飙到 100%。
  • 测试底半部的执行时间:ktime_get 在顶半部和底半部各打一个时间戳,看看延迟是否在可接受范围内。我有个项目,workqueue 的调度延迟到了 10 毫秒,后来发现是系统负载太高,换了专用高优先级 workqueue 才解决。

中断处理的设计,说白了就是权衡——在响应速度和功能完整性之间找平衡点。你想想看,一个中断来了,你是想让它快点结束,还是想让它把事情办完?这个问题的答案,决定了你选哪种底半部机制。