4、顶半部与底半部机制:顶半部(硬中断)职责、底半部(软中断/tasklet/workqueue)职责、分工原则
4.1 中断处理的两阶段模型
Linux 内核将中断处理严格划分为两个阶段:顶半部(Top Half)和底半部(Bottom Half)。这种划分的核心目的是在响应速度与处理能力之间取得平衡——硬中断上下文必须极短,而耗时工作可以推迟到更宽松的上下文中执行。
核心原则:顶半部只做“最少且必要”的工作,将大部分处理逻辑推迟到底半部。这直接决定了系统的实时性与吞吐量。
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())。它是三种机制中延迟最大但能力最强的,适合执行耗时的处理任务。
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 的串行化特性,减少自旋锁的使用。