Linux虚拟网卡驱动开发与NAPI抓包应用 - 第1节

1.1 Linux内核网络栈概览

Linux内核网络栈是一个分层的、高性能的数据包处理框架,从网卡接收到用户态应用,中间经过多个协议层与内核子系统。理解其整体架构是掌握NAPI与ndo接口的前提。

1.1.1 网络栈分层架构

内核网络栈从上到下(或从下到上)可划分为以下关键层次:

层次 核心组件 主要职责
套接字层 BSD Socket、INET Socket 提供用户态系统调用接口(socket, bind, send, recv)
传输层 TCP、UDP、SCTP 端到端可靠传输、流量控制、拥塞控制
网络层 IPv4、IPv6、路由子系统、Netfilter 路由查找、分片重组、防火墙过滤
数据链路层 邻居子系统(ARP/ND)、桥接、LLC MAC地址解析、二层转发、VLAN处理
设备层 网络设备子系统、NAPI、ndo 驱动接口抽象、中断/轮询调度、数据包收发
关键认知:设备层是驱动开发者直接打交道的层次。NAPI和ndo接口正是位于设备层与驱动之间的核心抽象,决定了数据包如何从硬件进入协议栈。

1.1.2 数据包接收路径(传统中断模式)

在NAPI诞生之前,传统收包路径如下:

  1. 硬件中断:网卡收到数据包,通过PCIe总线触发CPU中断。
  2. 中断处理:驱动中断服务程序(ISR)被调用,从RX ring buffer中取出sk_buff。
  3. 软中断调度:ISR标记NET_RX_SOFTIRQ软中断,退出硬中断上下文。
  4. 协议栈处理:ksoftirqd内核线程执行net_rx_action(),将sk_buff送入协议栈。
  5. 用户态交付:经过TCP/IP处理后,数据到达socket接收队列,唤醒用户进程。
性能瓶颈:高吞吐场景下,每个数据包都触发一次中断,导致频繁的上下文切换和CPU缓存污染。这就是NAPI要解决的核心问题。

1.2 NAPI机制诞生背景

1.2.1 中断风暴问题

在千兆乃至万兆网卡时代,传统中断驱动模型暴露出严重缺陷:

  • 中断频率过高:10Gbps线速下,64字节小包可达14.88Mpps(百万包/秒),每秒触发千万级中断。
  • CPU饥饿:CPU大量时间花在中断上下文的保存/恢复上,无法处理协议栈和用户态任务。
  • 缓存抖动:每次中断都刷新TLB和L1/L2缓存,有效吞吐量急剧下降。

1.2.2 NAPI的设计思想

NAPI(New API)由David S. Miller等人于2002年引入Linux 2.5内核,核心思想是中断与轮询混合:

模式 触发条件 行为
中断模式 第一个数据包到达 触发中断,驱动关闭该队列的中断使能,切换到轮询
轮询模式 中断后进入poll回调 驱动批量从硬件读取数据包,直到预算耗尽或队列为空
切回中断 轮询结束且无更多数据 重新开启中断使能,等待下一个数据包触发
核心收益:高负载时自动切换为轮询,消除中断风暴;低负载时仍使用中断,保持低延迟。NAPI将收包效率从O(N)中断降低为接近O(1)轮询。

1.2.3 NAPI在协议栈中的位置

下图展示了NAPI在数据包接收路径中的核心位置:

NAPI在Linux内核网络栈中的位置 网卡硬件(NIC) 驱动中断处理(ISR) NAPI poll 回调 net_rx_action() TCP/IP协议栈 用户态应用 NAPI核心流程 ① 首包触发中断 → ② ISR关闭中断,注册poll → ③ net_rx_action()调用poll批量收包 → ④ 收完重新开中断

1.3 ndo(网络设备操作)接口的作用

1.3.1 ndo接口定义

ndo是net_device_ops结构体的缩写,定义在include/linux/netdevice.h中。它是内核网络设备子系统与具体驱动之间的操作函数表,驱动通过填充该结构体向内核注册自己的硬件操作能力。

// 核心数据结构(简化)
struct net_device_ops {
    int (*ndo_open)(struct net_device *dev);
    int (*ndo_stop)(struct net_device *dev);
    netdev_tx_t (*ndo_start_xmit)(struct sk_buff *skb, struct net_device *dev);
    int (*ndo_set_rx_mode)(struct net_device *dev);
    int (*ndo_change_mtu)(struct net_device *dev, int new_mtu);
    int (*ndo_set_mac_address)(struct net_device *dev, void *addr);
    int (*ndo_do_ioctl)(struct net_device *dev, struct ifreq *ifr, int cmd);
    // ... 更多回调
};

1.3.2 ndo与NAPI的协作关系

ndo接口和NAPI机制在驱动中紧密配合,共同完成数据包的收发:

操作方向 ndo回调 NAPI角色
发送 ndo_start_xmit:将sk_buff送入硬件发送队列 发送完成中断触发NAPI poll,回收TX描述符
接收 ndo_open:注册NAPI结构体到内核 接收中断触发后,poll回调批量读取RX数据包
控制 ndo_set_rx_mode:设置多播/混杂模式 影响NAPI是否接收特定类型数据包
生命周期 ndo_stop:注销NAPI,释放硬件资源 确保NAPI poll不再被调度

1.3.3 典型驱动初始化流程

以下代码片段展示了一个虚拟网卡驱动如何注册ndo和NAPI:

// 1. 定义ndo操作表
static const struct net_device_ops my_ndo_ops = {
    .ndo_open       = my_ndo_open,
    .ndo_stop       = my_ndo_stop,
    .ndo_start_xmit = my_ndo_start_xmit,
    .ndo_set_rx_mode = my_ndo_set_rx_mode,
};

// 2. 在probe函数中初始化
static int my_probe(struct pci_dev *pdev, const struct pci_device_id *id)
{
    struct net_device *ndev;
    struct my_priv *priv;

    // 分配net_device
    ndev = alloc_etherdev(sizeof(struct my_priv));
    ndev->netdev_ops = &my_ndo_ops;  // 绑定ndo

    // 初始化NAPI
    priv = netdev_priv(ndev);
    netif_napi_add(ndev, &priv->napi, my_poll, 64); // 预算=64

    // 注册网络设备
    register_netdev(ndev);
    return 0;
}

// 3. NAPI poll回调
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_rx_packet(priv);
        if (!skb) break;
        napi_gro_receive(napi, skb); // 送入协议栈
        work_done++;
    }

    if (work_done < budget) {
        napi_complete_done(napi, work_done); // 重新开中断
    }
    return work_done;
}
实践要点:ndo接口是内核与驱动之间的契约,驱动必须正确实现所有必需的回调。NAPI poll回调的预算参数(budget)直接影响收包延迟和CPU占用,是性能调优的关键杠杆。

1.4 本章小结

通过本章学习,你应该掌握以下核心概念:

  • 网络栈分层:设备层是驱动开发的主战场,NAPI和ndo位于该层。
  • NAPI诞生背景:解决高吞吐下的中断风暴问题,采用中断+轮询混合模式。
  • ndo接口作用:作为内核与驱动的操作函数表,定义了设备生命周期、数据收发、控制管理等标准接口。
  • 协作关系:ndo提供框架,NAPI提供高效收包引擎,两者共同构成现代Linux网卡驱动的基石。