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 架构图
下图展示了从硬件中断到软中断触发的完整数据流:
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 调度是否正常工作。若该值持续增长但网络吞吐量低,说明轮询效率可能存在问题。