4. 中断下半部机制概述

在嵌入式 Linux 驱动开发中,中断处理程序(Interrupt Service Routine, ISR)必须快速执行,以释放 CPU 资源响应其他中断。然而,许多设备在中断到达后需要完成大量非紧急处理(如数据拷贝、协议解析、唤醒等待队列)。如果将这些工作全部放在 ISR 中,将导致系统中断延迟急剧增大,甚至丢失后续中断。

因此,Linux 内核将中断处理拆分为两个部分:上半部(Top Half) 和 下半部(Bottom Half)。上半部执行必须立即完成的、时间敏感的操作(如清除中断标志、保存硬件寄存器);下半部处理可以推迟的、相对耗时的任务。

核心原则: 上半部关中断执行,要求极短(微秒级);下半部开中断执行,允许被其他中断抢占,但同一 CPU 上同类型下半部不会并发执行。
中断处理上下半部拆分流程 硬件中断触发 上半部 ISR 关中断 · 快速 · 必须立即响应 下半部处理 开中断 · 可推迟 · 非紧急 Tasklet Workqueue Softirq Threaded IRQ 也属于下半部

4.1 为什么需要下半部

中断上下文具有以下严格限制,迫使驱动开发者将非紧急工作移出 ISR:

  • 中断上下文不可睡眠: ISR 运行在中断上下文中,不能调用可能导致睡眠的函数(如 copy_to_user()、kmalloc(GFP_KERNEL)、mutex_lock())。
  • 中断关闭时间必须极短: 长时间关中断会导致其他中断丢失,系统响应恶化。
  • 中断嵌套风险: 高优先级中断可能打断低优先级 ISR,复杂处理容易导致栈溢出或死锁。
  • 实时性要求: 实时系统要求中断延迟有确定上限,下半部机制允许调度器介入。
经验法则: 如果 ISR 中需要做超过 100 条指令的工作,或者涉及任何可能阻塞的操作,就应该考虑使用下半部。

4.2 下半部机制对比

Linux 内核提供了四种主流下半部实现机制:Softirq、Tasklet、Workqueue 和 Threaded IRQ。它们在运行上下文、可睡眠性、并发模型和适用场景上存在显著差异。

特性 Softirq Tasklet Workqueue Threaded IRQ
运行上下文 中断上下文 中断上下文 进程上下文 内核线程上下文
是否可睡眠 否 否 是 是
并发模型 同一 Softirq 可在多 CPU 同时运行 同一 Tasklet 串行执行,不同 CPU 可运行不同 Tasklet 由工作队列线程调度,多 CPU 并行 每个 IRQ 对应一个内核线程,可设置优先级
实时性 高(软中断优先级高于进程) 中(依赖软中断调度点) 低(受进程调度影响) 中(可设置实时优先级)
使用复杂度 高(需静态分配,需处理重入) 低(动态注册,自动串行化) 中(需创建工作项和队列) 低(request_threaded_irq 接口)
典型场景 网络协议栈、块设备层 简单驱动下半部、少量数据拷贝 大量数据拷贝、文件系统操作、设备 I/O 需要睡眠的复杂驱动、实时驱动
四种下半部机制上下文层级与调度关系 进程上下文(可睡眠) Workqueue Threaded IRQ 内核线程 中断上下文(不可睡眠) Softirq Tasklet 硬件中断层 上半部 ISR(关中断执行) Softirq 和 Tasklet 运行在中断上下文,不能睡眠;Workqueue 和 Threaded IRQ 运行在进程上下文,可以睡眠

4.3 各机制详解

4.3.1 Softirq

Softirq 是内核中优先级最高的下半部机制,编译时静态分配。同一类型的 Softirq 可以在多个 CPU 上同时执行,因此必须处理重入问题。网络子系统(NET_TX_SOFTIRQ、NET_RX_SOFTIRQ)和块设备层是典型使用者。驱动开发者一般不直接使用 Softirq,而是通过 Tasklet 间接使用。

4.3.2 Tasklet

Tasklet 基于 Softirq 实现(使用 TASKLET_SOFTIRQ 和 HI_SOFTIRQ),但提供了更简单的接口:同一 Tasklet 在任意时刻只能在一个 CPU 上执行,不同 Tasklet 可以并行。Tasklet 是驱动开发中最常用的下半部机制,适合执行时间较短、不需要睡眠的任务。

/* Tasklet 典型使用模式 */
void my_tasklet_handler(unsigned long data)
{
    /* 下半部处理:数据拷贝、状态更新、唤醒等待队列 */
    struct my_device *dev = (struct my_device *)data;
    dev->status = DATA_READY;
    wake_up_interruptible(&dev->wq);
}

DECLARE_TASKLET(my_tasklet, my_tasklet_handler, (unsigned long)&my_device);

/* 在 ISR 中调度 Tasklet */
irqreturn_t my_isr(int irq, void *dev_id)
{
    /* 上半部:清除中断标志,保存必要寄存器 */
    writel(0, REG_INT_CLEAR);
    /* 调度下半部 */
    tasklet_schedule(&my_tasklet);
    return IRQ_HANDLED;
}

4.3.3 Workqueue

Workqueue 将工作推迟到内核线程中执行,运行在进程上下文,因此可以睡眠、可以持有互斥锁、可以执行文件系统操作。适用于需要大量数据拷贝、复杂协议处理或设备 I/O 的场景。Linux 提供全局工作队列 system_wq,也支持创建专用工作队列。

/* Workqueue 典型使用模式 */
struct work_struct my_work;

void my_work_handler(struct work_struct *work)
{
    /* 可以睡眠,可以调用 copy_to_user */
    struct my_device *dev = container_of(work, struct my_device, work);
    copy_to_user(dev->buf, dev->data, dev->len);
}

/* 在 ISR 中调度 Work */
irqreturn_t my_isr(int irq, void *dev_id)
{
    /* 上半部快速处理 */
    schedule_work(&my_work);  /* 使用系统工作队列 */
    return IRQ_HANDLED;
}

4.3.4 Threaded IRQ

Threaded IRQ 是内核 2.6.30 引入的机制,通过 request_threaded_irq() 注册。它将 ISR 分为两部分:一个不可睡眠的快速处理函数(handler),和一个可睡眠的线程化处理函数(thread_fn)。线程化处理函数运行在内核线程中,可以设置实时优先级,适合对实时性有要求的驱动。

/* Threaded IRQ 典型使用模式 */
irqreturn_t my_handler(int irq, void *dev_id)
{
    /* 快速上半部:清除中断标志 */
    writel(0, REG_INT_CLEAR);
    return IRQ_WAKE_THREAD;  /* 唤醒线程化处理 */
}

irqreturn_t my_thread_fn(int irq, void *dev_id)
{
    /* 线程化下半部:可以睡眠,可以持有 mutex */
    struct my_device *dev = (struct my_device *)dev_id;
    mutex_lock(&dev->lock);
    /* 复杂处理 */
    mutex_unlock(&dev->lock);
    return IRQ_HANDLED;
}

/* 注册 Threaded IRQ */
ret = request_threaded_irq(irq, my_handler, my_thread_fn,
                           IRQF_TRIGGER_RISING, "my_device", dev);
选择建议:
  • 简单、快速、不需要睡眠 → Tasklet(驱动首选)
  • 需要睡眠、大量数据拷贝、文件系统操作 → Workqueue
  • 高性能、需要多 CPU 并行、内核子系统 → Softirq
  • 需要实时优先级、需要睡眠、希望简化中断注册 → Threaded IRQ

4.4 下半部机制选择决策树

下半部机制选择决策树 下半部需要睡眠? 是 → 进程上下文 需要实时优先级? 是 → Threaded IRQ 否 → Workqueue 否 → 中断上下文 需要多 CPU 并行? 是 → Softirq 否 → Tasklet 决策要点 Tasklet 是驱动开发默认选择;需要睡眠时优先考虑 Workqueue;实时场景使用 Threaded IRQ;内核子系统使用 Softirq

4.5 下半部调度时机

下半部并非在 ISR 返回后立即执行,而是在内核特定的调度点被处理:

  • 中断返回时: 内核在 irq_exit() 中检查是否有待处理的软中断,如果有则调用 do_softirq()。
  • ksoftirqd 内核线程: 当软中断数量过多或执行时间过长时,内核将软中断处理委托给 ksoftirqd 线程,避免用户空间进程饥饿。
  • 显式调用: 某些内核代码路径会主动调用 local_bh_enable() 或 do_softirq() 来触发下半部处理。
关键理解: 下半部机制的核心价值在于将中断处理从“必须关中断的紧急上下文”转移到“可以开中断的宽松上下文”,从而在保证系统实时性的同时,允许驱动完成复杂处理。Tasklet 作为 Softirq 的简化封装,是驱动开发者最常打交道的下半部接口。