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 四种机制的结构与流程
下图展示了从硬件中断触发到四种下半部机制执行的完整流程:
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 机制选择决策树
在实际驱动开发中,可以根据以下条件快速选择合适的下半部机制:
4.5 机制对比总结
| 对比维度 | 软中断 | tasklet | 工作队列 | 内核线程 |
|---|---|---|---|---|
| 上下文 | 软中断 | 软中断 | 进程 | 进程 |
| 可睡眠 | ❌ | ❌ | ✅ | ✅ |
| 同类型并行 | ✅ | ❌ (串行) | ✅ (多线程池) | ✅ (手动控制) |
| API 复杂度 | 高 | 低 | 中 | 中 |
| 典型延迟 | ~10 µs | ~50 µs | ~1 ms | ~10 ms |
| 资源开销 | 极低 | 低 | 中 | 高 |
| 驱动使用频率 | 极少 | 高 | 高 | 中 |
实践建议:对于本课程后续的实验,我们将重点使用 tasklet 和 工作队列 两种机制。tasklet 适合处理简单的数据拷贝、状态更新;工作队列适合需要访问文件系统、执行复杂协议解析的场景。