4. 中断处理流程:上半部 (Top Half) 与下半部 (Bottom Half) 机制详解
在 Linux 内核中,中断处理程序必须快速执行,以避免阻塞其他中断或导致系统响应延迟。然而,许多设备在中断发生后需要完成大量工作(例如数据拷贝、协议处理、状态更新)。为了解决“快速响应”与“繁重处理”之间的矛盾,内核将中断处理流程拆分为两个阶段:上半部 (Top Half) 和 下半部 (Bottom Half)。
4.1 上半部 (Top Half) 机制
上半部是中断处理程序本身,即注册到 request_irq() 或 request_threaded_irq() 中的处理函数。它在中断上下文(原子上下文)中运行,具有以下特征:
- 执行环境:中断上下文,不可睡眠,不可调度。
- 中断状态:执行期间,当前 CPU 上的本地中断通常被禁用(取决于 IRQ 标志),但其他 CPU 仍可处理中断。
- 核心职责:快速确认硬件、保存关键状态、清除中断挂起位、将耗时工作调度到下半部。
- 性能要求:必须极短(微秒级),否则会导致高延迟甚至中断丢失。
关键原则: 上半部只做“最少且必要”的操作。任何可以推迟的工作都应交给下半部。
4.2 下半部 (Bottom Half) 机制
下半部负责执行中断触发的“重活”,它运行在软中断上下文或进程上下文,允许睡眠(取决于具体机制),并且可以被更高优先级的中断抢占。Linux 内核提供了多种下半部实现机制,各有适用场景:
| 机制 | 上下文 | 可睡眠 | 并发度 | 典型用途 |
|---|---|---|---|---|
| 软中断 (Softirq) | 软中断上下文 | 否 | 同类型可在多 CPU 并行 | 网络 RX/TX、块设备层、定时器 |
| Tasklet | 软中断上下文 | 否 | 同类型串行(同一 CPU 上) | 简单设备驱动、旧式网络协议 |
| 工作队列 (Workqueue) | 进程上下文 | 是 | 由内核线程调度 | 需要睡眠的操作(如 I/O、锁) |
| 线程化中断 (Threaded IRQ) | 进程上下文 | 是 | 专用内核线程 | 需要互斥体、复杂状态机的驱动 |
选择建议: 对于性能敏感且无需睡眠的场景,优先使用软中断或 Tasklet;对于需要阻塞操作(如访问 I2C/SPI 设备、分配内存)的场景,必须使用工作队列或线程化中断。
4.3 完整中断处理流程 (SVG 架构图)
下图展示了从硬件中断触发到上半部、再到下半部调度的完整流程:
4.4 上半部与下半部的协作模式
在实际驱动中,上半部与下半部通过“标记-调度”模式协作。以下是一个典型的网络设备驱动中断处理代码片段:
/* 上半部:中断处理程序 */
static irqreturn_t netdev_interrupt(int irq, void *dev_id)
{
struct net_device *dev = dev_id;
struct my_priv *priv = netdev_priv(dev);
/* 1. 读取硬件状态寄存器,确认中断源 */
u32 status = readl(priv->regs + STATUS_REG);
/* 2. 清除中断挂起位,防止重复触发 */
writel(status & TX_DONE_MASK | RX_PKT_MASK, priv->regs + CLEAR_REG);
/* 3. 保存关键状态到私有数据结构 */
priv->last_status = status;
/* 4. 调度下半部:使用 NAPI(基于软中断)处理接收数据 */
if (status & RX_PKT_MASK) {
napi_schedule(&priv->napi);
}
/* 5. 如果发送完成,唤醒发送队列(使用 Tasklet) */
if (status & TX_DONE_MASK) {
tasklet_schedule(&priv->tx_tasklet);
}
return IRQ_HANDLED;
}
/* 下半部:NAPI poll 函数(软中断上下文) */
static int netdev_poll(struct napi_struct *napi, int budget)
{
struct my_priv *priv = container_of(napi, struct my_priv, napi);
int work_done = 0;
/* 从硬件 FIFO 读取数据包,送入协议栈 */
while (work_done < budget) {
struct sk_buff *skb = ...; /* 从硬件读取数据 */
if (!skb) break;
netif_receive_skb(skb);
work_done++;
}
if (work_done < budget) {
napi_complete_done(napi, work_done);
/* 重新使能接收中断 */
enable_irq(priv->irq);
}
return work_done;
}
常见陷阱: 在上半部中调用可能导致睡眠的函数(如
kmalloc(GFP_KERNEL)、mutex_lock())会导致内核崩溃。所有需要睡眠的操作必须推迟到下半部(工作队列或线程化中断)中执行。
4.5 线程化中断:上半部与下半部的融合
对于复杂设备驱动,内核提供了 线程化中断 (Threaded IRQ) 机制,将上半部精简为“快速确认 + 唤醒线程”,而所有业务逻辑都在内核线程中执行。这简化了驱动开发,但增加了上下文切换开销。
| 特性 | 传统上半部 + 下半部 | 线程化中断 |
|---|---|---|
| 上半部代码量 | 较多(需手动调度下半部) | 极少(仅确认 + 返回 IRQ_WAKE_THREAD) |
| 下半部执行上下文 | 软中断 / Tasklet / 工作队列 | 专用内核线程 |
| 可睡眠 | 取决于机制 | 是 |
| 实时性 | 高(软中断优先级高) | 中(受内核线程调度影响) |
| 适用场景 | 高性能网络、存储 | 慢速设备、复杂状态机、需要 RT-mutex |
/* 线程化中断注册示例 */
static irqreturn_t my_irq_handler(int irq, void *dev_id)
{
/* 上半部:仅确认硬件,不做任何耗时操作 */
writel(0x1, REG_ACK);
return IRQ_WAKE_THREAD; /* 唤醒下半部线程 */
}
static irqreturn_t my_irq_thread(int irq, void *dev_id)
{
struct my_dev *dev = dev_id;
/* 下半部线程:可以睡眠,可以持有互斥体 */
mutex_lock(&dev->lock);
/* 执行复杂的状态机处理、I2C/SPI 读写等 */
process_hardware_event(dev);
mutex_unlock(&dev->lock);
return IRQ_HANDLED;
}
/* 注册时使用 IRQF_ONESHOT 标志,确保中断线在 thread 执行期间保持屏蔽 */
request_threaded_irq(irq, my_irq_handler, my_irq_thread,
IRQF_TRIGGER_RISING | IRQF_ONESHOT,
"my_device", dev);
4.6 性能考量与排查要点
- 上半部过长:使用
ftrace或perf测量中断处理时间。若上半部超过 100μs,应考虑将更多工作移至下半部。 - 下半部饥饿:软中断或 Tasklet 执行时间过长会导致 ksoftirqd 占用大量 CPU。检查
/proc/softirqs统计,观察 HI/SCHED/NET_TX/NET_RX 等计数是否异常增长。 - 线程化中断优先级:线程化中断运行在 SCHED_FIFO 优先级 50 的内核线程中。若系统中有更高优先级的实时线程,可能导致中断处理延迟。
- 共享中断中的下半部:当多个设备共享同一 IRQ 时,每个设备的上半部都会被调用。如果某个设备的上半部调度了耗时的下半部,会影响其他共享设备的中断响应。
排查命令: 使用
cat /proc/interrupts 查看每个 CPU 上的中断计数;使用 cat /proc/softirqs 查看软中断分布;使用 trace-cmd record -e irq:* -e softirq:* 追踪完整的中断处理流程。