内核网络协议栈测试: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 开销和内存带宽。

数据封装与解封装流程 发送端 应用数据 (Data) TCP 头 + 应用数据 IP 头 + TCP 头 + 应用数据 以太网头 + IP 头 + TCP 头 + 应用数据 + FCS 接收端 以太网头 + IP 头 + TCP 头 + 应用数据 + FCS

2.2 内核网络子系统架构

Linux 网络子系统是一个庞大的模块化体系,从系统调用到底层驱动,主要分为以下核心路径:

Linux 内核网络子系统架构 用户态应用 (Socket API) 系统调用层 (sys_socket, sys_sendmsg) 协议无关层 (struct sock, inet_sock) TCP 协议栈 UDP 协议栈 SCTP/RAW 网络层 (IP 路由、邻居子系统) 邻居子系统 (ARP/ND) Netfilter 钩子 流量控制 (TC) 网络设备层 (NAPI, 网卡驱动)
性能关键路径:在压测场景中,数据包在传输层(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 指针,无需拷贝数据。

sk_buff 缓冲区指针布局 线性缓冲区 (Linear Buffer) 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 工作流程

  1. 中断触发:网卡收到数据包,触发硬件中断。
  2. 关闭中断:中断处理程序关闭该网卡的中断,将设备加入 NAPI 轮询列表。
  3. 软中断调度:触发 NET_RX_SOFTIRQ 软中断,唤醒 ksoftirqd 线程。
  4. 轮询收包:NAPI 轮询函数从网卡 RX 队列批量读取数据包,送入协议栈。
  5. 退出条件:当收包数量达到预算(budget)或队列为空时,重新开启中断,退出轮询。
NAPI 收包流程 网卡硬件中断 关闭网卡中断 加入 NAPI 列表 NET_RX_SOFTIRQ ksoftirqd 轮询 批量收包 (poll) 预算耗尽? 是 否 重新开启中断 退出轮询

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 可能引入额外的延迟抖动。