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 架构图)

下图展示了从硬件中断触发到上半部、再到下半部调度的完整流程:

中断处理流程:上半部与下半部 中断处理流程:上半部 (Top Half) 与下半部 (Bottom Half) 硬件中断触发 CPU 中断控制器 (APIC/GIC) 上半部 (Top Half) 保存状态 · 清除挂起 · 调度下半部 下半部调度决策:选择机制 软中断 (Softirq) 不可睡眠 · 多 CPU 并行 Tasklet 不可睡眠 · 同 CPU 串行 工作队列 (Workqueue) 可睡眠 · 内核线程 中断处理完成

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:* 追踪完整的中断处理流程。