内核网络协议栈测试:pktgen与netperf压测方法 - 第2节
2.1 网络分层模型:OSI 与 TCP/IP
网络协议栈的核心设计思想是分层。每一层只负责特定的通信功能,层与层之间通过标准接口交互。理解分层模型是分析内核网络性能瓶颈的前提。
2.1.1 OSI 七层模型 vs. TCP/IP 四层模型
| OSI 七层模型 | TCP/IP 四层模型 | 典型协议与设备 | 内核关注点 |
|---|---|---|---|
| 应用层 | 应用层 | HTTP, FTP, DNS, NFS | Socket 接口、系统调用 |
| 表示层 | SSL/TLS, 数据加密 | 内核 crypto 子系统 | |
| 会话层 | RPC, NetBIOS | — | |
| 传输层 | 传输层 | TCP, UDP, SCTP | 拥塞控制、重传、滑动窗口 |
| 网络层 | 网络层 | IP, ICMP, ARP | 路由查找、邻居子系统 |
| 数据链路层 | 网络接口层 | Ethernet, VLAN, Bridge | NAPI、网卡驱动、队列规则 |
| 物理层 | 网线、光纤、信号 | 硬件中断、DMA |
压测视角:pktgen 工作在网络层(构造 IP 包),绕过完整的传输层协议栈;netperf 工作在应用层,经过完整的 TCP/UDP 协议栈。选择哪种工具取决于你要测试哪一层的极限。
2.1.2 数据封装与解封装流程
发送数据时,每层添加自己的头部;接收数据时,每层剥离对应的头部。这个过程直接影响 CPU 开销和内存带宽。
2.2 内核网络子系统架构
Linux 网络子系统是一个庞大的模块化体系,从系统调用到底层驱动,主要分为以下核心路径:
性能关键路径:在压测场景中,数据包在传输层(TCP 拥塞控制)、网络层(路由缓存查找)和邻居子系统(ARP 解析)三个环节最容易产生 CPU 瓶颈。使用 pktgen 可以绕过传输层,直接测试网络层及以下的极限吞吐。
2.3 sk_buff:网络数据的核心载体
sk_buff(Socket Buffer)是内核网络子系统中最重要的数据结构。每一个收发的数据包在内核中都以 sk_buff 实例表示。它通过指针和预分配缓冲区实现零拷贝数据操作。
2.3.1 sk_buff 结构概览
struct sk_buff {
/* 链表管理 */
struct sk_buff *next;
struct sk_buff *prev;
struct sk_buff_head *list;
/* 网络层信息 */
struct net_device *dev; // 关联的网络设备
struct sock *sk; // 关联的 socket
/* 数据指针 */
unsigned char *head; // 缓冲区起始
unsigned char *data; // 协议头起始
unsigned char *tail; // 数据结束
unsigned char *end; // 缓冲区结束
/* 长度与校验 */
__u32 len; // 实际数据长度
__u32 truesize; // 总分配大小(含 sk_buff 自身)
__u32 data_len; // 非线性区数据长度(frag 列表)
/* 协议标识 */
__be16 protocol; // L3 协议类型 (ETH_P_IP, ETH_P_ARP)
__u16 mac_header; // MAC 头偏移
__u16 network_header; // 网络头偏移
__u16 transport_header; // 传输头偏移
};
2.3.2 数据指针与头部操作
sk_buff 通过 head、data、tail、end 四个指针管理缓冲区。协议栈在封装和解封装时,只需移动 data 和 tail 指针,无需拷贝数据。
压测优化点:pktgen 在构造数据包时,会直接操作
sk_buff 的 data 指针填充 IP 头和以太网头,跳过 socket 层和传输层,从而大幅降低 CPU 开销。这也是 pktgen 能打到线速的原因之一。
2.4 NAPI 机制:高性能收包引擎
NAPI(New API)是 Linux 内核为应对高速网络而设计的收包机制。它结合了中断和轮询两种模式,在低负载时使用中断降低延迟,在高负载时切换为轮询减少中断风暴。
2.4.1 NAPI 工作流程
- 中断触发:网卡收到数据包,触发硬件中断。
- 关闭中断:中断处理程序关闭该网卡的中断,将设备加入 NAPI 轮询列表。
- 软中断调度:触发 NET_RX_SOFTIRQ 软中断,唤醒 ksoftirqd 线程。
- 轮询收包:NAPI 轮询函数从网卡 RX 队列批量读取数据包,送入协议栈。
- 退出条件:当收包数量达到预算(budget)或队列为空时,重新开启中断,退出轮询。
2.4.2 NAPI 关键参数
| 参数 | 位置 | 说明 | 压测建议 |
|---|---|---|---|
netdev_budget |
/proc/sys/net/core/netdev_budget |
每次 NAPI 轮询最多处理的包数(默认 300) | 高吞吐场景可增大至 600~1200 |
netdev_budget_usecs |
/proc/sys/net/core/netdev_budget_usecs |
NAPI 轮询的最大时间预算(微秒,默认 2000) | 延迟敏感场景可减小,吞吐场景可增大 |
gro_flush_timeout |
/proc/sys/net/core/gro_flush_timeout |
GRO 合并超时时间(纳秒) | 默认 0 表示立即刷新;增大可提高合并效率 |
rps_flow_cnt |
/sys/class/net/ethX/queues/rx-X/rps_flow_cnt |
RPS 流表大小,用于多队列负载均衡 | 多核场景建议设置为 4096 |
常见陷阱:使用 pktgen 压测时,如果发现 CPU 软中断(softirq)占用接近 100%,但收包速率上不去,通常是因为
netdev_budget 太小,导致 NAPI 频繁退出轮询并重新触发中断。此时应增大 netdev_budget 并观察效果。
2.4.3 NAPI 与 GRO 协同
GRO(Generic Receive Offload)在 NAPI 轮询过程中将多个小包合并为一个大包,减少协议栈处理次数。合并后的 sk_buff 通过 skb_shinfo(skb)->frag_list 链接原始包。
// NAPI poll 函数典型流程(伪代码)
int napi_poll(struct napi_struct *napi, int budget) {
int work_done = 0;
struct sk_buff *skb;
while (work_done < budget) {
skb = netdev->rx_queue->dequeue();
if (!skb) break;
// GRO 合并尝试
if (gro_receive(napi, skb)) {
// 包被合并,不计数
continue;
}
// 送入协议栈
netif_receive_skb(skb);
work_done++;
}
// 判断是否退出轮询
if (work_done < budget) {
napi_complete(napi);
netdev->enable_irq();
}
return work_done;
}
压测关联:netperf 的 TCP_STREAM 测试中,GRO 可以显著降低 CPU 占用。使用
ethtool -K ethX gro on 开启 GRO 后,同一条流上的小包会被合并,协议栈处理次数减少 30%~50%。pktgen 测试时建议关闭 GRO(gro off),因为 pktgen 构造的包通常不需要合并,且 GRO 可能引入额外的延迟抖动。