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 架构图)
下图展示了传统中断模型下,网卡驱动从硬件中断触发到数据包进入协议栈的完整流程:
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 函数)在中断关闭的情况下轮询硬件描述符,批量取包。
- 当轮询结束后,重新使能中断,回到传统中断模式。
这种混合策略既保留了中断的低延迟响应,又通过轮询避免了高负载下的中断风暴,是当前主流网卡驱动的标准实现。