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 设备注册与注销流程
- 分配:调用
alloc_netdev()或alloc_netdev_mqs()分配net_device结构体,同时分配私有数据区。 - 初始化:设置
netdev_ops、flags、mtu、dev_addr等字段。 - 注册:调用
register_netdev()将设备加入内核网络子系统,此时设备对用户空间可见。 - 注销:调用
unregister_netdev()移除设备,随后调用free_netdev()释放内存。
编程提示:对于 TUN/TAP 设备,
alloc_netdev() 的 setup 回调函数会设置 netdev_ops 为 tun_netdev_ops,其中 ndo_start_xmit 指向 tun_net_xmit(),该函数将数据包从内核协议栈转发到用户空间的文件描述符。
2.1.3 网络设备驱动模型架构图
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 操作与指针移动示意图
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 工作流程
- 中断触发:网卡收到第一个数据包,触发硬件中断。
- 关闭中断:中断处理程序调用
napi_schedule(),将设备的 NAPI 结构体加入软中断处理队列,并关闭该设备的硬件中断。 - 轮询开始:软中断(NET_RX_SOFTIRQ)调用 NAPI 的
poll回调函数,批量从网卡接收数据包。 - 批量处理:
poll函数循环调用netif_receive_skb()将 skb 送入协议栈,直到预算(budget)耗尽或队列为空。 - 重新开启中断:当队列为空时,NAPI 完成轮询,重新开启硬件中断,等待下一个数据包。
关键数据结构:
struct napi_struct 包含 poll 函数指针、weight(预算上限)、state(NAPI_STATE_SCHED 等标志)以及 dev_list(关联的设备列表)。
2.3.3 NAPI 收包流程图
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 提升性能。