4、底半部机制概览:软中断、tasklet、工作队列、内核线程的对比与选择

在 Linux 内核的中断处理框架中,底半部(Bottom Half) 机制负责执行那些不要求立即响应、但需要在内核上下文中完成的任务。Linux 提供了四种主要的底半部实现方式:软中断(SoftIRQ)、tasklet、工作队列(Workqueue) 和 内核线程(Kernel Thread)。理解它们的差异与适用场景,是编写高效、稳定驱动程序的基石。

核心原则: 顶半部(硬中断上下文)只做最紧急的硬件操作(如清除中断标志、拷贝 FIFO 数据),其余所有处理逻辑都应下沉到底半部。选择哪种底半部,取决于对 实时性、可睡眠性 和 执行上下文 的要求。

4.1 四种底半部机制总览

下表从执行上下文、是否可睡眠、并发模型、典型用途四个维度,对四种机制进行横向对比:

特性 软中断 (SoftIRQ) tasklet 工作队列 (Workqueue) 内核线程 (Kthread)
执行上下文 软中断上下文(原子) 软中断上下文(原子) 进程上下文(可睡眠) 进程上下文(可睡眠)
是否可睡眠 ❌ 不可 ❌ 不可 ✅ 可以 ✅ 可以
并发模型 同一类型可在多 CPU 上并行 同一类型串行执行 由 worker-pool 调度,可并行 完全由调度器管理
典型用途 网络收发、块设备层 简单、低开销的后处理 需要持有锁、访问文件系统 长时间运行的后台监控
创建/销毁开销 静态分配(编译时) 低(动态注册) 中(需要创建工作项) 高(完整线程生命周期)
关键区分: 软中断和 tasklet 运行在 软中断上下文,不能睡眠、不能持有信号量、不能访问用户空间。工作队列和内核线程运行在 进程上下文,可以睡眠、可以持有互斥锁、可以执行可能阻塞的 I/O 操作。

4.2 软中断(SoftIRQ)

软中断是内核中优先级最高、最接近硬中断的底半部机制。它在硬中断处理完成后立即执行,且允许同一类型的软中断在不同 CPU 上同时运行。

  • 静态分配: 软中断索引在 include/linux/interrupt.h 中通过枚举定义,编译时确定,不支持动态注册。
  • 高性能: 由于无锁设计(依赖 per-CPU 变量)和并行执行,软中断是网络栈和块设备层的首选。
  • 使用限制: 开发者通常不直接编写软中断,而是通过 tasklet 或内核提供的专用软中断(如 NET_RX_SOFTIRQ)间接使用。
/* 软中断枚举示例(位于 kernel/softirq.c 和 include/linux/interrupt.h) */
enum {
    HI_SOFTIRQ = 0,      /* 高优先级 tasklet */
    TIMER_SOFTIRQ,       /* 定时器 */
    NET_TX_SOFTIRQ,      /* 网络发送 */
    NET_RX_SOFTIRQ,      /* 网络接收 */
    BLOCK_SOFTIRQ,       /* 块设备 */
    IRQ_POLL_SOFTIRQ,    /* 中断轮询 */
    TASKLET_SOFTIRQ,     /* 普通 tasklet */
    SCHED_SOFTIRQ,       /* 调度器 */
    HRTIMER_SOFTIRQ,     /* 高精度定时器 */
    RCU_SOFTIRQ,         /* RCU 回调 */
    NR_SOFTIRQS
};
软中断执行流程 硬中断处理完成 __do_softirq() 执行 pending 软中断 检查是否超时/重入 唤醒 ksoftirqd ⚠ 软中断上下文 禁止睡眠、禁止阻塞

4.3 tasklet

tasklet 是建立在软中断之上的简化接口。它继承了软中断的原子性,但提供了更友好的编程模型:同一 tasklet 在任意时刻只能在一个 CPU 上执行,避免了开发者处理并发问题。

  • 串行保证: 同一类型的 tasklet 不会并行执行,简化了数据保护逻辑。
  • 动态注册: 使用 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);

/* 在中断顶半部中调度 tasklet */
irqreturn_t my_interrupt_handler(int irq, void *dev_id)
{
    /* ... 硬件操作 ... */
    tasklet_schedule(&my_tasklet);
    return IRQ_HANDLED;
}
注意: 如果 tasklet 处理函数中调用了可能导致睡眠的函数(如 kmalloc(GFP_KERNEL)、mutex_lock()),内核会触发 BUG: scheduling while atomic 错误。此时必须改用工作队列或内核线程。

4.4 工作队列(Workqueue)

工作队列将底半部任务委托给内核线程执行,从而允许在进程上下文中处理中断后任务。它是 Linux 内核中最灵活、最常用的底半部机制之一。

  • 进程上下文: 可以睡眠、可以持有信号量、可以执行文件系统操作。
  • 并发管理: 现代内核使用 CMWQ(Concurrency Managed Workqueue),自动管理 worker 线程数量,避免资源浪费。
  • API 丰富: 支持延迟工作、排队到指定 CPU、取消工作项等高级功能。
/* 工作队列使用示例 */
#include <linux/workqueue.h>

/* 定义工作处理函数 */
void my_work_handler(struct work_struct *work)
{
    /* 进程上下文,可以睡眠 */
    msleep(100);  /* 模拟阻塞操作 */
    printk(KERN_INFO "work executed in process context\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;
}
工作队列调度架构 (CMWQ) 中断顶半部 工作项 A 工作项 B 工作项 C 全局 worklist (per-pool) worker-0 (CPU0) worker-1 (CPU1) worker-2 (CPU2) 💡 CMWQ 自动调整 worker 数量:任务多时创建新 worker,空闲时销毁,避免线程泛滥

4.5 内核线程(Kthread)

内核线程是运行在内核空间的独立执行单元,由内核调度器管理。它是最重量级的底半部方案,适用于需要长期存在、持续监控或执行复杂业务逻辑的场景。

  • 完全控制: 开发者可以设置优先级、绑定 CPU、管理线程生命周期。
  • 资源开销: 每个内核线程拥有独立的栈和 task_struct,创建/销毁开销较大。
  • 通信机制: 通常与中断处理程序通过 等待队列(waitqueue) 或 completion 机制交互。
/* 内核线程与中断配合示例 */
#include <linux/kthread.h>
#include <linux/wait.h>

static struct task_struct *my_thread;
static DECLARE_WAIT_QUEUE_HEAD(my_wq);
static int event_flag = 0;

/* 内核线程主函数 */
static int my_kthread_func(void *data)
{
    while (!kthread_should_stop()) {
        wait_event_interruptible(my_wq, event_flag != 0);
        if (kthread_should_stop())
            break;
        /* 处理中断传递的数据,可以睡眠 */
        printk(KERN_INFO "kthread processing...\n");
        event_flag = 0;
    }
    return 0;
}

/* 中断处理程序 */
irqreturn_t my_interrupt_handler(int irq, void *dev_id)
{
    /* ... 硬件操作 ... */
    event_flag = 1;
    wake_up_interruptible(&my_wq);  /* 唤醒内核线程 */
    return IRQ_HANDLED;
}

/* 模块初始化 */
static int __init my_init(void)
{
    my_thread = kthread_run(my_kthread_func, NULL, "my_monitor");
    return PTR_ERR_OR_ZERO(my_thread);
}

4.6 选择指南:何时使用哪种机制?

以下决策树可以帮助你在实际开发中快速做出选择:

  1. 处理函数是否需要睡眠?
    • 是 → 进入第 2 步
    • 否 → 进入第 3 步
  2. 任务是否长期存在或需要复杂状态管理?
    • 是 → 内核线程
    • 否 → 工作队列
  3. 是否需要最高性能且能处理并发?
    • 是 → 软中断(如网络驱动)
    • 否 → tasklet(简单、安全)
经验法则:
  • 80% 的驱动底半部需求可以用 工作队列 满足。
  • 如果处理逻辑极其简单(如只设置一个标志位),使用 tasklet 减少开销。
  • 只有网络、块设备等性能关键路径才考虑直接使用 软中断。
  • 内核线程 通常用于实现内核中的服务进程(如 kswapd、jbd2),而非普通驱动。

4.7 性能与开销对比

指标 软中断 tasklet 工作队列 内核线程
调度延迟 极低(~1μs) 低(~2μs) 中(~10μs) 高(~100μs)
上下文切换 无 无 有(进程切换) 有(进程切换)
内存开销 极小(静态) 小(~100 字节) 中(~1KB) 大(~8KB 栈 + task_struct)
适用中断频率 极高(>10K/s) 高(1K~10K/s) 中(100~1K/s) 低(<100/s)
性能陷阱: 不要因为工作队列“方便”就在高频中断中使用它。如果中断频率超过 1000 次/秒,工作队列的进程切换开销可能导致 CPU 负载飙升,甚至触发 soft lockup 警告。此时应优先考虑 tasklet 或软中断。
底半部机制选择决策图 中断底半部需求 可睡眠? 是 否 长期存在? 是 否 内核线程 工作队列 高性能? 是 否 软中断 tasklet

4.8 小结

本章详细对比了 Linux 内核中四种底半部机制的核心特性、使用场景和性能开销。核心要点总结如下:

  • 软中断:最高性能,但编程复杂,适用于网络、块设备等核心子系统。
  • tasklet:软中断的简化版,串行执行,适合简单、低开销的后处理。
  • 工作队列:进程上下文,可睡眠,是通用驱动开发的首选底半部机制。
  • 内核线程:最重量级,适合长期运行的后台服务或复杂状态机。