4. 上半部与下半部机制:上半部(硬中断)与下半部(Tasklet/Workqueue)的分工
中断处理的核心矛盾在于:响应速度与处理工作量之间的冲突。硬件中断发生时,处理器必须尽快响应,否则可能导致数据丢失或硬件超时;但中断处理程序往往需要完成大量工作,如数据拷贝、协议解析、设备状态查询等。若将所有工作都放在中断上下文中完成,会长时间关闭本地中断,导致系统实时性急剧下降。
Linux 内核通过将中断处理拆分为上半部(Top Half)和下半部(Bottom Half)来解决这一矛盾。上半部在硬中断上下文中执行,要求极短且不可睡眠;下半部在稍后的安全时机执行,允许延迟、可睡眠(取决于具体机制),从而在响应速度与处理能力之间取得平衡。
4.1 上半部(硬中断上下文)
上半部即注册到 request_irq() 或 request_threaded_irq() 的 handler 函数,它在硬件中断触发时立即执行,运行在硬中断上下文中。其核心约束如下:
| 特性 | 说明 |
|---|---|
| 执行上下文 | 硬中断上下文(IRQ context),不可睡眠 |
| 中断状态 | 默认关闭本地 CPU 的中断(除非使用 IRQF_DISABLED 标志) |
| 允许操作 | 原子操作、寄存器读写、唤醒下半部、设置状态标志 |
| 禁止操作 | 睡眠、信号量、用户空间访问、大块内存分配 |
| 典型耗时 | 微秒级,通常不超过几十微秒 |
4.2 下半部机制演进与选择
Linux 内核提供了多种下半部实现机制,从早期的 BH(Bottom Half)到软中断(Softirq)、Tasklet,再到 Workqueue。当前主流驱动开发中,Tasklet 和 Workqueue 是最常用的两种选择。
| 机制 | 执行上下文 | 可睡眠 | 并发模型 | 适用场景 |
|---|---|---|---|---|
| Softirq | 软中断上下文 | 否 | 同一类型可在多 CPU 上并行 | 网络栈、块设备等高性能子系统 |
| Tasklet | 软中断上下文 | 否 | 同一 Tasklet 串行执行,不同 Tasklet 可并行 | 大多数驱动下半部,简单、轻量 |
| Workqueue | 进程上下文 | 是 | 工作线程池调度,可并行 | 需要睡眠、大块内存操作、锁竞争复杂的任务 |
4.3 Tasklet 机制详解
Tasklet 基于 Softirq 实现,是驱动开发中最常用的下半部机制之一。其核心数据结构为 struct tasklet_struct:
struct tasklet_struct {
struct tasklet_struct *next;
unsigned long state; /* TASKLET_STATE_SCHED / TASKLET_STATE_RUN */
atomic_t count; /* 0 表示激活,非 0 表示禁用 */
void (*func)(unsigned long data);
unsigned long data;
};
Tasklet 生命周期与使用步骤:
- 声明与初始化:使用
DECLARE_TASKLET(name, func, data)静态声明,或tasklet_init()动态初始化。 - 调度:在上半部中调用
tasklet_schedule(&my_tasklet),将 Tasklet 挂入当前 CPU 的待执行队列。 - 执行:在
do_softirq()中被调用,同一 Tasklet 在同一时刻最多在一个 CPU 上执行。 - 禁用/启用:
tasklet_disable()/tasklet_enable()用于同步控制。 - 销毁:
tasklet_kill()等待 Tasklet 执行完毕并移除。
4.4 Workqueue 机制详解
Workqueue 将下半部工作交给内核工作线程(kworker)在进程上下文中执行。对于需要睡眠、执行文件操作、使用信号量或进行大块内存分配的场景,Workqueue 是唯一选择。
Workqueue 核心 API:
| API | 功能 |
|---|---|
INIT_WORK(work, func) |
初始化工作项 |
schedule_work(work) |
将工作提交到系统默认工作队列(system_wq) |
schedule_delayed_work(dwork, delay) |
延迟指定 jiffies 后执行 |
flush_work(work) |
等待工作项完成 |
cancel_work_sync(work) |
取消并等待工作项结束 |
system_wq)即可。若需要隔离或控制并发度,可使用 alloc_workqueue() 创建专用工作队列,并设置 WQ_UNBOUND 或 WQ_HIGHPRI 等标志。
4.5 上半部与下半部协作流程
下图展示了从硬件中断触发到下半部执行的完整协作流程:
4.6 实战代码模板:Tasklet 与 Workqueue 对比
以下代码展示了同一设备驱动中,分别使用 Tasklet 和 Workqueue 实现下半部的典型模式:
/* ========== Tasklet 版本 ========== */
static struct tasklet_struct my_tasklet;
/* 下半部处理函数(软中断上下文,不可睡眠) */
static void my_tasklet_handler(unsigned long data)
{
struct my_device *dev = (struct my_device *)data;
/* 快速处理:读取 FIFO、更新统计、唤醒等待队列 */
spin_lock(&dev->lock);
dev->rx_count += dev->fifo_level;
spin_unlock(&dev->lock);
wake_up_interruptible(&dev->wq);
}
/* 上半部 */
static irqreturn_t my_irq_handler(int irq, void *dev_id)
{
struct my_device *dev = (struct my_device *)dev_id;
/* 清除硬件中断标志 */
writel(0, dev->regs + INT_CLR);
/* 调度 Tasklet */
tasklet_schedule(&my_tasklet);
return IRQ_HANDLED;
}
/* 初始化 */
static int my_probe(struct platform_device *pdev)
{
/* ... 分配设备结构、映射寄存器 ... */
tasklet_init(&my_tasklet, my_tasklet_handler, (unsigned long)dev);
return request_irq(dev->irq, my_irq_handler, IRQF_SHARED, "mydev", dev);
}
/* ========== Workqueue 版本 ========== */
static struct work_struct my_work;
/* 下半部处理函数(进程上下文,可睡眠) */
static void my_work_handler(struct work_struct *work)
{
struct my_device *dev = container_of(work, struct my_device, work);
/* 可睡眠操作:拷贝数据到用户空间、调用 USB 核心 API */
struct my_data *data = kmalloc(sizeof(*data), GFP_KERNEL);
if (data) {
/* 处理数据 */
kfree(data);
}
}
/* 上半部 */
static irqreturn_t my_irq_handler_threaded(int irq, void *dev_id)
{
struct my_device *dev = (struct my_device *)dev_id;
writel(0, dev->regs + INT_CLR);
schedule_work(&my_work);
return IRQ_HANDLED;
}
/* 初始化 */
static int my_probe(struct platform_device *pdev)
{
/* ... */
INIT_WORK(&dev->work, my_work_handler);
return request_irq(dev->irq, my_irq_handler_threaded,
IRQF_SHARED, "mydev", dev);
}
- 若下半部只需做寄存器操作、更新原子变量、唤醒等待队列 → 使用 Tasklet(更轻量、开销更小)。
- 若下半部需要分配大块内存、访问用户空间、调用可能睡眠的 API → 使用 Workqueue。
- 若不确定,优先使用 Workqueue,它更安全、更灵活;Tasklet 适合对延迟极度敏感且路径极短的场景。
4.7 上半部与下半部的同步考量
上半部与下半部之间、下半部与其他上下文之间的数据共享必须谨慎处理同步问题:
| 共享场景 | 推荐同步机制 | 说明 |
|---|---|---|
| 上半部 ↔ Tasklet | spin_lock_irqsave() |
Tasklet 在软中断上下文,需关闭本地中断防止死锁 |
| 上半部 ↔ Workqueue | spin_lock_irqsave() 或 mutex |
Workqueue 可睡眠,但上半部不可,需用 spin_lock 保护共享数据 |
| Tasklet ↔ 进程上下文 | spin_lock_bh() |
关闭下半部抢占,防止与 Tasklet 形成死锁 |
| Workqueue ↔ 进程上下文 | mutex 或 spin_lock |
若 Workqueue 中不使用自旋锁,可用 mutex 简化 |
mutex_lock() 会导致内核崩溃,因为软中断上下文不允许睡眠。同样,在 Workqueue 中长时间持有自旋锁会阻塞其他工作项的执行,应尽量使用互斥锁或 RCU。
4.8 下半部机制性能对比
下表从延迟、吞吐量和资源消耗三个维度对比 Tasklet 与 Workqueue:
| 指标 | Tasklet | Workqueue |
|---|---|---|
| 调度延迟 | 极低(软中断返回后立即执行) | 中等(依赖工作线程调度) |
| 执行延迟 | 低(不可阻塞) | 可变(可能因阻塞而延长) |
| 吞吐量 | 高(轻量、无上下文切换) | 中(涉及进程切换开销) |
| 内存开销 | 小(每个 Tasklet ~40 字节) | 较大(工作项 + 工作线程栈) |
| 适用中断频率 | 高频率(>10K IRQ/s) | 中低频率(<1K IRQ/s) |