4. NAPI 的调度入口:中断处理函数中调用 napi_schedule() 的完整流程

NAPI 的核心思想是将中断驱动的收包模式转变为“中断+轮询”混合模式。当网卡收到数据包时,中断处理函数不再直接读取数据,而是通过 napi_schedule() 将 NAPI 实例挂入软中断调度队列,并触发 NET_RX_SOFTIRQ 软中断。本节将深入剖析这一入口流程的完整链路。

4.1 从硬件中断到 napi_schedule() 的调用路径

以典型网卡驱动(如 Intel igb)为例,中断处理函数的结构如下:

static irqreturn_t igb_msix_ring(int irq, void *data)
{
    struct igb_q_vector *q_vector = data;

    /* 1. 写网卡寄存器清除中断状态,避免重复触发 */
    writel(IGB_EICR_RX_QUEUE0, hw->hw_addr + EICR);

    /* 2. 关闭该队列的中断产生 */
    napi_disable(&q_vector->napi);

    /* 3. 调度 NAPI 实例进入软中断处理 */
    napi_schedule(&q_vector->napi);

    return IRQ_HANDLED;
}

关键步骤说明:

  • 清除中断状态:必须优先操作,否则网卡会持续产生中断,导致 CPU 陷入中断风暴。
  • 关闭中断:通过 napi_disable() 或直接操作寄存器,确保在轮询完成前不再接收新中断。
  • 调度 NAPI:将 NAPI 实例加入软中断处理队列,由 net_rx_action() 在软中断上下文中执行轮询。
设计要点:中断处理函数应尽可能短,只做“最小必要操作”——清除中断、关闭中断、调度 NAPI。数据包的读取和协议栈递交全部交给软中断轮询阶段完成。

4.2 napi_schedule() 内部实现解析

napi_schedule() 是一个内联函数,定义在 <linux/netdevice.h> 中。其核心逻辑是检查 NAPI 实例是否已被调度,若未调度则将其加入当前 CPU 的 softnet_data 的轮询列表,并触发软中断。

static inline void napi_schedule(struct napi_struct *n)
{
    if (napi_schedule_prep(n))
        __napi_schedule(n);
}

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

  • 状态位检查:通过 test_and_set_bit(NAPI_STATE_SCHED, &n->state) 原子设置调度标志,防止重复调度。
  • 禁用状态检查:若 NAPI 实例处于 NAPI_STATE_DISABLE 状态,则返回 false,拒绝调度。
原子操作的重要性:test_and_set_bit 保证了在多核环境下,同一个 NAPI 实例不会被多个 CPU 同时调度,避免了竞态条件。

4.3 __napi_schedule() 的核心实现

当 napi_schedule_prep() 返回 true 后,__napi_schedule() 执行实际的入队和软中断触发操作:

void __napi_schedule(struct napi_struct *n)
{
    unsigned long flags;
    struct softnet_data *sd;

    /* 获取当前 CPU 的 softnet_data 结构 */
    local_irq_save(flags);
    sd = this_cpu_ptr(&softnet_data);

    /* 将 NAPI 实例加入 poll_list 链表尾部 */
    list_add_tail(&n->poll_list, &sd->poll_list);

    /* 触发 NET_RX_SOFTIRQ 软中断 */
    __raise_softirq_irqoff(NET_RX_SOFTIRQ);

    local_irq_restore(flags);
}

流程拆解:

步骤 操作 说明
1 local_irq_save(flags) 保存当前中断状态并关闭本地中断,防止在操作链表时被中断打断
2 this_cpu_ptr(&softnet_data) 获取当前 CPU 的 softnet_data 结构,每个 CPU 拥有独立的轮询队列
3 list_add_tail() 将 NAPI 实例挂入 poll_list 尾部,保证公平轮询顺序
4 __raise_softirq_irqoff() 设置 NET_RX_SOFTIRQ 对应的软中断标志位,等待 do_softirq() 处理
5 local_irq_restore(flags) 恢复之前的中断状态
关键约束:__napi_schedule() 必须在中断上下文或关闭本地中断的上下文中调用。这是因为 list_add_tail 操作需要保护,且 __raise_softirq_irqoff 要求中断已关闭。

4.4 NET_RX_SOFTIRQ 的触发机制

__raise_softirq_irqoff(NET_RX_SOFTIRQ) 的作用是设置当前 CPU 的软中断 pending 位图中的 NET_RX_SOFTIRQ 位。软中断的实际执行发生在以下时机:

  • 中断返回路径:irq_exit() 中调用 invoke_softirq(),检查 pending 位图并执行待处理的软中断。
  • ksoftirqd 内核线程:当软中断处理时间过长或嵌套次数过多时,由 ksoftirqd 线程在进程上下文中继续处理。

软中断处理函数通过 open_softirq() 注册:

open_softirq(NET_RX_SOFTIRQ, net_rx_action);

net_rx_action() 是 NAPI 轮询的入口函数,它遍历当前 CPU 的 poll_list,依次调用每个 NAPI 实例的 poll 回调函数读取数据包。

4.5 完整调度流程 SVG 架构图

下图展示了从硬件中断到软中断触发的完整数据流:

NAPI 调度入口完整流程 硬件中断触发 中断处理函数 (igb_msix_ring) napi_schedule() napi_schedule_prep() 失败(已调度或禁用) __napi_schedule() NET_RX_SOFTIRQ 软中断触发 关键操作: 1. 清除中断状态寄存器 2. 关闭该队列中断 3. 调用 napi_schedule() __napi_schedule() 内部: 1. 获取当前 CPU softnet_data 2. 将 napi 加入 poll_list 尾部 3. __raise_softirq_irqoff()

4.6 调度流程中的关键数据结构

理解 NAPI 调度需要掌握以下核心数据结构:

数据结构 作用域 关键字段 说明
napi_struct 每个 NAPI 实例 state, poll_list, poll 表示一个 NAPI 轮询实体,包含状态标志、链表节点和轮询回调
softnet_data 每个 CPU poll_list, backlog_dev 管理当前 CPU 上所有待轮询的 NAPI 实例,是软中断处理的核心数据
irq_cpustat_t 每个 CPU __softirq_pending 记录待处理的软中断位图,NET_RX_SOFTIRQ 对应第 0 位

4.7 常见问题与避坑指南

中断风暴陷阱:如果在中断处理函数中忘记清除中断状态寄存器,网卡会持续产生中断,导致 CPU 100% 占用在中断上下文中,系统完全失去响应。务必在调度 NAPI 之前完成中断清除操作。
调试技巧:可以通过 /proc/softirqs 查看 NET_RX_SOFTIRQ 的触发次数,判断 NAPI 调度是否正常工作。若该值持续增长但网络吞吐量低,说明轮询效率可能存在问题。