4、中断处理上半部:中断服务程序(ISR)的设计、如何触发NAPI调度(__napi_schedule())

在NAPI轮询机制中,中断服务程序(ISR)扮演着“触发器”的角色。它负责在硬件产生中断时,以最轻量的方式通知内核有数据到达,并立即调度NAPI轮询,从而将耗时的数据接收工作从中断上下文转移到软中断上下文中执行。本节将深入剖析ISR的设计原则、关键实现以及如何通过__napi_schedule()高效触发NAPI调度。

4.1 ISR的设计原则:快进快出

中断上下文是系统中最宝贵的执行环境之一,它不允许睡眠、不允许持有互斥锁、且执行时间必须极短。因此,NAPI驱动的ISR设计遵循“快进快出”的黄金法则:

  • 最小化操作:ISR只做必须立即完成的事,如读取中断状态寄存器、清除中断标志位、以及调用NAPI调度函数。
  • 避免数据拷贝:绝对不要在ISR中拷贝数据包。数据包的接收和协议栈递交应全部交给后续的NAPI轮询函数(poll)处理。
  • 禁止睡眠与阻塞:ISR中不能调用任何可能导致睡眠的函数(如kmalloc(GFP_KERNEL)、mutex_lock()等)。
  • 关闭中断(可选):在调度NAPI之前,通常需要关闭当前设备的中断,防止ISR被重入,直到NAPI轮询完成后再重新开启。
设计要点:ISR的核心职责是“通知”而非“处理”。将数据接收逻辑从ISR中剥离,是NAPI提升吞吐量的关键所在。

4.2 ISR的标准实现流程

一个典型的NAPI驱动ISR实现包含以下步骤:

  1. 读取中断状态寄存器:确认中断源是否属于本设备,并判断中断类型(如接收中断、发送完成中断、错误中断等)。
  2. 清除中断标志:向硬件写入特定值,清除已触发的中断标志位,防止同一中断被反复触发。
  3. 关闭设备中断:调用类似netif_stop_subqueue()或直接操作硬件寄存器,禁止设备继续产生新的接收中断。
  4. 调度NAPI:调用__napi_schedule()将设备的napi_struct结构体添加到当前CPU的软中断处理队列中。
  5. 返回IRQ_HANDLED:告知内核中断已被成功处理。
代码示例:NAPI驱动ISR骨架
static irqreturn_t my_napi_isr(int irq, void *dev_id)
{
    struct net_device *dev = dev_id;
    struct my_priv *priv = netdev_priv(dev);
    u32 status;

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

    // 2. 检查是否是本设备的中断
    if (!(status & INT_RX_EVENT))
        return IRQ_NONE;

    // 3. 清除中断标志位
    writel(INT_RX_EVENT, priv->ioaddr + INT_CLEAR_REG);

    // 4. 关闭接收中断(防止重入)
    netif_stop_subqueue(dev, 0);

    // 5. 调度NAPI轮询
    if (likely(napi_schedule_prep(&priv->napi))) {
        __napi_schedule(&priv->napi);
    }

    return IRQ_HANDLED;
}

4.3 触发NAPI调度的核心:__napi_schedule()

__napi_schedule()是连接中断上半部与软中断下半部的桥梁。它的核心作用是将设备的napi_struct结构体挂入当前CPU的softnet_data的poll_list链表中,并触发NET_RX_SOFTIRQ软中断。

其内部逻辑可概括为:

  • 获取当前CPU的softnet_data:每个CPU都有一个独立的softnet_data实例,用于管理该CPU上的NAPI轮询列表。
  • 将napi_struct加入poll_list:将设备的napi_struct通过其poll_list成员,添加到softnet_data->poll_list链表的尾部。
  • 设置NAPI调度标志:将napi_struct的state字段置为NAPI_STATE_SCHED,表示该NAPI已被调度,正在等待轮询。
  • 触发NET_RX_SOFTIRQ:调用__raise_softirq_irqoff(NET_RX_SOFTIRQ),唤醒软中断处理线程net_rx_action()。
注意:在调用__napi_schedule()之前,必须先调用napi_schedule_prep()进行原子检查。该函数会判断napi_struct是否已经被调度(NAPI_STATE_SCHED标志是否已置位),防止重复调度导致的数据竞争。

4.4 ISR与NAPI调度流程SVG图

下图展示了从硬件中断触发到NAPI轮询被调度的完整流程:

ISR与NAPI调度流程图 ISR与NAPI调度流程 硬件中断触发 ISR执行(上半部) 清除中断标志 & 关闭中断 __napi_schedule() NET_RX_SOFTIRQ 关键操作说明 • 读取中断状态寄存器 • 清除中断标志位 • 关闭设备接收中断 • 调用__napi_schedule()

4.5 ISR设计中的关键决策点

在实际驱动开发中,ISR的设计需要权衡多个因素。下表总结了不同场景下的设计选择:

决策点 选项 适用场景 注意事项
中断关闭时机 ISR中立即关闭 高吞吐量、中断频繁 减少中断重入,但可能增加延迟
中断关闭时机 延迟关闭(在poll中关闭) 低延迟、中断较少 允许中断重入,但需处理并发
中断类型处理 统一ISR处理所有中断 功能简单、中断源少 代码简洁,但可能引入不必要的处理
中断类型处理 分离ISR(RX/TX/ERR) 功能复杂、中断源多 提高处理效率,但增加代码复杂度
NAPI调度检查 使用napi_schedule_prep() 所有NAPI驱动 必须使用,防止重复调度

4.6 常见陷阱与最佳实践

  • 陷阱1:在ISR中调用napi_schedule()而非__napi_schedule()。前者会检查中断是否已禁用,在ISR中可能导致不必要的开销。ISR中应直接使用__napi_schedule()。
  • 陷阱2:忘记清除中断标志。如果中断标志未被清除,硬件会持续产生中断,导致系统陷入无限中断风暴。
  • 陷阱3:在ISR中执行耗时操作。如内存分配、数据拷贝、打印日志等,会严重增加中断延迟,降低系统实时性。
  • 最佳实践:使用napi_schedule_prep()进行原子检查。该函数返回true时再调用__napi_schedule(),确保调度操作的安全性。
  • 最佳实践:ISR中只操作硬件寄存器。所有与协议栈相关的操作都应推迟到NAPI的poll函数中执行。
性能指标参考:在10Gbps网络环境下,一个设计良好的NAPI驱动ISR执行时间应控制在1-2微秒以内。如果ISR执行时间超过5微秒,就需要考虑将更多逻辑迁移到NAPI轮询中。

4.7 ISR与NAPI调度架构SVG图

下图展示了ISR、NAPI调度与软中断处理之间的架构关系:

ISR与NAPI调度架构图 ISR与NAPI调度架构图 硬件层:网络设备(NIC) 中断上半部:ISR(中断服务程序) NAPI调度层:__napi_schedule() → 加入poll_list → 触发NET_RX_SOFTIRQ 软中断层:net_rx_action() → 轮询poll函数 → 接收数据包 说明:ISR只负责调度,数据接收全部在软中断的poll函数中完成,实现中断上下文的轻量化。