4、顶半部与底半部机制:顶半部(硬中断)职责、底半部(软中断/tasklet/workqueue)职责、分工原则

4.1 中断处理的两阶段模型

Linux 内核将中断处理严格划分为两个阶段:顶半部(Top Half)和底半部(Bottom Half)。这种划分的核心目的是在响应速度与处理能力之间取得平衡——硬中断上下文必须极短,而耗时工作可以推迟到更宽松的上下文中执行。

核心原则:顶半部只做“最少且必要”的工作,将大部分处理逻辑推迟到底半部。这直接决定了系统的实时性与吞吐量。
顶半部与底半部处理流程 顶半部与底半部处理流程 硬件中断触发 顶半部(硬中断上下文) 保存寄存器 → 应答硬件 → 标记中断 → 调度底半部 软中断 (SoftIRQ) 高优先级、可抢占 Tasklet 基于软中断、串行化 Workqueue 进程上下文、可睡眠 顶半部执行时间必须 < 10μs;底半部可根据需要选择不同的延迟机制

4.2 顶半部(硬中断上下文)职责

顶半部在硬件中断触发后立即执行,运行在关中断或中断禁用的上下文中。其执行时间必须极短,通常限制在 10 微秒以内。

职责项 说明 典型操作
保存关键上下文 保存 CPU 寄存器、程序计数器等现场信息 pt_regs 保存
应答硬件 向中断控制器发送 ACK,清除中断挂起位 writel(REG, val)
标记中断源 记录中断类型、设备标识、状态标志 设置 irq_data 标志位
调度底半部 触发软中断、唤醒 tasklet 或 queue work raise_softirq() / tasklet_schedule() / queue_work()
快速返回 立即退出,不执行任何耗时逻辑 IRQ_HANDLED 返回
常见错误:在顶半部中执行 printk()、kmalloc(GFP_KERNEL)、mutex_lock() 等可能导致睡眠或长时间阻塞的操作。这些操作必须严格禁止。

4.3 底半部机制详解

底半部负责处理顶半部推迟下来的剩余工作。Linux 内核提供了三种主要的底半部实现机制,它们在优先级、上下文和可睡眠性上存在显著差异。

4.3.1 软中断(SoftIRQ)

软中断是最高优先级的底半部机制,在 do_softirq() 中执行。它运行在软中断上下文中,不可睡眠,且可以被硬中断抢占。软中断通常用于网络收发、块设备 I/O 等对延迟极度敏感的场景。

4.3.2 Tasklet

Tasklet 基于软中断实现,但提供了更简单的接口。同一类型的 tasklet 在任意时刻只能在一个 CPU 上执行,保证了串行化。Tasklet 同样运行在软中断上下文,不可睡眠。

4.3.3 Workqueue

Workqueue 运行在进程上下文(内核线程)中,可以睡眠,支持阻塞操作(如 mutex_lock()、msleep())。它是三种机制中延迟最大但能力最强的,适合执行耗时的处理任务。

三种底半部机制对比 三种底半部机制对比 软中断 (SoftIRQ) 上下文:软中断上下文 可睡眠:❌ 不可 优先级:最高 典型场景:网络、块设备 延迟:~1μs Tasklet 上下文:软中断上下文 可睡眠:❌ 不可 优先级:中 典型场景:简单设备驱动 延迟:~5μs Workqueue 上下文:进程上下文 可睡眠:✅ 可以 优先级:低 典型场景:复杂处理、I/O 延迟:~100μs+ 选择原则:延迟敏感 → 软中断;简单串行 → Tasklet;需要睡眠/阻塞 → Workqueue 注意:Workqueue 的延迟受内核线程调度影响,不适合硬实时场景

4.4 分工原则与选择策略

在驱动开发中,选择哪种底半部机制取决于具体的延迟需求、操作类型和上下文限制。以下表格总结了核心选择依据:

判断条件 推荐机制 理由
需要极低延迟(< 10μs) 软中断 在 do_softirq() 中立即执行,不被调度延迟
操作简单且需要串行化 Tasklet 同一 tasklet 不会并发执行,简化同步
需要睡眠(如等待硬件、分配内存) Workqueue 进程上下文支持 msleep()、mutex_lock()
处理时间较长(> 1ms) Workqueue 避免长时间占用软中断上下文,影响其他中断
需要访问用户空间 Workqueue 只有进程上下文可以访问用户态内存
高频率中断(如网卡) 软中断 + NAPI NAPI 轮询机制减少中断次数,提升吞吐量
实践建议:对于大多数设备驱动,优先使用 Workqueue 作为默认底半部机制。它提供了最灵活的处理能力,且在现代内核中通过 system_wq 或自定义 workqueue 可以很好地控制延迟。仅在确认延迟敏感且操作简单时,才考虑软中断或 Tasklet。

4.5 典型分工示例:网卡驱动

以网卡驱动为例,展示顶半部与底半部的典型分工:

/* 顶半部:硬中断处理函数 */
static irqreturn_t eth_irq_handler(int irq, void *dev_id)
{
    struct net_device *dev = dev_id;
    struct my_priv *priv = netdev_priv(dev);

    /* 1. 保存硬件状态寄存器 */
    priv->irq_status = readl(priv->regs + STATUS_REG);

    /* 2. 应答硬件中断(清除中断挂起) */
    writel(priv->irq_status, priv->regs + ACK_REG);

    /* 3. 调度底半部(使用 NAPI 或 workqueue) */
    if (priv->irq_status & RX_FLAG) {
        napi_schedule(&priv->napi);      /* 软中断方式 */
    }

    if (priv->irq_status & LINK_CHANGE) {
        queue_work(priv->wq, &priv->link_work);  /* workqueue 方式 */
    }

    return IRQ_HANDLED;
}

/* 底半部:workqueue 处理链路状态变化 */
static void link_change_work(struct work_struct *work)
{
    struct my_priv *priv = container_of(work, struct my_priv, link_work);

    /* 可以睡眠,等待 PHY 芯片稳定 */
    msleep(100);

    /* 读取 PHY 状态寄存器 */
    priv->phy_status = readl(priv->regs + PHY_REG);

    /* 调用内核 API 更新链路状态 */
    netif_carrier_on(priv->netdev);
}
关键点:顶半部只做了三件事——保存状态、应答硬件、调度底半部。耗时操作(msleep、读取 PHY、调用内核 API)全部放在 workqueue 中。这种分工保证了中断响应时间在微秒级,同时不影响后续中断的接收。

4.6 分工原则总结

  • 顶半部最小化原则:只做必须立即完成的操作(保存现场、应答硬件、调度底半部),其余全部推迟。
  • 底半部匹配原则:根据操作是否可睡眠、延迟要求、并发需求选择软中断/Tasklet/Workqueue。
  • 不可睡眠操作禁止原则:在硬中断上下文和软中断上下文中,严禁调用任何可能导致睡眠的函数。
  • 优先级递减原则:软中断 > Tasklet > Workqueue,延迟敏感度越高,越应使用优先级高的机制。
  • 同步简化原则:优先使用 Tasklet 或 Workqueue 的串行化特性,减少自旋锁的使用。