2. Linux 网络协议栈基础:网络设备驱动模型、sk_buff 结构体、NAPI 与中断收包

在深入 TUN/TAP 虚拟网络设备之前,必须理解 Linux 内核网络协议栈的底层基石。本章聚焦三个核心主题:网络设备驱动模型(设备如何注册与操作)、sk_buff 结构体(数据包在内核中的载体)以及 NAPI 与中断收包机制(高性能收包路径)。掌握这些知识是后续实现虚拟设备驱动的前提。

2.1 网络设备驱动模型

Linux 内核中的网络设备驱动模型围绕 struct net_device 展开。每个网络接口(无论是物理网卡还是虚拟设备)在内核中都对应一个 net_device 实例。驱动通过注册该结构体,向协议栈暴露设备能力与操作函数。

核心概念:网络设备驱动模型采用“设备-驱动”分离架构。设备描述硬件属性(MAC 地址、MTU、队列数),驱动提供操作函数集(ndo_start_xmit、ndo_open、ndo_stop 等)。

2.1.1 struct net_device 关键字段

字段 类型 说明
name char[] 设备名称,如 eth0、tun0
dev_addr unsigned char[] MAC 地址(仅以太网设备)
mtu unsigned int 最大传输单元
flags unsigned short 设备标志(IFF_UP、IFF_RUNNING 等)
netdev_ops const struct net_device_ops * 驱动操作函数指针表
priv_destructor void (*)(struct net_device *) 设备销毁回调
ml_priv void * 驱动私有数据指针

2.1.2 设备注册与注销流程

  1. 分配:调用 alloc_netdev() 或 alloc_netdev_mqs() 分配 net_device 结构体,同时分配私有数据区。
  2. 初始化:设置 netdev_ops、flags、mtu、dev_addr 等字段。
  3. 注册:调用 register_netdev() 将设备加入内核网络子系统,此时设备对用户空间可见。
  4. 注销:调用 unregister_netdev() 移除设备,随后调用 free_netdev() 释放内存。
编程提示:对于 TUN/TAP 设备,alloc_netdev() 的 setup 回调函数会设置 netdev_ops 为 tun_netdev_ops,其中 ndo_start_xmit 指向 tun_net_xmit(),该函数将数据包从内核协议栈转发到用户空间的文件描述符。

2.1.3 网络设备驱动模型架构图

网络设备驱动模型架构 用户空间 (Socket API / ioctl) 内核网络协议栈 (TCP/IP, AF_PACKET, 路由子系统) struct net_device (eth0 / tun0 / tap0) net_device_ops ndo_start_xmit / ndo_open 驱动私有数据 (ml_priv) 物理硬件 / 虚拟设备后端 (TUN/TAP 文件描述符)

2.2 sk_buff 结构体:数据包的内核载体

struct sk_buff(简称 skb)是 Linux 网络协议栈中最核心的数据结构。它贯穿整个协议栈,从网卡驱动收包到 socket 层交付,或从 socket 层发包到网卡发送,所有数据包都以 skb 形式存在。

注意:sk_buff 是一个“胖”结构体,包含约 200 个字段。理解其核心布局(head、data、tail、end 指针)和共享/克隆机制是高效网络编程的关键。

2.2.1 sk_buff 内存布局

每个 skb 关联一个线性数据缓冲区,由四个指针界定:

  • head:缓冲区起始地址。
  • data:有效数据起始地址(协议头操作时移动)。
  • tail:有效数据结束地址。
  • end:缓冲区结束地址。

协议栈通过移动 data 和 tail 指针来实现协议头的添加(push)与去除(pull),避免数据拷贝。

2.2.2 关键字段速查表

字段 说明
dev 接收或发送该 skb 的网络设备
sk 关联的 socket(如果有)
len 有效数据总长度(包括分片)
data_len 分片数据总长度
protocol L3 协议类型(如 ETH_P_IP)
pkt_type 包类型(PACKET_HOST、PACKET_BROADCAST 等)
cb 控制块,供协议层私有使用(48 字节)
destructor skb 释放时的回调函数

2.2.3 sk_buff 操作示例

/* 分配一个 skb,并预留 headroom */
struct sk_buff *skb = alloc_skb(len + headroom, GFP_KERNEL);
skb_reserve(skb, headroom);  /* 移动 data/tail 指针,预留头部空间 */

/* 向 skb 尾部添加数据 */
skb_put(skb, len);
memcpy(skb->data, data, len);

/* 向 skb 头部添加协议头(push) */
struct ethhdr *eth = (struct ethhdr *)skb_push(skb, ETH_HLEN);
eth->h_proto = htons(ETH_P_IP);
memcpy(eth->h_source, src_mac, ETH_ALEN);
memcpy(eth->h_dest, dst_mac, ETH_ALEN);

/* 设置协议类型 */
skb->protocol = eth_type_trans(skb, dev);

/* 将 skb 交给协议栈 */
netif_rx(skb);  /* 或使用 NAPI 的 netif_receive_skb */

2.2.4 sk_buff 操作与指针移动示意图

sk_buff 线性缓冲区指针操作 headroom 有效数据 (data ~ tail) tailroom head data tail end 常见操作 skb_push(): data--, len++ skb_put(): tail++, len++ skb_pull(): data++, len--

2.3 NAPI 与中断收包机制

传统收包方式中,每个数据包到达都会触发硬件中断,导致 CPU 频繁被中断,在高吞吐场景下效率极低(中断风暴)。NAPI(New API) 是 Linux 内核引入的混合收包机制,结合中断与轮询,显著提升高性能网络场景下的收包效率。

2.3.1 传统中断收包 vs NAPI

特性 传统中断模式 NAPI 模式
触发方式 每个包产生一次中断 首个包中断,后续轮询
高吞吐表现 中断风暴,CPU 100% 处理中断 轮询批量收包,CPU 利用率低
延迟 低(单包场景) 略高(但可接受)
适用场景 低速网络、低吞吐 高速网络(1Gbps+)
内核接口 netif_rx() netif_receive_skb()

2.3.2 NAPI 工作流程

  1. 中断触发:网卡收到第一个数据包,触发硬件中断。
  2. 关闭中断:中断处理程序调用 napi_schedule(),将设备的 NAPI 结构体加入软中断处理队列,并关闭该设备的硬件中断。
  3. 轮询开始:软中断(NET_RX_SOFTIRQ)调用 NAPI 的 poll 回调函数,批量从网卡接收数据包。
  4. 批量处理:poll 函数循环调用 netif_receive_skb() 将 skb 送入协议栈,直到预算(budget)耗尽或队列为空。
  5. 重新开启中断:当队列为空时,NAPI 完成轮询,重新开启硬件中断,等待下一个数据包。
关键数据结构:struct napi_struct 包含 poll 函数指针、weight(预算上限)、state(NAPI_STATE_SCHED 等标志)以及 dev_list(关联的设备列表)。

2.3.3 NAPI 收包流程图

NAPI 收包流程 ① 数据包到达,硬件中断触发 ② 中断处理:关闭网卡中断,napi_schedule() ③ NET_RX_SOFTIRQ 调用 poll() 回调 ④ 循环:netif_receive_skb(skb) 送入协议栈 预算未耗尽 且 队列非空 队列空? 否(继续) 回到 ④ 是 ⑤ 重新开启中断

2.3.4 NAPI 在虚拟设备中的特殊考量

对于 TUN/TAP 这类虚拟设备,数据包并非来自硬件,而是来自用户空间写入文件描述符。因此,虚拟设备驱动通常不直接使用硬件中断,而是模拟 NAPI 行为:

  • 当用户空间写入数据时,驱动在进程上下文构造 skb,并直接调用 netif_receive_skb() 将包送入协议栈。
  • 对于发送方向(协议栈 -> 用户空间),驱动在 ndo_start_xmit 中构造 skb,并通过 skb_queue_tail 放入队列,随后唤醒用户空间进程读取。
  • 部分虚拟设备实现会使用 napi_gro_receive() 来利用 GRO(Generic Receive Offload)提高性能。
性能建议:在实现 TUN/TAP 驱动时,如果吞吐量要求高(如 VPN 隧道),建议在收包路径上使用 napi_gro_receive() 替代 netif_receive_skb(),以利用 GRO 合并小包,减少协议栈处理开销。

2.4 本章小结

  • 网络设备驱动模型以 struct net_device 为核心,通过 netdev_ops 操作函数集与协议栈交互。虚拟设备驱动需实现 ndo_start_xmit 等回调。
  • sk_buff 是数据包在内核中的标准载体,理解其 head/data/tail/end 指针布局和 push/put/pull 操作是编写驱动的基础。
  • NAPI 通过中断+轮询混合模式解决高吞吐下的中断风暴问题。虚拟设备虽无硬件中断,但可借鉴 NAPI 的批量处理思想,结合 GRO 提升性能。