4. 上半部与下半部机制:上半部(硬中断)与下半部(Tasklet/Workqueue)的分工

中断处理的核心矛盾在于:响应速度与处理工作量之间的冲突。硬件中断发生时,处理器必须尽快响应,否则可能导致数据丢失或硬件超时;但中断处理程序往往需要完成大量工作,如数据拷贝、协议解析、设备状态查询等。若将所有工作都放在中断上下文中完成,会长时间关闭本地中断,导致系统实时性急剧下降。

Linux 内核通过将中断处理拆分为上半部(Top Half)和下半部(Bottom Half)来解决这一矛盾。上半部在硬中断上下文中执行,要求极短且不可睡眠;下半部在稍后的安全时机执行,允许延迟、可睡眠(取决于具体机制),从而在响应速度与处理能力之间取得平衡。

4.1 上半部(硬中断上下文)

上半部即注册到 request_irq() 或 request_threaded_irq() 的 handler 函数,它在硬件中断触发时立即执行,运行在硬中断上下文中。其核心约束如下:

特性 说明
执行上下文 硬中断上下文(IRQ context),不可睡眠
中断状态 默认关闭本地 CPU 的中断(除非使用 IRQF_DISABLED 标志)
允许操作 原子操作、寄存器读写、唤醒下半部、设置状态标志
禁止操作 睡眠、信号量、用户空间访问、大块内存分配
典型耗时 微秒级,通常不超过几十微秒
设计原则:上半部只做最紧急、最轻量的工作——确认中断、清除硬件中断标志、从 FIFO 读取关键数据、调度下半部。其余所有非紧急处理都推给下半部。

4.2 下半部机制演进与选择

Linux 内核提供了多种下半部实现机制,从早期的 BH(Bottom Half)到软中断(Softirq)、Tasklet,再到 Workqueue。当前主流驱动开发中,Tasklet 和 Workqueue 是最常用的两种选择。

机制 执行上下文 可睡眠 并发模型 适用场景
Softirq 软中断上下文 否 同一类型可在多 CPU 上并行 网络栈、块设备等高性能子系统
Tasklet 软中断上下文 否 同一 Tasklet 串行执行,不同 Tasklet 可并行 大多数驱动下半部,简单、轻量
Workqueue 进程上下文 是 工作线程池调度,可并行 需要睡眠、大块内存操作、锁竞争复杂的任务
关键区别:Tasklet 运行在软中断上下文中,不能睡眠,不能使用信号量或阻塞式 API;Workqueue 运行在进程上下文中,可以睡眠,可以使用大部分内核 API。选择时需根据下半部是否需要睡眠来决定。

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 生命周期与使用步骤:

  1. 声明与初始化:使用 DECLARE_TASKLET(name, func, data) 静态声明,或 tasklet_init() 动态初始化。
  2. 调度:在上半部中调用 tasklet_schedule(&my_tasklet),将 Tasklet 挂入当前 CPU 的待执行队列。
  3. 执行:在 do_softirq() 中被调用,同一 Tasklet 在同一时刻最多在一个 CPU 上执行。
  4. 禁用/启用:tasklet_disable() / tasklet_enable() 用于同步控制。
  5. 销毁:tasklet_kill() 等待 Tasklet 执行完毕并移除。
Tasklet 执行保证:同一 Tasklet 不会在多个 CPU 上同时执行,但不同 Tasklet 可以在不同 CPU 上并行。这简化了驱动开发者的并发控制,无需为 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 上半部与下半部协作流程

下图展示了从硬件中断触发到下半部执行的完整协作流程:

上半部与下半部协作流程图 上半部与下半部协作流程 硬件中断触发 上半部(硬中断上下文) 清除中断标志 · 读取关键数据 · 调度下半部 Tasklet(软中断上下文) Workqueue(进程上下文) 不可睡眠 · 同一 Tasklet 串行 适合轻量、快速处理 可睡眠 · 工作线程池调度 适合重量、阻塞式处理 中断处理完成

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 简化
常见陷阱:在 Tasklet 中使用 mutex_lock() 会导致内核崩溃,因为软中断上下文不允许睡眠。同样,在 Workqueue 中长时间持有自旋锁会阻塞其他工作项的执行,应尽量使用互斥锁或 RCU。

4.8 下半部机制性能对比

下表从延迟、吞吐量和资源消耗三个维度对比 Tasklet 与 Workqueue:

指标 Tasklet Workqueue
调度延迟 极低(软中断返回后立即执行) 中等(依赖工作线程调度)
执行延迟 低(不可阻塞) 可变(可能因阻塞而延长)
吞吐量 高(轻量、无上下文切换) 中(涉及进程切换开销)
内存开销 小(每个 Tasklet ~40 字节) 较大(工作项 + 工作线程栈)
适用中断频率 高频率(>10K IRQ/s) 中低频率(<1K IRQ/s)