第3篇:驱动集成 — igb 与 NAPI 全程¶
源码:drivers/net/ethernet/intel/igb/igb_main.c net/core/dev.c | 头文件:include/linux/netdevice.h
系列目录:NAPI 内核源码深度解析
1. 概述¶
前两篇分别分析了 NAPI 核心调度和 GRO 合并引擎。本文以 Intel igb 驱动(e1000 系列千兆网卡)为实例,完整走读驱动如何将硬件收包、中断处理、NAPI 调度、GRO 合并集成到一起。
igb 是 Linux 中最成熟的网卡驱动之一,同时支持 MSI-X(多队列)、INTx(传统中断)、ITR(中断调节)、XDP、AF_XDP 等高级特性。选择 igb 作为示例,因为它包含了一个 NAPI 驱动应有的所有典型模式。
2. igb 驱动架构概览¶
2.1 核心数据结构关系¶
struct igb_adapter 顶层适配器
├── struct net_device *netdev
├── struct igb_q_vector *q_vector[N] 每个中断向量一个
├── int num_q_vectors
├── int num_rx_queues / num_tx_queues
├── struct e1000_hw hw
└── ...
struct igb_q_vector 中断向量
├── struct napi_struct napi 内嵌 NAPI 上下文
├── struct igb_ring rx / tx 收发 Ring
├── struct igb_adapter *adapter
├── int cpu
└── ...
struct igb_ring 收发 Ring
├── void *desc DMA 描述符区域
├── struct igb_tx_buffer / rx_buffer *buffer_info
├── u16 next_to_use / next_to_clean
└── ...
每个 igb_q_vector 对应一个中断向量和一个 NAPI 上下文。在多队列模式下,每个 MSI-X 向量处理一个收发队列对。
2.2 中断模式支持¶
| 模式 | 中断函数 | 行号 | NAPI 调度方式 |
|---|---|---|---|
| MSI-X (多队列) | igb_msix_ring |
igb_main.c:7153 | 每个队列独立 napi_schedule |
| MSI (单向量) | igb_intr_msi |
igb_main.c:8174 | 单次 napi_schedule |
| INTx (传统线中断) | igb_intr |
igb_main.c:8211 | 单次 napi_schedule |
3. NAPI 注册¶
3.1 netif_napi_add_weight_locked — NAPI 初始化 (dev.c:7558)¶
驱动初始化时(通常在 probe 中),通过此函数注册 NAPI:
// net/core/dev.c:7558
void netif_napi_add_weight_locked(struct net_device *dev,
struct napi_struct *napi,
int (*poll)(struct napi_struct *, int),
int weight)
{
netdev_assert_locked(dev);
if (WARN_ON(test_and_set_bit(NAPI_STATE_LISTED, &napi->state)))
return;
INIT_LIST_HEAD(&napi->poll_list);
INIT_HLIST_NODE(&napi->napi_hash_node);
hrtimer_setup(&napi->timer, napi_watchdog,
CLOCK_MONOTONIC, HRTIMER_MODE_REL_PINNED);
gro_init(&napi->gro); // 初始化 GRO 节点(哈希表和 bitmask)
napi->skb = NULL;
napi->poll = poll; // 保存驱动 poll 函数指针
if (weight > NAPI_POLL_WEIGHT)
netdev_err_once(dev, "%s() called with weight %d\n",
__func__, weight);
napi->weight = weight; // igb 使用 64
napi->dev = dev;
napi->list_owner = -1;
set_bit(NAPI_STATE_SCHED, &napi->state); // 注册期间的临时状态
set_bit(NAPI_STATE_NPSVC, &napi->state); // 防止 netpoll 干扰
netif_napi_dev_list_add(dev, napi);
// 添加到 NAPI hash 表(用于 busy poll 查找)
// ...
}
igb 的调用(在 igb_alloc_q_vector 中,igb_main.c:1200):
igb 的 weight=64,这是驱动自己决定的——表示每次 poll 最多处理 64 个包(实际包含 TX clean + RX 收包)。
4. 中断处理路径¶
4.1 MSI-X 路径:igb_msix_ring (igb_main.c:7153)¶
// drivers/net/ethernet/intel/igb/igb_main.c:7153
static irqreturn_t igb_msix_ring(int irq, void *data)
{
struct igb_q_vector *q_vector = data;
// 写入 ITR (Interrupt Throttle Rate),为下次中断设置时间间隔
igb_write_itr(q_vector);
// 关键调用:调度 NAPI
napi_schedule(&q_vector->napi);
return IRQ_HANDLED;
}
这是最简洁的中断处理——只有两步: 1. ITR 更新:根据当前负载动态调整中断频率 2. napi_schedule:触发软中断处理
为什么不需要显式 mask 中断? igb 硬件支持 Auto-Mask 特性:读取 ICR(中断原因寄存器)时硬件自动屏蔽中断。对于 MSI-X,由于每个队列有独立的中断向量,设计上只需要依赖此硬件特性。
4.2 INTx 路径:igb_intr (igb_main.c:8211)¶
// drivers/net/ethernet/intel/igb/igb_main.c:8211
static irqreturn_t igb_intr(int irq, void *data)
{
struct igb_adapter *adapter = data;
struct igb_q_vector *q_vector = adapter->q_vector[0];
struct e1000_hw *hw = &adapter->hw;
u32 icr = rd32(E1000_ICR); // 读取 ICR(硬件自动 mask)
if (!(icr & E1000_ICR_INT_ASSERTED))
return IRQ_NONE; // 不是我们的中断
igb_write_itr(q_vector); // 更新 ITR
// 处理各种 interrupt cause
if (icr & E1000_ICR_DRSTA)
schedule_work(&adapter->reset_task);
if (icr & E1000_ICR_DOUTSYNC)
adapter->stats.doosync++;
if (icr & (E1000_ICR_RXSEQ | E1000_ICR_LSC)) {
hw->mac.get_link_status = 1;
if (!test_bit(__IGB_DOWN, &adapter->state))
mod_timer(&adapter->watchdog_timer, jiffies + 1);
}
if (icr & E1000_ICR_TS)
igb_tsync_interrupt(adapter);
// 调度 NAPI
napi_schedule(&q_vector->napi);
return IRQ_HANDLED;
}
INTx 模式下只有 1 个中断向量,因此需要读取 ICR 来判断中断原因:
- E1000_ICR_INT_ASSERTED:确认中断来自本设备(共享中断线判断)
- E1000_ICR_RXSEQ / LSC:接收序列错误或链路状态变化
- E1000_ICR_TS:时间戳中断(PTP)
4.3 中断屏蔽与恢复模式¶
┌──────────────────────────────────────────────────────────────┐
│ igb 中断屏蔽/恢复时序 │
├──────────────────────────────────────────────────────────────┤
│ │
│ IRQ 到达 (MSI-X) │
│ │ │
│ │ [硬件] 读取 ICR → Auto-Mask (中断被屏蔽) │
│ │ │
│ ▼ │
│ igb_msix_ring() [igb_main.c:7153] │
│ ├── igb_write_itr() 更新中断调节 │
│ └── napi_schedule() 调度 NAPI │
│ │ │
│ ▼ │
│ NET_RX_SOFTIRQ → net_rx_action() │
│ └── igb_poll() [igb_main.c:8278] │
│ ├── igb_clean_tx_irq() 清理 TX 完成 │
│ ├── igb_clean_rx_irq() 收包 + GRO │
│ └── napi_complete_done() │
│ └── igb_ring_irq_enable() [igb_main.c:8252] │
│ 重新启用该队列的中断 │
│ │
└──────────────────────────────────────────────────────────────┘
4.4 igb_ring_irq_enable — 重新启用中断 (igb_main.c:8252)¶
// drivers/net/ethernet/intel/igb/igb_main.c:8252
static void igb_ring_irq_enable(struct igb_q_vector *q_vector)
{
struct igb_adapter *adapter = q_vector->adapter;
struct e1000_hw *hw = &adapter->hw;
if ((q_vector->rx.ring && (adapter->rx_itr_setting & 3)) ||
(!q_vector->rx.ring && (adapter->tx_itr_setting & 3))) {
if ((adapter->num_q_vectors == 1) && !adapter->vf_data)
igb_set_itr(q_vector);
else
igb_update_ring_itr(q_vector);
}
if (!test_bit(__IGB_DOWN, &adapter->state)) {
if (adapter->flags & IGB_FLAG_HAS_MSIX) {
// MSI-X: 写入 EIMS (Extended Interrupt Mask Set/ Clear)
wr32(E1000_EIMS, q_vector->eims_value);
}
// ...
}
}
关键点:只有在 napi_complete_done() 返回 true 后才会调用此函数(见 igb_poll 返回值逻辑)。
5. Poll 函数:igb_poll¶
5.1 igb_poll — NAPI 轮询入口 (igb_main.c:8278)¶
// drivers/net/ethernet/intel/igb/igb_main.c:8278
static int igb_poll(struct napi_struct *napi, int budget)
{
struct igb_q_vector *q_vector =
container_of(napi, struct igb_q_vector, napi);
struct xsk_buff_pool *xsk_pool;
bool clean_complete = true;
int work_done = 0;
#ifdef CONFIG_IGB_DCA
if (q_vector->adapter->flags & IGB_FLAG_DCA_ENABLED)
igb_update_dca(q_vector); // Direct Cache Access 更新
#endif
// Step 1: TX 清理(回收已发送的包)
if (q_vector->tx.ring)
clean_complete = igb_clean_tx_irq(q_vector, budget);
// Step 2: RX 收包
if (q_vector->rx.ring) {
int cleaned;
xsk_pool = READ_ONCE(q_vector->rx.ring->xsk_pool);
cleaned = xsk_pool ?
igb_clean_rx_irq_zc(q_vector, xsk_pool, budget) :
igb_clean_rx_irq(q_vector, budget);
work_done += cleaned;
if (cleaned >= budget)
clean_complete = false;
}
// Step 3: 决定是否完成
if (!clean_complete)
return budget; // 还有工作 → 继续 poll
// Step 4: napi_complete → 重新启用中断
if (likely(napi_complete_done(napi, work_done)))
igb_ring_irq_enable(q_vector);
return work_done;
}
三个退出路径:
1. work_done (正常完成):TX 和 RX 都处理完毕,调用 napi_complete_done → 如果返回 true → 重新启用中断
2. budget (有剩余工作):TX 或 RX 处理到 budget 上限,返回 budget 告诉 net_rx_action 不要清除 SCHED → 下一轮继续
3. napi_complete_done 返回 false:即使 work < budget,但由于 MISSED 标志(意味着 poll 期间有新包到达),也不启用中断 → 重新调度
注意:TX clean 总是完成(不消耗 budget 计数),budget 只用于 RX 收包。work_done 只累计 RX 处理的包数。
5.2 igb_clean_rx_irq — 核心收包循环¶
虽然本文不展开此函数的完整代码,但其核心流程是:
循环直到 budget:
1. 从 RX ring 读取下一个描述符
2. 检查 DD (Descriptor Done) 位
3. 如果未完成 → 跳出循环
4. dma_sync_single_for_cpu (DMA 工程)
5. 构建 sk_buff
6. 设置协议类型 (eth_type_trans)
7. napi_gro_receive(&q_vector->napi, skb) ← GRO 入口
8. 返回新的空 buffer 给硬件
9. work_done++
6. 完整数据流:从网线到协议栈¶
6.1 硬件到驱动的路径¶
┌───────────────┐
│ 物理网络 │
│ 数据包到达 │
└───────┬───────┘
│
▼
┌───────────────────────────────────────────────┐
│ igb 网卡硬件 │
│ - DMA 帧到 RX Ring Buffer │
│ - 更新描述符 (设置 DD 位) │
│ - 触发 MSI-X 中断 (如果 ITR 允许) │
└───────────────────┬───────────────────────────┘
│
▼
┌───────────────────────────────────────────────┐
│ igb_msix_ring() [igb_main.c:7153]│
│ - igb_write_itr() 更新中断节流 │
│ - napi_schedule(&q_vector->napi) │
└───────────────────┬───────────────────────────┘
│
▼
┌───────────────────────────────────────────────┐
│ ____napi_schedule() [dev.c:4957] │
│ - list_add_tail → sd->poll_list │
│ - raise_softirq_irqoff(NET_RX_SOFTIRQ) │
└───────────────────┬───────────────────────────┘
│
▼
┌───────────────────────────────────────────────┐
│ net_rx_action() [dev.c:7914] │
│ - splice poll_list │
│ - 遍历 NAPI │
└───────────────────┬───────────────────────────┘
│
▼
┌───────────────────────────────────────────────┐
│ igb_poll() [igb_main.c:8278]│
│ │
│ ┌─ igb_clean_tx_irq() 清理 TX 完成 │
│ │ │
│ └─ igb_clean_rx_irq() 收包循环 │
│ │ │
│ ├── 读取描述符 (检查 DD 位) │
│ ├── dma_unmap / sync │
│ ├── 构建 sk_buff │
│ ├── eth_type_trans() │
│ └── napi_gro_receive() → GRO 合并 │
│ │ │
│ ├── GRO_MERGED 合并到已有包 │
│ ├── GRO_HELD 保留在 GRO 中 │
│ └── GRO_NORMAL 直接送入协议栈 │
│ │
│ ┌─ napi_complete_done(napi, work_done) │
│ │ └── gro_flush_normal() 刷新 GRO 中剩余包 │
│ └─ igb_ring_irq_enable() 重新启用中断 │
└───────────────────────────────────────────────┘
6.2 完整 ASCII 流程图¶
┌──────────────────────────────────────────────────────────────────────────┐
│ igb 驱动: NAPI + GRO 全链路 │
├──────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────┐ │
│ │ 硬件: 数据包到达 │ │
│ │ DMA → RX Ring │ │
│ │ 触发 MSI-X 中断 │ │
│ └──────────┬──────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────┐ │
│ │ igb_msix_ring() [igb_main.c:7153] │ │
│ │ │ │
│ │ igb_write_itr(q_vector) │ 动态中断调节 │
│ │ napi_schedule(&q_vector->napi) │ 调度 NAPI │
│ │ │ │
│ │ [硬件自动 mask 中断] │ ICR 读取 → Auto-Mask │
│ └──────────────┬───────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────┐ │
│ │ ____napi_schedule() [dev.c:4957] │ │
│ │ │ │
│ │ list_add_tail(&napi->poll_list, │ 加入 per-CPU poll_list │
│ │ &sd->poll_list) │ │
│ │ raise_softirq_irqoff │ 触发 NET_RX_SOFTIRQ │
│ │ (NET_RX_SOFTIRQ) │ │
│ └──────────────┬───────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────┐ │
│ │ net_rx_action() [dev.c:7914] │ │
│ │ │ │
│ │ list_splice_init → 取出所有 NAPI │ │
│ │ budget = 300 (全局限制) │ │
│ │ │ │
│ │ for each napi: │ │
│ │ budget -= napi_poll(n, &repoll) │ │
│ └──────────────┬───────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────┐ │
│ │ igb_poll() [igb_main.c:8278] │ │
│ │ │ │
│ │ ┌──────────────────────────────┐ │ │
│ │ │ igb_clean_tx_irq() │ │ 回收已发送的 buffer │
│ │ │ (不消耗 budget) │ │ │
│ │ └──────────────────────────────┘ │ │
│ │ │ │
│ │ ┌──────────────────────────────┐ │ │
│ │ │ igb_clean_rx_irq() │ │ │
│ │ │ │ │ │
│ │ │ while (work < budget): │ │ budget = 64 (NAPI weight) │
│ │ │ rx_desc = ring[next] │ │ │
│ │ │ if !(DD) break │ │ 没有新包 → 退出 │
│ │ │ skb = build_skb(rx_desc) │ │ 构建 sk_buff │
│ │ │ napi_gro_receive(napi,skb)│ │ → GRO 合并 │
│ │ │ work_done++ │ │ │
│ │ └──────────────────────────────┘ │ │
│ │ │ │
│ │ if (!clean_complete) │ │
│ │ return budget; // 继续 poll │ │
│ │ │ │
│ │ if (napi_complete_done(napi, work))│ │
│ │ igb_ring_irq_enable(q_vector); │ 重新启用中断 │
│ │ │ │
│ │ return work_done; │ │
│ └──────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────────────────┘
7. ITR:中断调节¶
7.1 igb_write_itr (igb_main.c:7144)¶
// drivers/net/ethernet/intel/igb/igb_main.c:7144
static void igb_write_itr(struct igb_q_vector *q_vector)
{
u32 itr_val = q_vector->itr_val & ...;
// ...
writel(itr_val, ...); // 写入 EITR 寄存器
}
ITR (Interrupt Throttle Rate) 控制中断的触发频率: - 低 ITR:中断频率高 → 延迟低 → CPU 占用高 - 高 ITR:中断频率低 → 延迟高 → CPU 占用低(批量处理更多包)
igb 支持多种 ITR 模式: - 动态模式:根据 RX/TX 包速率自适应调整 - 固定模式:用户通过 ethtool 设置固定值 - "自适应"模式:根据包大小、中断间隔动态微调
ITR 的工作原理是在每次中断处理时设定下一次中断的最小间隔,类似于 hrtimer 的 tick 合并。
8. 驱动开发要点¶
8.1 NAPI 驱动的标准模板¶
// === 初始化阶段 (probe) ===
static int driver_probe(struct pci_dev *pdev, ...)
{
// 1. 分配和初始化适配器
struct driver_adapter *adapter = ...;
// 2. 为每个队列初始化 NAPI
for (i = 0; i < num_queues; i++) {
netif_napi_add(dev, &adapter->q_vector[i].napi,
driver_poll, 64);
// 或使用新的 netif_napi_add_config()
}
// 3. 注册中断处理函数
request_irq(irq, driver_msix_ring, 0, name, q_vector);
}
// === 中断处理 (硬中断上下文) ===
static irqreturn_t driver_msix_ring(int irq, void *data)
{
struct driver_q_vector *q_vector = data;
// 屏蔽中断 (硬件 Auto-Mask 或显式 mask)
napi_schedule(&q_vector->napi);
return IRQ_HANDLED;
}
// === NAPI 轮询 (软中断上下文) ===
static int driver_poll(struct napi_struct *napi, int budget)
{
struct driver_q_vector *q_vector =
container_of(napi, struct driver_q_vector, napi);
int work_done = 0;
// TX clean (可选)
driver_clean_tx(q_vector);
// RX clean
while (work_done < budget) {
skb = driver_get_rx_packet(q_vector);
if (!skb)
break;
napi_gro_receive(napi, skb);
work_done++;
}
if (work_done < budget) {
napi_complete_done(napi, work_done);
driver_enable_irq(q_vector); // 重新启用中断
}
return work_done;
}
8.2 常见陷阱¶
| 问题 | 原因 | 解决方案 |
|---|---|---|
| hung task / soft lockup | poll 函数无限循环 | 确保在无包时 break,budget 检查 |
| 中断风暴 | Budget 总是用完 | 使用 ITR / 增大 weight |
| 包丢失 | Ring buffer 溢出 | 增大 ring size / 调整中断亲和性 |
| NAPI 双重调度 | 竞态条件 | 正确使用 napi_schedule_prep 返回值 |
| GRO 不工作 | skb 没有设置正确的 hash | 使用 ethtool 配置 RSS hash |
| XDP 与 NAPI 冲突 | XDP 在 NAPI 上下文中运行 | 理解执行顺序:XDP → GRO → 协议栈 |
8.3 性能调优参数 (通过 ethtool)¶
| 参数 | ethtool 选项 | 默认 | 影响 |
|---|---|---|---|
| RX ring size | -G rx N |
256 | 更大的 ring 容忍突发流量 |
| TX ring size | -G tx N |
256 | 同上 |
| ITR | -C rx-usecs N |
动态 | 调节中断频率,平衡延迟/吞吐 |
| 中断聚合 | -C rx-frames N |
0 | 收到 N 帧后才中断 |
| 队列数 | -L combined N |
系统决定 | 更多队列 → 更好的并行性 |
9. 总结¶
以 igb 为例的 NAPI 驱动集成展示了标准模式:
- 注册阶段:
netif_napi_add_weight_locked()将 poll 函数与 NAPI 上下文绑定 - 中断处理:最简形式只有
napi_schedule()— 依赖硬件 Auto-Mask 和软中断处理 - Poll 函数:TX clean + RX 收包 + GRO →
napi_complete_done()→ 重新启用中断 - budget 控制:
work < budget正常完成;work == budget继续轮询;MISSED标志处理竞态 - ITR 调节:在中断入口动态调整中断频率,平衡延迟和吞吐
下一篇将分析 NAPI 的高级模式:Busy Poll(忙轮询)和线程化 NAPI。
下一篇: 第4篇:忙轮询与线程化 NAPI