4. 中断处理与NAPI调度:网卡中断处理函数如何调用napi_schedule()、__napi_schedule()的核心逻辑

当网卡接收到数据帧并通过DMA写入内存后,硬件会触发中断信号通知CPU。中断处理函数(ISR)是NAPI收包流程的入口点,其核心职责是最小化中断上下文的工作量,并快速将数据接收任务调度到软中断上下文中执行。本章深入剖析从硬件中断触发到napi_schedule()、__napi_schedule()被调用的完整路径。

4.1 中断处理函数的两阶段设计

现代Linux内核将中断处理拆分为两个阶段,以平衡实时性与吞吐量:

阶段 上下文 核心操作 耗时要求
上半部(Top Half) 硬中断上下文 禁止同设备中断、调度NAPI、记录统计 微秒级,必须极短
下半部(Bottom Half) 软中断上下文(ksoftirqd) 轮询收包、协议处理、重新使能中断 毫秒级,可被抢占
性能要点:上半部只做“通知”不做“收包”,这是NAPI相比传统中断收包模式的核心优化——避免在中断上下文中逐包拷贝,从而降低中断延迟抖动。

4.2 典型网卡驱动中断处理函数流程

以Intel ixgbe驱动为例,中断处理函数ixgbe_msix_clean_rings()的执行路径如下:

  1. 读取中断状态寄存器:确认中断来源,清除中断挂起位。
  2. 获取NAPI结构体:通过napi_struct实例关联到具体队列。
  3. 调用napi_schedule():将NAPI实例挂入CPU的poll_list,并触发软中断NET_RX_SOFTIRQ。
  4. 返回IRQ_HANDLED:中断处理结束。
// drivers/net/ethernet/intel/ixgbe/ixgbe_main.c (简化示意)
static irqreturn_t ixgbe_msix_clean_rings(int irq, void *data)
{
    struct ixgbe_q_vector *q_vector = data;
    struct napi_struct *napi = &q_vector->napi;

    /* 1. 读取并清除中断状态寄存器 */
    u32 eicr = ixgbe_read_reg(hw, IXGBE_EICR);
    if (!eicr)
        return IRQ_NONE;

    /* 2. 禁止当前队列中断(硬件层面) */
    ixgbe_irq_disable_queues(hw, q_vector->v_idx);

    /* 3. 调度NAPI */
    if (napi_schedule(napi)) {
        /* 记录统计 */
        q_vector->tx_ring->total_packets++;
    }

    return IRQ_HANDLED;
}

4.3 napi_schedule() 与 __napi_schedule() 的核心逻辑

napi_schedule()是一个内联函数,它首先检查NAPI实例是否已经被调度,避免重复入队。其核心逻辑封装在__napi_schedule()中。

4.3.1 napi_schedule() 的入口检查

// include/linux/netdevice.h
static inline bool napi_schedule(struct napi_struct *n)
{
    /* 检查NAPI是否已启用,且未被调度 */
    return napi_schedule_prep(n) && __napi_schedule(n);
}

napi_schedule_prep()执行两个关键检查:

  • NAPI_STATE_SCHED标志位:如果该位已置位,说明NAPI实例已经在poll_list中等待处理,直接返回false。
  • NAPI_STATE_DISABLE标志位:如果NAPI被禁用(如设备关闭),返回false。

4.3.2 __napi_schedule() 的入队与软中断触发

当检查通过后,__napi_schedule()执行以下原子操作:

// net/core/dev.c
bool __napi_schedule(struct napi_struct *n)
{
    unsigned long flags;

    /* 获取当前CPU的softnet_data */
    struct softnet_data *sd = &__get_cpu_var(softnet_data);

    /* 关本地中断,保护链表操作 */
    local_irq_save(flags);

    /* 将NAPI实例加入当前CPU的poll_list尾部 */
    list_add_tail(&n->poll_list, &sd->poll_list);

    /* 置位NAPI_STATE_SCHED标志 */
    napi_schedule_set_sched(n);

    /* 触发NET_RX_SOFTIRQ软中断 */
    ____napi_schedule(sd, n);

    local_irq_restore(flags);

    return true;
}
关键设计:NAPI实例被加入当前CPU的poll_list,这意味着中断亲和性(IRQ affinity)决定了哪个CPU执行收包处理。合理设置中断亲和性可以避免CPU间的缓存颠簸。

4.4 核心数据结构:softnet_data 与 poll_list

每个CPU维护一个softnet_data结构体,它是NAPI调度的核心数据容器:

字段 类型 作用
poll_list struct list_head 挂载所有待处理的NAPI实例链表
backlog_dev struct napi_struct 传统非NAPI设备的回退NAPI实例
processed unsigned int 本轮软中断处理的包数统计
time_squeeze unsigned int 因时间配额耗尽而提前退出的次数
图4-1:中断处理函数调用napi_schedule()的完整流程 硬件中断触发 中断处理函数(ISR) napi_schedule_prep() 检查NAPI_STATE_SCHED 已调度 → 返回false __napi_schedule() 入队到poll_list 触发NET_RX_SOFTIRQ 说明:整个流程在硬中断上下文中执行,耗时必须控制在微秒级;实际收包在后续软中断中完成

4.5 中断屏蔽与重新使能的时机

NAPI的核心思想是中断关闭 + 轮询收包。中断处理函数中关闭中断的时机至关重要:

  • 关闭中断:在调用napi_schedule()之前,驱动必须确保硬件不会再产生新的中断,避免中断风暴。
  • 重新使能中断:在NAPI的poll()回调中,当收包完成(或达到预算)后,驱动调用napi_complete(),并在其中重新使能硬件中断。
常见性能陷阱:如果中断处理函数中忘记关闭硬件中断,或者napi_complete()中忘记重新使能,会导致以下问题:
• 中断未关闭 → 高负载下产生中断风暴,CPU 100% 忙于处理中断
• 中断未重新使能 → 设备停止接收新数据,造成丢包

4.6 多队列网卡的中断分发策略

现代网卡支持多队列(RSS/Flow Director),每个队列有独立的中断向量。中断处理函数与NAPI实例的映射关系如下:

队列编号 中断号 NAPI实例 绑定的CPU
Queue 0 IRQ 78 napi_0 CPU 0
Queue 1 IRQ 79 napi_1 CPU 1
Queue 2 IRQ 80 napi_2 CPU 2
Queue 3 IRQ 81 napi_3 CPU 3

每个队列的中断处理函数只操作自己的NAPI实例,实现无锁并行收包。这是多核扩展性的关键设计。

4.7 性能优化检查清单

排查中断与NAPI调度问题的常用工具:
• /proc/interrupts:查看各CPU的中断分布是否均衡
• perf top -G:观察napi_schedule和__napi_schedule的调用频率
• ethtool -S eth0:查看网卡驱动统计中的中断次数与NAPI轮询次数
• cat /sys/class/net/eth0/queues/rx-*/rps_cpus:检查RPS(Receive Packet Steering)配置

通过理解中断处理函数到__napi_schedule()的完整路径,开发者可以精准定位收包流程中的瓶颈——无论是中断亲和性设置不当、NAPI预算配置不合理,还是驱动层中断屏蔽逻辑错误,都能从本章的知识体系中找到分析依据。