3. 传统中断处理模型:上半部(Top Half)与下半部(Bottom Half)分工

在 Linux 内核的早期版本及非 NAPI 驱动中,中断处理被严格划分为两个阶段:上半部(Top Half)和下半部(Bottom Half)。这种分工的核心目的是在保证实时响应的前提下,尽量减少关中断的时间,从而提升系统的整体吞吐量。

3.1 为什么需要分工?

当网卡硬件接收到一个数据包时,它会通过中断线通知 CPU。CPU 在中断上下文中执行中断服务例程(ISR)。如果在 ISR 中完成所有工作(如拷贝数据、协议栈处理),会导致以下问题:

  • 中断延迟增加:长时间关中断会丢失后续中断,尤其是高速网卡。
  • 系统实时性下降:高优先级任务无法抢占中断上下文。
  • 嵌套中断风险:在复杂的中断控制器中,长时间处理容易引发死锁。

因此,传统模型将中断处理拆分为两个阶段:

核心原则:上半部只做最紧急、最轻量的硬件操作(如清除中断标志、拷贝寄存器),下半部处理耗时的数据搬运和协议栈递交。

3.2 上半部(Top Half)职责

上半部在硬件中断上下文中执行,默认关中断或仅开部分高优先级中断。其典型工作包括:

  • 读取中断状态寄存器,确认中断源。
  • 清除硬件中断挂起位,防止重复触发。
  • 将数据从硬件 FIFO 拷贝到内核缓冲区(sk_buff)。
  • 调度下半部(如触发 softirq、tasklet 或 workqueue)。
  • 返回 IRQ_HANDLED 或 IRQ_NONE。
// 典型的上半部处理框架
static irqreturn_t nic_interrupt(int irq, void *dev_id)
{
    struct net_device *dev = dev_id;
    struct nic_private *priv = netdev_priv(dev);
    u32 status;

    // 1. 读取中断状态寄存器
    status = readl(priv->ioaddr + INT_STATUS);

    // 2. 清除中断标志(硬件要求)
    writel(status, priv->ioaddr + INT_STATUS);

    // 3. 如果是接收中断
    if (status & RX_INT) {
        // 禁用接收中断(防止风暴)
        disable_rx_interrupt(priv);
        // 调度下半部(softirq 方式)
        __napi_schedule(&priv->napi);
    }

    // 4. 如果是发送完成中断
    if (status & TX_INT) {
        // 唤醒发送队列
        netif_wake_queue(dev);
    }

    return IRQ_HANDLED;
}

3.3 下半部(Bottom Half)职责

下半部在软中断(softirq)或 tasklet 上下文中执行,此时中断已重新使能。其典型工作包括:

  • 从硬件描述符环中批量取出接收到的数据包。
  • 构造 sk_buff 结构,填充协议头信息。
  • 调用 netif_receive_skb() 将数据送入协议栈。
  • 重新使能接收中断(或保持轮询模式)。
  • 处理发送完成回收、统计更新等。
// 典型的下半部处理框架(NAPI poll 函数)
static int nic_poll(struct napi_struct *napi, int budget)
{
    struct nic_private *priv = container_of(napi, struct nic_private, napi);
    int work_done = 0;

    // 循环处理接收描述符
    while (work_done < budget) {
        struct sk_buff *skb = nic_rx_one_packet(priv);
        if (!skb)
            break;
        netif_receive_skb(skb);
        work_done++;
    }

    // 如果处理完所有包,重新使能中断并退出 NAPI
    if (work_done < budget) {
        napi_complete_done(napi, work_done);
        enable_rx_interrupt(priv);
    }

    return work_done;
}

3.4 上半部与下半部的对比

对比维度 上半部(Top Half) 下半部(Bottom Half)
执行上下文 硬件中断上下文 软中断 / tasklet / workqueue
中断状态 默认关中断(或仅开紧急中断) 中断已使能,可被抢占
执行时间要求 极短(微秒级),必须快速返回 相对宽松(毫秒级),可处理批量数据
典型操作 读状态、清标志、调度下半部 拷贝数据、协议栈递交、统计更新
可睡眠 否(禁止睡眠、禁止信号量) 视机制而定(workqueue 可睡眠)
调度方式 硬件触发,自动执行 由上半部显式调度(如 napi_schedule)

3.5 传统中断处理流程(SVG 架构图)

下图展示了传统中断模型下,网卡驱动从硬件中断触发到数据包进入协议栈的完整流程:

传统中断处理模型:上半部与下半部分工流程 网卡硬件 硬件中断信号 上半部(Top Half) 读状态寄存器 · 清中断标志 · 调度下半部 触发 softirq / tasklet 下半部(Bottom Half) 批量取包 · 构造 sk_buff · 递交协议栈 netif_receive_skb() 内核协议栈(IP/TCP/UDP) ⚠ 关键约束: 上半部必须极短(通常 < 10μs),否则会阻塞其他中断;下半部可批量处理,但需注意 budget 限制。

3.6 传统模型的优缺点

适用场景:传统中断模型适用于低带宽(< 100Mbps)或中断频率可控的网卡。对于千兆及以上网卡,频繁的中断切换会导致 CPU 开销过高,此时 NAPI 模型更具优势。
  • 优点:
    • 实现简单,逻辑清晰,易于调试。
    • 响应延迟低,适合对实时性要求高的场景。
    • 兼容性好,所有 Linux 内核版本均支持。
  • 缺点:
    • 高吞吐下中断风暴严重,CPU 被频繁打断。
    • 每个数据包都触发一次中断,缓存利用率低。
    • 上半部关中断时间累积后影响整体性能。

3.7 下半部实现机制对比

Linux 内核提供了多种下半部实现机制,驱动开发者可根据需求选择:

机制 上下文 可睡眠 并发性 典型用途
softirq 软中断 否 同类型可多 CPU 并行 NAPI poll、网络 RX/TX
tasklet 软中断 否 同类型串行执行 旧版驱动、非性能关键路径
workqueue 进程上下文 是 可并行,可阻塞 耗时的硬件配置、链路检测
threaded IRQ 内核线程 是 专用线程,可设置优先级 实时系统、复杂外设驱动
注意:在网卡驱动中,NAPI 的 poll 函数本质上是 softirq 机制的一种封装。如果驱动不使用 NAPI,则通常使用 tasklet 或 workqueue 作为下半部。对于新开发的驱动,强烈建议使用 NAPI 框架。

3.8 传统模型与 NAPI 的衔接

传统中断模型是理解 NAPI 的基础。NAPI 并没有完全抛弃上半部/下半部分工,而是将下半部改造为轮询模式:

  • 上半部仍然负责清除中断标志和调度 NAPI。
  • 下半部(poll 函数)在中断关闭的情况下轮询硬件描述符,批量取包。
  • 当轮询结束后,重新使能中断,回到传统中断模式。

这种混合策略既保留了中断的低延迟响应,又通过轮询避免了高负载下的中断风暴,是当前主流网卡驱动的标准实现。