2. 网络设备驱动基础:struct net_device 结构体详解、NAPI 轮询机制、中断与轮询的权衡

网络设备驱动是内核网络协议栈的基石,而 struct net_device 则是驱动与协议栈之间的核心桥梁。本章将深入剖析该结构体的关键字段、NAPI 轮询机制的设计原理,以及在实际工程中如何权衡中断与轮询模式。

2.1 struct net_device 结构体详解

struct net_device 是 Linux 内核中用于描述和管理网络设备的巨型结构体(定义在 include/linux/netdevice.h),它包含了设备的状态、操作函数、统计数据、硬件特征等数百个字段。面试中常考察的核心字段可归纳为以下几类:

分类 字段名 说明 面试关注点
标识与名称 char name[IFNAMSIZ] 设备名称,如 eth0、lo 命名规则、重命名机制
状态与标志 unsigned long state 设备状态位,如 __LINK_STATE_START 状态机转换、与用户态 ifconfig 的对应
操作函数集 const struct net_device_ops *netdev_ops 驱动实现的 ndo_open/ndo_stop/ndo_start_xmit 等 数据发送路径、回调注册时机
NAPI 相关 struct napi_struct napi_list 设备关联的 NAPI 实例链表 多队列 NAPI 绑定、轮询权重
特征与能力 netdev_features_t features 硬件 offload 能力位图,如 TSO/GRO/LRO 特性协商、与协议栈交互
统计数据 struct rtnl_link_stats64 stats 收发包计数、错误计数等 原子更新、procfs 导出
队列管理 struct netdev_queue *ingress_queue 入向队列(用于 ifb 等) 多队列调度、XDP 重定向
面试高频点:面试官常问“net_device_ops 中的 ndo_start_xmit 函数返回 NETDEV_TX_OK 或 NETDEV_TX_BUSY 时,协议栈如何处理?”——返回 BUSY 时,协议栈会将 skb 重新入队并稍后重试,驱动应避免频繁返回 BUSY 导致性能下降。

2.2 NAPI 轮询机制

NAPI(New API)是 Linux 内核为高性能网络设备设计的混合中断-轮询机制,旨在解决传统中断模式在高速网络下的“中断风暴”问题。其核心思想是:在数据流量较低时使用中断唤醒处理,在流量较高时切换到轮询模式批量收包。

2.2.1 NAPI 工作流程

  1. 中断触发:硬件收到数据包后触发 IRQ,驱动中断处理函数调用 napi_schedule()。
  2. 关闭中断并加入轮询列表:napi_schedule() 将设备的 NAPI 实例添加到当前 CPU 的 softnet_data 轮询链表中,并关闭硬件中断。
  3. 软中断调度:触发 NET_RX_SOFTIRQ 软中断,内核开始遍历轮询链表。
  4. 轮询回调:调用驱动注册的 poll() 函数,驱动从硬件队列中批量读取数据包并送入协议栈。
  5. 预算控制:poll() 函数返回实际处理的数据包数量,不能超过预设的 budget(通常为 64)。
  6. 退出条件:若处理完所有数据包(返回 < budget),则重新开启硬件中断;否则继续轮询。
关键数据结构:struct napi_struct 包含 poll() 函数指针、weight(轮询权重)、state(NAPI_STATE_SCHED 等标志位)以及 dev_list 链表节点。
NAPI 中断-轮询混合工作流程 ① 硬件中断到达 ② napi_schedule() ③ 关闭硬件中断 ④ 加入 softnet_data 链表 ⑤ NET_RX_SOFTIRQ 软中断 ⑥ poll() 批量收包 返回 < budget? 是 → 重新开启中断 否 → 继续轮询

2.2.2 NAPI 驱动注册示例

// 驱动初始化时注册 NAPI
static int my_driver_probe(struct pci_dev *pdev, const struct pci_device_id *id)
{
    struct net_device *ndev;
    struct my_priv *priv;

    ndev = alloc_etherdev(sizeof(struct my_priv));
    priv = netdev_priv(ndev);

    // 初始化 NAPI 实例,weight 通常设为 64
    netif_napi_add(ndev, &priv->napi, my_poll, 64);

    // 注册 net_device_ops
    ndev->netdev_ops = &my_netdev_ops;

    register_netdev(ndev);
    return 0;
}

// 中断处理函数
static irqreturn_t my_interrupt(int irq, void *dev_id)
{
    struct net_device *ndev = dev_id;
    struct my_priv *priv = netdev_priv(ndev);

    // 禁止硬件中断(由 NAPI 管理)
    // ...

    // 调度 NAPI 轮询
    if (napi_schedule_prep(&priv->napi)) {
        __napi_schedule(&priv->napi);
    }

    return IRQ_HANDLED;
}

// 轮询回调函数
static int my_poll(struct napi_struct *napi, int budget)
{
    struct my_priv *priv = container_of(napi, struct my_priv, napi);
    int work_done = 0;

    // 从硬件队列批量读取数据包
    while (work_done < budget) {
        struct sk_buff *skb = my_read_packet(priv);
        if (!skb)
            break;

        // 送入协议栈
        napi_gro_receive(napi, skb);
        work_done++;
    }

    // 如果处理完所有包,退出轮询并重新开启中断
    if (work_done < budget) {
        napi_complete_done(napi, work_done);
        // 重新开启硬件中断
        my_enable_irq(priv);
    }

    return work_done;
}

2.3 中断与轮询的权衡

在高速网络场景下,中断模式和纯轮询模式各有优劣。NAPI 通过动态切换实现了两者的平衡,但工程师仍需根据硬件特性和业务负载进行调优。

对比维度 传统中断模式 纯轮询模式 NAPI 混合模式
CPU 开销 低负载时低,高负载时因中断风暴急剧升高 持续占用 CPU,低负载时浪费 低负载时接近中断,高负载时接近轮询
延迟 低(立即响应) 取决于轮询间隔 低负载时低,高负载时略有增加
吞吐量 高负载时因中断开销下降 高负载时稳定 高负载时保持高吞吐
实现复杂度 简单 简单 中等(需管理状态切换)
适用场景 低速网络(< 1Gbps) 专用高速设备(如 DPDK) 通用高速网络(1Gbps ~ 100Gbps)
工程陷阱:NAPI 的 weight 参数并非越大越好。过大的 weight 会导致单次轮询占用 CPU 时间过长,增加其他网络流量的延迟。内核默认值为 64,对于 10Gbps 网卡,建议根据 MTU 和包大小进行微调。例如,小包场景(64 字节)可适当降低 weight 以平衡延迟,大包场景(1500 字节)可适当提高 weight 以提升吞吐。

2.3.1 自适应中断调节

现代网卡驱动通常支持自适应中断调节(Adaptive Interrupt Coalescing),通过硬件参数(如 ITR/Interrupt Throttling Rate)动态调整中断频率。NAPI 与硬件中断调节协同工作时,需注意以下配置:

  • 中断合并(Interrupt Coalescing):硬件在发送中断前等待一定时间或积累一定数量的数据包,减少中断次数。但会增加延迟。
  • NAPI 权重与硬件队列深度:确保 weight 不小于硬件队列的收包阈值,避免 NAPI 频繁退出轮询导致中断重开。
  • 多队列 RSS 与 NAPI 绑定:每个 RX 队列应绑定独立的 NAPI 实例,并亲和到不同 CPU,实现并行收包。
中断 vs 轮询:CPU 开销随负载变化趋势 网络负载(pps) CPU 开销 传统中断 纯轮询 NAPI 混合 低负载区:中断优势 高负载区:轮询优势 切换点

2.4 面试高频问题与实战建议

Q1:NAPI 如何避免中断风暴?
A:NAPI 在第一次中断后关闭硬件中断,通过软中断轮询收包。只要轮询未完成(即仍有数据包待处理),硬件中断保持关闭,从而避免中断风暴。只有当轮询返回数量小于 budget 时,才重新开启中断。
Q2:napi_schedule() 和 __napi_schedule() 的区别?
A:napi_schedule() 会先检查 NAPI 是否已被调度(通过 napi_schedule_prep()),确保原子性;__napi_schedule() 则假设调用者已确认 NAPI 可调度,直接加入轮询链表。驱动中应优先使用 napi_schedule()。
Q3:多队列网卡如何配置 NAPI 实现并行收包?
A:每个 RX 队列分配独立的 napi_struct 实例,通过 netif_napi_add() 注册到同一个 net_device。利用 RSS(Receive Side Scaling)将不同队列的中断亲和到不同 CPU,实现并行处理。
实战调优参数:
/sys/class/net/eth0/napi_defer_hard_irqs:延迟硬件中断的触发次数,减少中断频率。
/sys/class/net/eth0/gro_flush_timeout:GRO 刷新的超时时间,与 NAPI 轮询配合使用。
建议在 10Gbps 以上场景中,将 napi_defer_hard_irqs 设置为 2~5,gro_flush_timeout 设置为 20000~50000 ns。