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()的执行路径如下:
- 读取中断状态寄存器:确认中断来源,清除中断挂起位。
- 获取NAPI结构体:通过
napi_struct实例关联到具体队列。 - 调用
napi_schedule():将NAPI实例挂入CPU的poll_list,并触发软中断NET_RX_SOFTIRQ。 - 返回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.5 中断屏蔽与重新使能的时机
NAPI的核心思想是中断关闭 + 轮询收包。中断处理函数中关闭中断的时机至关重要:
- 关闭中断:在调用
napi_schedule()之前,驱动必须确保硬件不会再产生新的中断,避免中断风暴。 - 重新使能中断:在NAPI的
poll()回调中,当收包完成(或达到预算)后,驱动调用napi_complete(),并在其中重新使能硬件中断。
常见性能陷阱:如果中断处理函数中忘记关闭硬件中断,或者
• 中断未关闭 → 高负载下产生中断风暴,CPU 100% 忙于处理中断
• 中断未重新使能 → 设备停止接收新数据,造成丢包
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预算配置不合理,还是驱动层中断屏蔽逻辑错误,都能从本章的知识体系中找到分析依据。