2. Linux 中断处理机制:上半部(Top Half)与下半部(Bottom Half)机制详解

在 Linux 内核中,中断处理被明确划分为两个阶段:上半部(Top Half) 和 下半部(Bottom Half)。这种设计是为了在中断响应速度和系统整体性能之间取得平衡。上半部在关闭中断或屏蔽特定中断线的上下文中执行,要求极短的处理时间;而下半部则在中断重新使能后,以更宽松的调度条件完成剩余工作。

核心原则: 上半部只做最必要、最紧急的硬件操作(如清除中断标志、拷贝硬件寄存器数据),将耗时、可延迟的处理任务(如协议解析、数据分发)推迟到下半部执行。

2.1 上半部(Top Half)—— 硬中断上下文

上半部是中断请求(IRQ)触发后立即执行的代码路径。它运行在硬中断上下文中,具有以下特征:

  • 不可睡眠: 不能调用可能导致进程睡眠的函数(如 kmalloc(GFP_KERNEL)、mutex_lock())。
  • 不可重入: 对于同一中断线,上半部执行期间该中断线被屏蔽,避免嵌套。
  • 执行时间极短: 通常只进行几十到几百微秒的操作,否则会严重影响系统实时性。
  • 优先级最高: 任何进程上下文都会被抢占。
典型上半部工作: 读取硬件状态寄存器、保存关键数据到内存缓冲区、清除中断挂起位、调度下半部。

2.2 下半部(Bottom Half)—— 可延迟处理机制

下半部是上半部调度执行的、可延迟的处理任务。Linux 内核提供了三种主要的软中断机制来实现下半部:软中断(Softirq)、tasklet 和 工作队列(Workqueue)。它们在不同的上下文和调度约束下运行。

特性 软中断 (Softirq) Tasklet 工作队列 (Workqueue)
执行上下文 软中断上下文(中断上下文) 软中断上下文(中断上下文) 进程上下文(内核线程)
可睡眠 否 否 是
可重入 是(同一类型可在不同 CPU 上同时运行) 否(同一 tasklet 串行执行) 是(由内核线程池管理)
调度方式 编译时静态分配,通过 raise_softirq() 激活 动态注册,通过 tasklet_schedule() 调度 动态创建,通过 queue_work() 提交
适用场景 高性能、频繁触发、对延迟敏感(如网络 RX/TX) 简单、串行化的下半部任务(如块设备层) 需要睡眠、持有锁、执行文件系统操作等复杂任务
性能陷阱: 软中断和 tasklet 运行在中断上下文中,不能睡眠。如果处理时间过长,会导致用户空间进程饥饿,并增加中断延迟。工作队列虽然灵活,但上下文切换开销较大,不适合极高频的轻量级任务。

2.3 软中断(Softirq)机制详解

软中断是 Linux 内核中优先级最高、开销最小的下半部机制。它在内核编译时静态分配,数量有限(目前约 10 种,如 NET_TX_SOFTIRQ、TASKLET_SOFTIRQ)。

  • 注册: 通过 open_softirq() 将处理函数绑定到软中断号。
  • 触发: 上半部调用 raise_softirq() 或 raise_softirq_irqoff() 标记软中断为待处理。
  • 执行: 在 do_softirq() 中遍历待处理软中断向量表,调用对应的处理函数。软中断会在以下时机被检查并执行:
    • 硬中断处理程序退出时(irq_exit())。
    • 由 ksoftirqd 内核线程在进程上下文中处理(当软中断堆积过多时)。
关键优化点: 软中断可以在多个 CPU 上并行执行同一类型的处理函数,因此必须使用 per-CPU 变量或 RCU 等机制保护数据,避免锁竞争。
软中断执行流程与调度时机 硬件中断触发 上半部 (Top Half) raise_softirq() 标记待处理 irq_exit() do_softirq() 直接执行 ksoftirqd 内核线程处理 说明:软中断优先在硬中断退出时执行;若堆积过多或耗时过长,则由 ksoftirqd 接管,避免用户进程饥饿。

2.4 Tasklet 机制详解

Tasklet 是建立在软中断之上的、更易用的下半部接口。它本质上是对 TASKLET_SOFTIRQ 软中断的封装。与软中断相比,tasklet 具有以下特点:

  • 串行化执行: 同一类型的 tasklet 在同一时刻只能在一个 CPU 上执行,避免了复杂的并发保护。
  • 动态注册: 驱动可以随时创建和销毁 tasklet。
  • 简单 API: 使用 DECLARE_TASKLET() 或 tasklet_init() 初始化,tasklet_schedule() 调度执行。
/* Tasklet 使用示例 */
#include <linux/interrupt.h>

/* 定义 tasklet 处理函数 */
void my_tasklet_handler(unsigned long data)
{
    /* 执行下半部工作,不可睡眠 */
    printk(KERN_INFO "Tasklet executed, data=%lu\n", data);
}

/* 静态声明 tasklet */
DECLARE_TASKLET(my_tasklet, my_tasklet_handler, 0);

/* 上半部中断处理函数 */
irqreturn_t my_interrupt_handler(int irq, void *dev_id)
{
    /* 调度 tasklet 执行 */
    tasklet_schedule(&my_tasklet);
    return IRQ_HANDLED;
}
使用建议: 对于大多数设备驱动,tasklet 是下半部的首选机制。它平衡了性能与易用性。仅在需要极高吞吐量且能精细控制并发的场景(如网络子系统)才直接使用软中断。

2.5 工作队列(Workqueue)机制详解

工作队列将下半部任务交给内核线程在进程上下文中执行。这带来了最大的灵活性:

  • 可睡眠: 可以调用 kmalloc(GFP_KERNEL)、mutex_lock()、copy_from_user() 等可能阻塞的函数。
  • 可管理: 工作项可以被取消、延迟、排队到特定的工作队列(如系统默认的 system_wq 或自定义的 per-CPU 队列)。
  • 上下文切换开销: 相比软中断和 tasklet,工作队列的调度延迟更高,不适合微秒级的高频任务。
/* 工作队列使用示例 */
#include <linux/workqueue.h>

/* 定义工作项处理函数 */
void my_work_handler(struct work_struct *work)
{
    /* 执行下半部工作,可以睡眠 */
    msleep(10);
    printk(KERN_INFO "Workqueue handler executed\n");
}

/* 声明工作项 */
DECLARE_WORK(my_work, my_work_handler);

/* 上半部中断处理函数 */
irqreturn_t my_interrupt_handler(int irq, void *dev_id)
{
    /* 将工作项提交到系统默认工作队列 */
    schedule_work(&my_work);
    return IRQ_HANDLED;
}
三种下半部机制上下文与调度对比 软中断 (Softirq) 中断上下文,可并行 Tasklet 中断上下文,串行化 工作队列 (Workqueue) 进程上下文,可睡眠 低延迟 高灵活性 选择指南 • 高频、轻量、对延迟敏感 → 软中断(如网络协议栈) • 中等频率、需要简单并发保护 → Tasklet(如块设备、USB) • 低频、需要睡眠或复杂操作 → 工作队列(如文件系统、IOCTL)

2.6 中断处理流程总结

一个完整的中断处理生命周期可以概括为以下步骤:

  1. 硬件中断触发: CPU 接收到中断信号,保存当前上下文,跳转到中断向量表。
  2. 上半部执行: 调用注册的 irq_handler_t,在关闭中断的情况下执行紧急操作。
  3. 调度下半部: 上半部通过 raise_softirq()、tasklet_schedule() 或 schedule_work() 标记或提交下半部任务。
  4. 中断退出: 恢复硬件中断使能,检查是否有待处理的软中断。
  5. 下半部执行: 在合适的时机(软中断上下文或进程上下文)执行下半部处理函数。
性能优化关键: 上半部应尽可能短,只做“记录”和“调度”两件事。下半部的选择直接影响系统的吞吐量和调度延迟。对于中断负载均衡而言,理解这三种机制的上下文切换成本和并发模型,是后续配置 SMP IRQ Affinity 和 RPS/RFS 的基础。