4. 下半部机制概览:软中断、tasklet、工作队列、内核线程

中断处理的下半部(Bottom Half)机制是 Linux 内核将耗时工作从硬中断上下文推迟到更安全环境执行的核心手段。理解不同下半部机制的特性与适用场景,是编写高效设备驱动的关键前提。

核心原则:硬中断必须极短(通常 < 50 µs),所有非紧急的数据处理、协议解析、I/O 操作都应推迟到下半部执行。

4.1 下半部机制演进与分类

Linux 内核从 2.4 到 6.x 版本,下半部机制经历了多次重构。当前主流内核(5.x / 6.x)中,可用的下半部机制包括以下四种:

机制 引入版本 执行上下文 可睡眠 并发度 典型延迟
软中断 (SoftIRQ) Linux 2.4 软中断上下文 否 同类型可多核并行 ~ 10 µs
tasklet Linux 2.4 软中断上下文 否 同类型串行 ~ 50 µs
工作队列 (Workqueue) Linux 2.6 进程上下文 是 由内核线程调度 ~ 1 ms
内核线程 (kthread) Linux 2.6 进程上下文 是 完全由调度器管理 ~ 10 ms
关键区分:软中断和 tasklet 运行在软中断上下文,不能睡眠、不能阻塞、不能使用信号量;工作队列和内核线程运行在进程上下文,可以睡眠、可以持有锁、可以执行文件系统操作。

4.2 四种机制的结构与流程

下图展示了从硬件中断触发到四种下半部机制执行的完整流程:

下半部机制执行流程对比图 下半部机制执行流程对比 硬件中断 (IRQ) 上半部 (Top Half) 软中断 (SoftIRQ) tasklet 工作队列 (WQ) 内核线程 (kthread) 软中断上下文 软中断上下文 进程上下文 进程上下文 同类型可并行 延迟 ~10 µs 同类型串行 延迟 ~50 µs 可睡眠 延迟 ~1 ms 完全可睡眠 延迟 ~10 ms 选择原则:延迟要求越严格,越靠近软中断;需要睡眠能力,必须使用进程上下文机制 tasklet 是软中断的简化封装,工作队列是进程上下文的首选

4.3 各机制详解

4.3.1 软中断 (SoftIRQ)

软中断是最底层、延迟最低的下半部机制,直接由内核管理。软中断在 __do_softirq() 函数中执行,具有以下特点:

  • 静态分配:软中断索引在编译时确定,目前内核定义了 10 种软中断(如 NET_TX_SOFTIRQ、TASKLET_SOFTIRQ)。
  • 同类型可并行:同一类型的软中断可以在不同 CPU 上同时执行,必须使用 per-CPU 变量或自旋锁保护数据。
  • 不可睡眠:软中断上下文禁止调用任何可能睡眠的函数(如 kmalloc(GFP_KERNEL)、mutex_lock())。
  • 使用场景:网络协议栈、块设备层、定时器等对延迟极度敏感的子系统。
注意:普通驱动开发者通常不直接使用软中断,而是使用其封装——tasklet。直接注册软中断需要修改内核全局软中断表,属于内核核心开发范畴。

4.3.2 tasklet

tasklet 是建立在软中断之上的简化接口,是驱动开发中最常用的下半部机制之一。其核心特性:

  • 同类型串行:同一 tasklet 在同一时刻只能在一个 CPU 上执行,不同 tasklet 可以并行。
  • 基于 TASKLET_SOFTIRQ:tasklet 实际通过软中断索引 TASKLET_SOFTIRQ 调度。
  • 简单 API:只需定义处理函数,调用 tasklet_schedule() 即可触发。
  • 不可睡眠:与软中断相同,tasklet 上下文禁止睡眠。
/* tasklet 典型使用模式 */
#include <linux/interrupt.h>

/* 1. 定义 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);
}

/* 2. 声明并初始化 tasklet */
DECLARE_TASKLET(my_tasklet, my_tasklet_handler, (unsigned long)&my_dev);

/* 3. 在中断上半部中调度 */
irqreturn_t my_interrupt_handler(int irq, void *dev_id)
{
    struct my_device *dev = (struct my_device *)dev_id;
    /* 保存硬件数据到 dev->buffer */
    tasklet_schedule(&my_tasklet);  /* 触发下半部 */
    return IRQ_HANDLED;
}

4.3.3 工作队列 (Workqueue)

工作队列将工作推迟到内核线程中执行,运行在进程上下文。这是唯一可以睡眠的下半部机制:

  • 进程上下文:工作队列函数可以调用 msleep()、wait_event()、mutex_lock() 等阻塞函数。
  • 内核线程池:现代内核使用 CMWQ(Concurrency Managed Workqueue),动态管理工作者线程数量。
  • 延迟较高:由于涉及线程调度,工作队列的延迟通常在毫秒级别。
  • 使用场景:需要访问文件系统、需要大量内存分配、需要等待硬件操作完成的任务。
/* 工作队列典型使用模式 */
#include <linux/workqueue.h>

/* 1. 定义工作结构体 */
struct work_struct my_work;

/* 2. 工作处理函数 */
void my_work_handler(struct work_struct *work)
{
    /* 可以睡眠! */
    struct my_data *data = container_of(work, struct my_data, work);
    msleep(10);  /* 模拟耗时操作 */
    data->result = compute(data->input);
}

/* 3. 初始化工作 */
INIT_WORK(&my_work, my_work_handler);

/* 4. 在中断上半部中调度 */
irqreturn_t my_interrupt_handler(int irq, void *dev_id)
{
    struct my_device *dev = (struct my_device *)dev_id;
    /* 记录中断信息 */
    schedule_work(&dev->work);  /* 使用系统默认工作队列 */
    return IRQ_HANDLED;
}

4.3.4 内核线程 (kthread)

内核线程是最灵活的下半部机制,由开发者自行创建和管理内核线程:

  • 完全控制:开发者可以设置线程优先级、CPU 亲和性、调度策略。
  • 持续运行:内核线程通常包含一个循环,持续等待工作到来。
  • 资源开销:每个内核线程占用独立的栈空间和调度实体,资源消耗最大。
  • 使用场景:需要长期监控、周期性轮询、复杂状态机的驱动。
/* 内核线程典型使用模式 */
#include <linux/kthread.h>
#include <linux/sched.h>

static struct task_struct *my_thread;

/* 内核线程函数 */
static int my_kthread_func(void *data)
{
    struct my_device *dev = (struct my_device *)data;
    
    while (!kthread_should_stop()) {
        /* 等待工作条件 */
        wait_event_interruptible(dev->wq, dev->work_pending);
        
        if (dev->work_pending) {
            /* 执行复杂处理,可以睡眠 */
            process_data(dev);
            dev->work_pending = false;
        }
    }
    return 0;
}

/* 启动内核线程 */
int start_my_thread(struct my_device *dev)
{
    my_thread = kthread_run(my_kthread_func, dev, "my_device_wq");
    if (IS_ERR(my_thread))
        return PTR_ERR(my_thread);
    return 0;
}

/* 停止内核线程 */
void stop_my_thread(void)
{
    if (my_thread) {
        kthread_stop(my_thread);
        my_thread = NULL;
    }
}

4.4 机制选择决策树

在实际驱动开发中,可以根据以下条件快速选择合适的下半部机制:

下半部机制选择决策树 下半部机制选择决策树 需要睡眠? 是 需要持续监控? 是 内核线程 否 工作队列 否 需要极低延迟? 是 软中断 否 tasklet 提示:tasklet 是软中断的简化封装,驱动开发首选;工作队列是进程上下文的首选 内核线程仅在需要长期独立控制循环时使用

4.5 机制对比总结

对比维度 软中断 tasklet 工作队列 内核线程
上下文 软中断 软中断 进程 进程
可睡眠 ❌ ❌ ✅ ✅
同类型并行 ✅ ❌ (串行) ✅ (多线程池) ✅ (手动控制)
API 复杂度 高 低 中 中
典型延迟 ~10 µs ~50 µs ~1 ms ~10 ms
资源开销 极低 低 中 高
驱动使用频率 极少 高 高 中
实践建议:对于本课程后续的实验,我们将重点使用 tasklet 和 工作队列 两种机制。tasklet 适合处理简单的数据拷贝、状态更新;工作队列适合需要访问文件系统、执行复杂协议解析的场景。