4. 中断处理与NAPI调度:中断上半部调用napi_schedule()的完整路径、__napi_schedule()内部机制

当网络设备产生中断时,中断处理程序(ISR)的首要任务不是立即处理所有数据包,而是通过NAPI机制将数据接收任务调度到软中断上下文中执行。本节将深入剖析从硬件中断触发到napi_schedule()被调用,再到__napi_schedule()内部核心机制的完整路径。

4.1 中断上半部到napi_schedule()的调用路径

以Intel igb网卡驱动为例,中断处理函数igb_msix_ring()的典型调用路径如下:

  1. 硬件中断触发:网卡完成数据包DMA传输后,通过MSI-X中断通知CPU。
  2. 中断向量分发:CPU中断控制器将中断路由到注册的处理函数。
  3. 驱动ISR执行:igb_msix_ring()读取中断状态寄存器,确认中断来源。
  4. 关闭中断并调度NAPI:调用napi_schedule()将对应napi_struct加入调度队列。
  5. 返回IRQ_HANDLED:上半部结束,数据接收工作交由软中断NET_RX_SOFTIRQ处理。
关键设计原则:中断上半部必须极短,只做“标记工作”而非“完成工作”。NAPI机制通过napi_schedule()实现了中断屏蔽与轮询的平滑切换。

4.2 napi_schedule() 函数调用链

napi_schedule()是一个内联函数,其调用链清晰展示了从驱动到核心NAPI子系统的过渡:

// 驱动调用入口
napi_schedule(napi)
  └─> __napi_schedule(napi)        // 核心调度函数
       └─> ____napi_schedule(this_cpu_ptr(&softnet_data), napi)
            ├─> list_add_tail(&napi->poll_list, &sd->poll_list)  // 加入轮询列表
            ├─> napi->state |= NAPI_STATE_SCHED                   // 设置调度标志
            └─> __raise_softirq_irqoff(NET_RX_SOFTIRQ)            // 触发软中断
性能要点:napi_schedule()在中断上下文中执行,必须使用____napi_schedule()的irqoff变体以避免额外的中断状态保存开销。

4.3 __napi_schedule() 内部机制详解

__napi_schedule()是NAPI调度的核心枢纽,其内部实现包含三个关键步骤:

步骤 操作 数据结构 原子性保证
1. 状态检查与设置 检查NAPI_STATE_SCHED标志,若未设置则原子置位 napi->state (unsigned long) 使用test_and_set_bit()保证原子性
2. 加入轮询列表 将napi_struct追加到当前CPU的softnet_data->poll_list尾部 struct list_head poll_list 在关闭本地中断的上下文中操作
3. 触发软中断 调用__raise_softirq_irqoff(NET_RX_SOFTIRQ) 软中断向量表 irqoff变体,无需额外锁
// 内核源码:net/core/dev.c
static inline void ____napi_schedule(struct softnet_data *sd,
                                     struct napi_struct *napi)
{
    // 1. 设置调度标志,防止重复调度
    if (!test_and_set_bit(NAPI_STATE_SCHED, &napi->state)) {
        // 2. 将napi加入当前CPU的轮询列表
        list_add_tail(&napi->poll_list, &sd->poll_list);
        // 3. 触发NET_RX_SOFTIRQ软中断
        __raise_softirq_irqoff(NET_RX_SOFTIRQ);
    }
}
竞态条件防护:test_and_set_bit()是防止同一NAPI实例被多次调度的关键屏障。若没有此检查,多个中断可能同时将同一napi_struct加入多个CPU的轮询列表,导致数据包乱序或重复处理。

4.4 NAPI调度状态机与软中断触发

下图展示了从中断触发到NAPI调度,再到软中断执行的完整状态转换:

NAPI调度状态机与软中断触发流程 硬件中断触发 MSI-X/INTx 中断上半部 ISR 读取状态寄存器 napi_schedule() 调度入口 中断向量 关闭中断 __napi_schedule() 内部机制展开 ① 状态检查与置位 test_and_set_bit() ② 加入轮询列表 list_add_tail() ③ 触发软中断 __raise_softirq_irqoff() 软中断 NET_RX_SOFTIRQ net_rx_action() 轮询处理 软中断触发

4.5 关键数据结构:softnet_data 与 napi_struct 的交互

____napi_schedule()的核心操作围绕两个数据结构展开:

  • struct softnet_data (per-CPU):每个CPU维护一个softnet_data实例,包含poll_list链表头,用于挂载所有待调度的NAPI实例。
  • struct napi_struct:代表一个NAPI轮询实体,包含poll_list节点、state标志位、poll回调函数指针等。
// 关键数据结构定义(简化)
struct softnet_data {
    struct list_head   poll_list;        // NAPI轮询链表
    struct napi_struct *napi_list;       // 注册的NAPI实例链表
    unsigned int       processed;        // 已处理数据包计数
    // ... 其他字段
};

struct napi_struct {
    struct list_head   poll_list;        // 用于挂入softnet_data->poll_list
    unsigned long      state;            // NAPI_STATE_SCHED 等标志位
    int                weight;           // 单次轮询预算
    int                (*poll)(struct napi_struct *, int); // 轮询回调
    // ... 其他字段
};

4.6 中断关闭与NAPI调度的时序关系

在调用napi_schedule()之前,驱动ISR通常会关闭当前队列的中断,这是NAPI“中断抑制”策略的关键:

时序点 操作 中断状态 目的
T0 硬件中断到达 开启 通知CPU有新数据
T1 ISR读取状态寄存器 开启 确认中断来源
T2 关闭该队列中断 关闭 防止中断风暴,进入轮询模式
T3 调用napi_schedule() 关闭 调度NAPI到软中断
T4 ISR返回 关闭 上半部结束
性能调优关键:中断关闭与NAPI调度的原子性至关重要。如果在关闭中断和调用napi_schedule()之间发生新的中断,可能导致调度丢失。因此,驱动通常使用napi_schedule_irqoff()变体,确保在关闭本地中断的上下文中完成调度。

4.7 多队列场景下的调度行为

在多队列网卡(RSS)场景中,每个队列拥有独立的napi_struct实例,中断处理路径如下:

  • 队列0中断 → napi_schedule(napi_0) → 加入CPU0的poll_list
  • 队列1中断 → napi_schedule(napi_1) → 加入CPU1的poll_list
  • 队列2中断 → napi_schedule(napi_2) → 加入CPU2的poll_list

每个NAPI实例独立调度,互不干扰,实现了数据包处理的并行化。但需注意,若多个队列的中断被分配到同一CPU,则该CPU的poll_list会包含多个NAPI实例,软中断处理时需依次轮询。

调优建议:通过/proc/irq/<irq_num>/smp_affinity合理分布中断亲和性,避免多个高吞吐队列的中断集中在同一CPU,导致软中断处理成为瓶颈。