4. 中断处理与NAPI调度:中断上半部调用napi_schedule()的完整路径、__napi_schedule()内部机制
当网络设备产生中断时,中断处理程序(ISR)的首要任务不是立即处理所有数据包,而是通过NAPI机制将数据接收任务调度到软中断上下文中执行。本节将深入剖析从硬件中断触发到napi_schedule()被调用,再到__napi_schedule()内部核心机制的完整路径。
4.1 中断上半部到napi_schedule()的调用路径
以Intel igb网卡驱动为例,中断处理函数igb_msix_ring()的典型调用路径如下:
- 硬件中断触发:网卡完成数据包DMA传输后,通过MSI-X中断通知CPU。
- 中断向量分发:CPU中断控制器将中断路由到注册的处理函数。
- 驱动ISR执行:
igb_msix_ring()读取中断状态寄存器,确认中断来源。 - 关闭中断并调度NAPI:调用
napi_schedule()将对应napi_struct加入调度队列。 - 返回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调度,再到软中断执行的完整状态转换:
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,导致软中断处理成为瓶颈。