土法炼钢兴趣小组的算法知识备份

【Linux 网络子系统深度拆解】分段卸载:GRO/GSO/TSO 的内核实现与陷阱

文章导航

分类入口
linuxnetworking
标签入口
#kernel#GRO#GSO#TSO#LRO#offload#NAPI#skb_shared_info#netdev_features

目录

一个 10Gbps 网口每秒要处理约 812 万个 1500 字节包。如果每个包独立走一遍协议栈——分配 sk_buff、查路由、过 Netfilter、递交 socket——CPU 根本跟不上。内核的解决思路是批量化:收包时把多个小包”聚合”成一个大包再递交协议栈(GRO),发包时把一个大包”延迟”到尽可能晚才拆分成小包(GSO/TSO)。这两个方向的卸载机制极大减少了协议栈的遍历次数,是现代 Linux 网络性能的基石。

但卸载不是免费午餐。GRO 聚合后的超大包穿过 Netfilter 时,conntrack 看到的是一个”假包”;GSO 延迟分段意味着 TC qdisc 对单个巨大包做整形而非真实小包;隧道设备的二次封装让 GRO/GSO 的 offload 路径更加复杂。理解这些机制的内核实现,才能在性能调优和故障排查中避开陷阱。

本文基于 Linux 6.6/6.8 源码,从数据结构、函数调用链、可观测手段三个维度全面拆解分段卸载。

GRO/GSO/TSO 卸载全景

一、核心数据结构

分段卸载的所有元数据都存在 sk_buff 尾部的 skb_shared_info 中。理解这个结构是后续所有分析的基础。

1.1 skb_shared_info 中的 GSO 字段

// include/linux/skbuff.h
struct skb_shared_info {
    // ... 其他字段
    unsigned short  gso_size;    /* 每个分段的 MSS(不含头部) */
    unsigned short  gso_segs;    /* 分段数量 */
    unsigned int    gso_type;    /* SKB_GSO_* 类型位图 */
    struct sk_buff  *frag_list;  /* GRO 聚合链 / GSO 分段链 */
    skb_frag_t      frags[];     /* page fragment 数组 */
};

gso_sizegso_segs 共同描述这个 sk_buff “代表”多少个真实网络包:

1.2 SKB_GSO_* 类型枚举

// include/linux/skbuff.h
enum {
    SKB_GSO_TCPV4       = 1 << 0,   /* TCP/IPv4 分段 */
    SKB_GSO_DODGY       = 1 << 1,   /* 不可信来源(虚拟设备) */
    SKB_GSO_TCP_ECN     = 1 << 2,   /* ECN 感知 */
    SKB_GSO_TCP_FIXEDID = 1 << 3,   /* 固定 IP ID */
    SKB_GSO_TCPV6       = 1 << 4,   /* TCP/IPv6 分段 */
    SKB_GSO_FCOE        = 1 << 5,   /* FCoE */
    SKB_GSO_GRE         = 1 << 6,   /* GRE 隧道 */
    SKB_GSO_GRE_CSUM    = 1 << 7,   /* GRE + 校验和 */
    SKB_GSO_IPXIP4      = 1 << 8,   /* IP-in-IPv4 */
    SKB_GSO_IPXIP6      = 1 << 9,   /* IP-in-IPv6 */
    SKB_GSO_UDP_TUNNEL   = 1 << 10, /* UDP 隧道(VXLAN 等) */
    SKB_GSO_UDP_TUNNEL_CSUM = 1 << 11,
    SKB_GSO_PARTIAL     = 1 << 12,  /* 硬件只分段内层 L4 */
    SKB_GSO_TUNNEL_REMCSUM = 1 << 13,
    SKB_GSO_SCTP        = 1 << 14,  /* SCTP 分段 */
    SKB_GSO_ESP         = 1 << 15,  /* IPsec ESP */
    SKB_GSO_UDP_L4      = 1 << 16,  /* UDP 载荷分段 */
    SKB_GSO_FRAGLIST    = 1 << 17,  /* frag_list 聚合 */
};

这个枚举与 NETIF_F_GSO_* 特性标志一一对应,内核在编译时用 BUILD_BUG_ON() 强制校验二者的位偏移一致。

1.3 NETIF_F_* 特性协商

// include/linux/netdev_features.h
// 设备能力标志(部分)
NETIF_F_TSO         // TCP IPv4 分段
NETIF_F_TSO6        // TCP IPv6 分段
NETIF_F_TSO_ECN     // ECN 感知 TSO
NETIF_F_GSO         // 软件 GSO
NETIF_F_GRO         // 软件 GRO
NETIF_F_GRO_HW      // 硬件 GRO
NETIF_F_LRO         // Large Receive Offload(已废弃)
NETIF_F_GSO_PARTIAL // 部分硬件卸载
NETIF_F_GSO_UDP_L4  // UDP 载荷 GSO

设备通过三组标志管理 offload 能力:

字段 含义
dev->hw_features 硬件支持的能力(只读)
dev->features 当前生效的能力
dev->wanted_features 用户期望(ethtool -K 设置)

生效能力 = wanted_features & hw_features,再经过 ndo_fix_features() 驱动回调修正。例如,开启转发时驱动会自动关闭 LRO。

二、GRO:收包聚合引擎

GRO(Generic Receive Offload)在 NAPI 轮询阶段将同一条流的多个小包聚合为一个大 sk_buff,减少协议栈遍历次数。

2.1 GRO 入口与哈希表

驱动调用 napi_gro_receive() 将 skb 送入 GRO 引擎:

// include/net/gro.h / net/core/gro.c
gro_result_t napi_gro_receive(struct napi_struct *napi,
                              struct sk_buff *skb)
{
    // 1. 计算 skb 哈希
    // 2. 在 napi->gro_hash[bucket] 中查找同流 skb
    // 3. 找到 → skb_gro_receive() 聚合
    // 4. 未找到 → 新建链表项
    // 5. 超过 MAX_GRO_SKBS → flush 最旧的
}

Linux 6.x 用 8 桶哈希表GRO_HASH_BUCKETS = 8)替代了早期的单链表,将流匹配从 O(n) 优化到 O(1):

// include/linux/netdevice.h
#define GRO_HASH_BUCKETS  8

struct gro_list {
    struct list_head  list;
    int               count;
};

struct napi_struct {
    // ...
    struct gro_list   gro_hash[GRO_HASH_BUCKETS];
    // ...
};

2.2 napi_gro_cb:每包 GRO 元数据

每个进入 GRO 的 skb 都携带一个控制块,存储在 skb->cb[] 空间中:

// include/net/gro.h
struct napi_gro_cb {
    void      *frag0;          /* 第一个 fragment 的虚拟地址 */
    unsigned int frag0_len;    /* frag0 可用长度 */
    int       data_offset;     /* 当前层的数据偏移 */
    u16       flush;           /* 非零则不聚合,直接递交 */
    u16       flush_id;        /* IP ID 连续性校验 */
    u16       count;           /* 已聚合的分段数 */
    u16       proto;           /* 传输层协议 */

    unsigned long age;         /* 进入 GRO 的时间戳 */

    /* 位域 */
    u16       same_flow:1,     /* 与已有条目属同一流 */
              encap_mark:1,    /* 经过隧道解封装 */
              csum_valid:1,    /* 硬件校验和已验证 */
              csum_cnt:3,      /* 待验证的校验和层数 */
              is_flist:1,      /* frag_list 模式聚合 */
              recursion_counter:4; /* 隧道嵌套深度限制 */
};

关键字段解读:

2.3 协议层 GRO 回调链

GRO 采用分层回调架构,每一层协议提供 gro_receivegro_complete 回调:

napi_gro_receive()
  → dev_gro_receive()
    → inet_gro_receive()        // IP 层:校验 IP 头、IP ID
      → tcp4_gro_receive()      // TCP 层:校验五元组、序号
        → skb_gro_receive()     // 实际聚合

每层的 gro_receive 负责:

  1. 解析该层头部
  2. 设置 same_flow(五元组/协议匹配)
  3. 设置 flush(不可聚合条件)
  4. 调用下一层 gro_receive

TCP 层的关键判断:

// net/ipv4/tcp_offload.c (概念性)
static struct sk_buff *tcp4_gro_receive(struct list_head *head,
                                        struct sk_buff *skb)
{
    // 检查源/目的端口、序号连续性
    // 检查 TCP 标志:PSH/FIN/RST/SYN 中任一置位 → flush
    // 检查窗口大小是否变化
    // 检查 TCP 选项(timestamp)是否兼容
    // 通过 → same_flow = 1
}

2.4 skb_gro_receive():聚合引擎

当协议栈确认两个 skb 属同一流后,skb_gro_receive() 执行实际的数据合并:

// net/core/skbuff.c
int skb_gro_receive(struct sk_buff *p, struct sk_buff *skb)
{
    struct skb_shared_info *pinfo = skb_shinfo(p);
    struct skb_shared_info *skbinfo = skb_shinfo(skb);

    // 策略 1:page fragment 追加
    // 如果 p 的 frags[] 还有空间,直接把 skb 的数据页追加
    // 这是零拷贝路径,最高效

    // 策略 2:frag_list 链接
    // 如果 frags[] 满或数据在 linear 区域
    // 把 skb 挂到 p->frag_list 链表

    // 更新元数据
    p->len += skb->len;
    p->data_len += skb->len;
    p->truesize += skb->truesize;
    NAPI_GRO_CB(p)->count++;

    return 0;
}

聚合模式选择:

模式 条件 优点 缺点
frags[] 追加 skb 数据在 page fragment 零拷贝,GSO 分段高效 frags[] 有上限(17 个)
frag_list 链 linear 数据或 frags[] 满 无上限 GSO 分段时需遍历链表

2.5 GRO flush 时机

GRO 聚合不能无限等待,以下条件触发 flush(将聚合包递交协议栈):

  1. NAPI 轮询结束napi_complete_done()napi_gro_flush() 清空所有 GRO 链
  2. 超时gro_flush_timeout(sysctl 可配,默认 0 = 仅在 NAPI 结束时 flush)
  3. 聚合大小上限gro_max_size(默认 65536,6.x 可设到 524280)
  4. 不兼容包到达:同流但 TCP 标志变化(如 FIN)触发 flush
  5. 非同流包占满:GRO 链表达到 MAX_GRO_SKBS(默认 8),最旧的被 flush
# 查看/调整 GRO flush 超时
sysctl net.core.gro_flush_timeout
# 配合 napi_defer_hard_irqs 使用效果更好
sysctl net.core.napi_defer_hard_irqs

napi_defer_hard_irqs 是 6.x 引入的重要优化:让 NAPI 轮询在没有新包时不立即重新使能中断,而是延迟若干次,给 GRO 更多时间积累同流包。这与 gro_flush_timeout 配合,可以显著提升 GRO 聚合率。

2.6 GRO 递交:gro_complete 回调

当 GRO 包被 flush 时,按协议层逆序调用 gro_complete 回调:

napi_gro_complete()
  → inet_gro_complete()
    → tcp4_gro_complete()
      → 设置 skb_shinfo(skb)->gso_size = MSS
      → 设置 skb_shinfo(skb)->gso_type = SKB_GSO_TCPV4
      → 设置 skb_shinfo(skb)->gso_segs = count
  → netif_receive_skb()  // 进入正常收包路径

关键点:GRO 递交的包携带了 GSO 元数据(gso_size/gso_type),这使得后续如果需要转发(而非本地递交),可以用 GSO 重新分段,而无需回到原始小包。

三、GSO:延迟分段引擎

GSO(Generic Segmentation Offload)的核心思想是:尽可能晚地把大包拆成小包。如果硬件支持 TSO,大包直接交给网卡分段;如果不支持,在 dev_queue_xmit() 的最后一步才用软件分段。

3.1 GSO 触发判断

发包路径中,dev_queue_xmit() 在实际发送前检查是否需要软件 GSO:

// net/core/dev.c (概念性)
static int __dev_queue_xmit(struct sk_buff *skb, ...)
{
    // ...
    if (netif_needs_gso(skb, features)) {
        // 硬件不支持该 GSO 类型 → 软件分段
        skb = __skb_gso_segment(skb, features, tx_path);
    }
    // ...
}

netif_needs_gso() 的判断逻辑:

// include/linux/netdevice.h
static inline bool netif_needs_gso(struct sk_buff *skb,
                                   netdev_features_t features)
{
    return skb_is_gso(skb) &&              // 包标记了 GSO
           (!skb_gso_ok(skb, features) ||  // 硬件不支持该类型
            unlikely(skb->ip_summed != CHECKSUM_PARTIAL));
}

简而言之:如果包标记了 GSO,且硬件不支持对应的 SKB_GSO_* 类型,就需要软件 GSO。

3.2 __skb_gso_segment():分段核心

// net/core/gso.c
struct sk_buff *__skb_gso_segment(struct sk_buff *skb,
                                  netdev_features_t features,
                                  bool tx_path)
{
    struct sk_buff *segs;

    // 1. 克隆 skb(保留原件用于错误回退)
    // 2. 调用协议层回调 gso_segment
    segs = skb_mac_gso_segment(skb, features);
    // → inet_gso_segment()
    //   → tcp4_gso_segment()

    return segs;  // 返回分段链表
}

TCP 的 GSO 分段逻辑:

// net/ipv4/tcp_offload.c (概念性)
struct sk_buff *tcp4_gso_segment(struct sk_buff *skb,
                                 netdev_features_t features)
{
    unsigned int mss = skb_shinfo(skb)->gso_size;

    // 按 mss 拆分数据:
    // 1. 每 mss 字节创建一个新 skb
    // 2. 每个 skb 独立设置 TCP 头(序号、校验和)
    // 3. 每个 skb 独立设置 IP 头(ID、长度、校验和)
    // 4. 返回 skb 链表(通过 skb->next 链接)
}

GSO 分段结果:

输入:1 个大 skb(len=45000, gso_size=1460, gso_segs=31)
输出:31 个小 skb 链表,每个 len ≈ 1460 + 头部
      skb->next → skb->next → ... → NULL

3.3 GSO_PARTIAL:部分硬件卸载

现代网卡常常只支持对内层 L4 头做硬件分段,外层封装(VXLAN/GRE 头)需要软件处理。GSO_PARTIAL 解决这个问题:

// 驱动声明支持 PARTIAL
dev->hw_features |= NETIF_F_GSO_PARTIAL;
dev->gso_partial_features = NETIF_F_GSO_GRE |
                            NETIF_F_GSO_UDP_TUNNEL;

工作流程:

  1. 软件 GSO 只拆分外层头(每个分段独立的外层 IP/UDP/VXLAN 头)
  2. 内层 TCP 保持大包形态,gso_size 保留
  3. 网卡硬件对内层 TCP 做最终分段

这大幅减少了 CPU 开销:软件只处理少量外层头,繁重的内层分段交给硬件。

3.4 UDP GSO(GSO_UDP_L4)

Linux 4.18 引入 UDP GSO,允许应用一次 sendmsg() 发送多个 UDP 报文:

// 用户态通过 cmsg 指定分段大小
struct msghdr msg;
int gso_size = 1472;  // UDP 载荷大小
// 设置 UDP_SEGMENT cmsg
// 发送 N * gso_size 字节

// 内核路径:
// udp_sendmsg() → 创建大 skb
//   gso_type = SKB_GSO_UDP_L4
//   gso_size = 1472
// dev_queue_xmit() → 软件/硬件分段

UDP GSO 对 QUIC 等协议的性能提升显著:一次系统调用发送多个数据报,减少 socket 锁竞争和协议栈遍历。

四、TSO:硬件分段卸载

TSO(TCP Segmentation Offload)是 GSO 的硬件加速版。当网卡支持 NETIF_F_TSO 时,内核直接把大 TCP 包(最大可达 64KB 甚至更大)交给网卡,由网卡硬件完成分段。

4.1 TSO 设置路径

TCP 发送路径中,tcp_write_xmit() 构建大 skb:

// net/ipv4/tcp_output.c (概念性)
static int tcp_write_xmit(struct sock *sk, unsigned int mss_now,
                           int nonagle, int push_one, gfp_t gfp)
{
    // mss_now = TCP MSS(通常 1460)
    // tso_segs = min(skb->len / mss_now, tso_max_segs)

    while ((skb = tcp_send_head(sk))) {
        unsigned int limit = tcp_mss_split_point(sk, skb, mss_now,
                                                  nonagle, cwnd);
        // limit 可远大于 mss_now(受 cwnd、TSQ 限制)

        // 设置 GSO 元数据
        skb_shinfo(skb)->gso_size = mss_now;
        skb_shinfo(skb)->gso_segs = DIV_ROUND_UP(skb->len, mss_now);
        skb_shinfo(skb)->gso_type = SKB_GSO_TCPV4;  // 或 TCPV6

        tcp_transmit_skb(sk, skb, ...);
    }
}

4.2 TSO 尺寸限制

// include/linux/netdevice.h
struct net_device {
    unsigned int   gso_max_size;      /* 通用 GSO/TSO 最大字节数 */
    unsigned int   gso_ipv4_max_size; /* IPv4 专用限制 */
    u16            gso_max_segs;      /* 单个 GSO 包最大分段数 */
    u16            tso_max_size;      /* TSO 专用限制 */
    u16            tso_max_segs;      /* TSO 最大分段数 */
};

// 驱动通过以下 API 声明限制
netif_set_tso_max_size(dev, 65536);
netif_set_tso_max_segs(dev, 64);

TCP 层的 tcp_tso_segs() 综合考虑:

4.3 TSO 与 TCP pacing 的交互

TSO 的一个”陷阱”是微突发(micro-burst):一个 64KB 的 TSO 包到达网卡后,网卡会在极短时间内以线速发出 44 个 1500 字节包,瞬间占满交换机缓冲区。

内核的解决方案是 TSQ + pacing

// TCP Small Queues (TSQ)
// net/ipv4/tcp_output.c
// 限制每个 socket 在 qdisc + 驱动队列中的数据量
sysctl_tcp_limit_output_bytes  // 默认 2 × 64KB

// FQ qdisc pacing
// net/sched/sch_fq.c
// 根据 sk->sk_pacing_rate 控制发送时机
// 每个 TCP 流独立 pacing

最佳实践组合:

机制 作用 默认值
TSO 减少 CPU 开销 开启
TSQ 限制 socket 排队深度 2 × 64KB
FQ qdisc 精确 pacing 需手动配置
tcp_min_tso_segs 小窗口时保持 TSO 效率 2
# 推荐配置:TSO + FQ pacing
tc qdisc replace dev eth0 root fq
ethtool -K eth0 tso on gso on

五、LRO 的废弃与教训

LRO(Large Receive Offload)是 GRO 的前身,由网卡硬件完成收包聚合。它被废弃的原因是一个深刻的架构教训。

5.1 LRO 的致命缺陷

LRO 在网卡固件中聚合 TCP 包时,重写了 TCP/IP 头部

原始包 1: IP.id=100, TCP.seq=1000, TCP.ack=5000, TCP.window=32768
原始包 2: IP.id=101, TCP.seq=2460, TCP.ack=5000, TCP.window=32768
原始包 3: IP.id=102, TCP.seq=3920, TCP.ack=5200, TCP.window=65535

LRO 聚合后: IP.id=100, TCP.seq=1000, TCP.ack=5200, TCP.window=65535
             (中间包的 ack/window 信息丢失)

这导致三个问题:

  1. 破坏转发:聚合后的包不是合法的 IP 包,不能被路由转发
  2. 破坏 Netfilter:conntrack 看到的 ack/window 值不正确
  3. 破坏桥接:bridge 无法正确处理聚合包

因此内核强制规则:开启 ip_forward 时自动关闭 LRO

5.2 GRO 的设计改进

GRO 的核心改进是不修改原始头部

特性 LRO GRO
实现位置 网卡固件 内核软件
头部保真 重写头部 保留原始头部
可逆性 不可逆 可通过 GSO 还原
转发兼容 不兼容 完全兼容
协议支持 仅 TCP TCP/UDP/隧道/…
灵活性 固件固定 可通过回调扩展

GRO 聚合后的包可以通过 GSO 重新分段,恢复出与原始包等价的包序列。这是 GRO 能用于转发路径的关键。

六、隧道设备的卸载处理

隧道(VXLAN、GRE、IPsec 等)给卸载带来了额外复杂性:外层和内层各自需要独立的 GRO/GSO 处理。

6.1 gro_cells:虚拟设备的 GRO 中转

隧道设备解封装后,内层包需要重新走 GRO。内核提供 gro_cells 机制:

// include/net/gro_cells.h
struct gro_cells {
    struct gro_cell __percpu *cells;
};

struct gro_cell {
    struct sk_buff_head  napi_skbs;
    struct napi_struct   napi;
};

工作流程:

物理网卡 GRO → 聚合外层包
  → 隧道驱动解封装(剥离 VXLAN/GRE 头)
    → gro_cells_receive()
      → 内层包进入 per-CPU 的 gro_cell
        → 独立 NAPI 实例做内层 GRO
          → 内层聚合包递交协议栈

使用 gro_cells 的设备:

6.2 隧道 GSO 分段路径

发包方向,隧道设备的 GSO 需要处理多层封装:

tcp_write_xmit() → 大 TCP skb (gso_type = SKB_GSO_TCPV4)
  → 隧道设备 xmit
    → 添加外层头(VXLAN/GRE + 外层 IP/UDP)
    → gso_type |= SKB_GSO_UDP_TUNNEL  // 标记隧道封装
  → dev_queue_xmit() → 物理网卡
    → netif_needs_gso() 检查
    → 如果网卡支持隧道 TSO:直接硬件分段
    → 否则:GSO_PARTIAL 或完全软件分段

6.3 隧道 offload 的常见陷阱

陷阱 1:MTU 与 GSO 的不匹配

# VXLAN 封装增加 50 字节开销
# 内层 MTU 1450,但 GRO 聚合后内层包可达 65536
# 如果物理网卡不支持隧道 TSO,软件 GSO 按内层 MTU 分段
# 每个分段都要独立封装外层头 → CPU 开销倍增

陷阱 2:校验和卸载的层次

# 内层 TCP 校验和 → CHECKSUM_PARTIAL(硬件计算)
# 外层 UDP 校验和 → 取决于网卡能力
# 不匹配时回退到软件校验 → 性能下降
ethtool -k eth0 | grep tx-udp_tnl-segmentation

陷阱 3:GRO 与隧道嵌套

napi_gro_cb.recursion_counter 限制最多 15 层隧道嵌套 GRO。超过后强制回退到普通收包路径。实际中超过 2 层隧道就应该检查架构设计。

七、对 Netfilter 与 TC 的影响

GRO/GSO 对 Netfilter 和 TC 的行为有深远影响,是运维中最容易踩坑的地方。

7.1 GRO 与 conntrack

GRO 聚合发生在 Netfilter 之前(napi_gro_receive()netif_receive_skb() 之前)。这意味着:

时间线:
  GRO 聚合 → netif_receive_skb() → NF_INET_PRE_ROUTING (conntrack)
                                  → NF_INET_LOCAL_IN

conntrack 看到的是:
  一个 len=65536 的"假包",而非 45 个 1460 字节的真包

影响:

7.2 GSO 与 TC qdisc

TC qdisc 看到的是 GSO 大包,不是真实小包:

问题场景:
  HTB 限速 100Mbps,GSO 大包 65536 字节
  → HTB 把这个大包当作一个"token"消耗
  → 65536 字节瞬间发出 → 微突发
  → 然后等待 token 补充 → 延迟增大

解决:
  方案 1:在 TC 中使用 GSO 感知的 qdisc(如 fq_codel、fq)
  方案 2:手动在 TC 路径中触发 GSO 分段

FQ/FQ_Codel 等现代 qdisc 会在内部调用 skb_gso_segment() 将大包拆分后再排队,避免突发问题。但 HTB/TBF 等传统 qdisc 不会自动分段。

# 查看 qdisc 是否正确处理 GSO
tc -s qdisc show dev eth0
# 如果看到 backlog 中有超大包,可能是 GSO 未分段

# 推荐:fq_codel 作为叶子 qdisc
tc qdisc replace dev eth0 root handle 1: htb default 10
tc class add dev eth0 parent 1: classid 1:10 htb rate 100mbit
tc qdisc add dev eth0 parent 1:10 fq_codel

7.3 GSO 重新分段点

在转发路径中,GSO 分段可能在多个位置发生:

GRO 聚合包 → ip_forward()
  → NF_INET_FORWARD (Netfilter)    // 操作 GSO 大包
  → ip_output()
    → NF_INET_POST_ROUTING         // 仍然是大包
      → dev_queue_xmit()
        → netif_needs_gso()        // 在这里决定是否分段
          → 支持 TSO → 直接发送(硬件分段)
          → 不支持 → __skb_gso_segment() → 分段后逐个发送

关键理解:Netfilter 的所有钩子看到的都是 GSO 大包。如果 Netfilter 规则依赖包大小判断(如 -m length),需要注意 GRO/GSO 的影响。

八、可观测性实战

8.1 查看 offload 状态

# 查看设备 offload 能力
ethtool -k eth0 | grep -E '(generic-receive-offload|generic-segmentation|tcp-segmentation|large-receive)'
# generic-receive-offload: on       → GRO
# generic-segmentation-offload: on  → GSO
# tcp-segmentation-offload: on      → TSO
# large-receive-offload: off        → LRO(应保持关闭)

# 查看隧道 offload
ethtool -k eth0 | grep tunnel
# tx-udp_tnl-segmentation: on
# tx-udp_tnl-csum-segmentation: on

# 查看 GSO 最大值
cat /sys/class/net/eth0/gso_max_size
cat /sys/class/net/eth0/gro_max_size
cat /sys/class/net/eth0/tso_max_size

8.2 bpftrace 追踪 GRO 聚合效果

# 追踪 GRO 聚合后的分段数分布
bpftrace -e '
kprobe:napi_gro_complete {
    $skb = (struct sk_buff *)arg1;
    $shinfo = (struct skb_shared_info *)($skb->head + $skb->end);
    @gro_segs = hist($shinfo->gso_segs);
}
'

# 追踪 GRO flush 原因
bpftrace -e '
kprobe:napi_gro_flush {
    @flush_count = count();
}
kprobe:napi_gro_receive {
    @receive_count = count();
}
interval:s:1 {
    printf("GRO receive: %lld, flush: %lld, ratio: ",
           @receive_count, @flush_count);
    print(@gro_segs);
    clear(@receive_count);
    clear(@flush_count);
}
'

8.3 bpftrace 追踪 GSO 分段

# 追踪软件 GSO 分段事件(应尽量少,说明硬件 TSO 在工作)
bpftrace -e '
kprobe:__skb_gso_segment {
    $skb = (struct sk_buff *)arg0;
    @gso_segment_count = count();
    @gso_segment_dev[str($skb->dev->name)] = count();
}
interval:s:5 {
    print(@gso_segment_count);
    print(@gso_segment_dev);
    clear(@gso_segment_count);
    clear(@gso_segment_dev);
}
'

如果物理网卡频繁触发 __skb_gso_segment(),说明 TSO 未生效或存在不匹配的 GSO 类型。

8.4 perf 分析 offload CPU 开销

# 对比开启/关闭 TSO 的 CPU 开销
ethtool -K eth0 tso off
perf stat -e cycles,instructions -a -- sleep 10
# 记录关闭 TSO 时的 cycles

ethtool -K eth0 tso on
perf stat -e cycles,instructions -a -- sleep 10
# 对比开启 TSO 时的 cycles

# GRO 开销分析
perf record -g -a -e cycles -- sleep 10
perf report --no-children | grep -E '(gro_receive|gro_complete|gso_segment)'

8.5 监控 GRO 大小分布

# 统计 GRO 递交包的大小分布
bpftrace -e '
kprobe:netif_receive_skb_list_internal {
    // 这是 GRO flush 后的路径
}
kprobe:netif_receive_skb_core {
    $skb = (struct sk_buff *)arg0;
    @pkt_len = hist($skb->len);
}
interval:s:10 {
    print(@pkt_len);
    clear(@pkt_len);
}
'

正常情况下应该看到双峰分布:大量 > 10KB 的 GRO 聚合包,和少量无法聚合的小包(如 ACK、SYN)。

九、调优参数速查

参数 默认值 说明
ethtool -K eth0 gro on/off on 启用/禁用 GRO
ethtool -K eth0 tso on/off on 启用/禁用 TSO
ethtool -K eth0 gso on/off on 启用/禁用 GSO
ethtool -K eth0 lro off off LRO 应保持关闭
/sys/class/net/eth0/gro_max_size 65536 GRO 聚合最大字节数
/sys/class/net/eth0/gso_max_size 65536 GSO 单包最大字节数
net.core.gro_flush_timeout 0 GRO flush 超时(纳秒)
net.core.napi_defer_hard_irqs 0 延迟中断重使能次数
net.ipv4.tcp_min_tso_segs 2 TSO 最小分段数
net.ipv4.tcp_limit_output_bytes 131072 TSQ 每 socket 排队上限
net.ipv4.tcp_tso_win_divisor 3 TSO 占用窗口比例限制

推荐调优组合

高吞吐场景(数据库、大文件传输):

ethtool -K eth0 tso on gso on gro on
echo 524280 > /sys/class/net/eth0/gro_max_size
sysctl -w net.core.gro_flush_timeout=20000
sysctl -w net.core.napi_defer_hard_irqs=10

低延迟场景(交易系统、实时通信):

ethtool -K eth0 tso on gso on gro on
echo 65536 > /sys/class/net/eth0/gro_max_size  # 保守聚合
sysctl -w net.core.gro_flush_timeout=0           # 不额外等待
sysctl -w net.core.napi_defer_hard_irqs=0

转发场景(路由器、负载均衡器):

ethtool -K eth0 tso on gso on gro on lro off
# 确保 LRO 关闭!GRO + GSO 组合才能正确转发
# 检查隧道 TSO 支持
ethtool -k eth0 | grep tnl

十、总结

机制 方向 位置 核心函数 性能收益
GRO 收包 NAPI 轮询 napi_gro_receive() 减少协议栈遍历
GSO 发包 dev_queue_xmit() __skb_gso_segment() 延迟分段,减少头部开销
TSO 发包 网卡硬件 驱动 ndo_start_xmit() CPU 零开销分段
LRO 收包 网卡固件 已废弃 被 GRO 取代

分段卸载的核心原则是尽可能保持大包形态穿越协议栈

这减少了每包开销(分配 sk_buff、查路由、查 conntrack 等),但也意味着协议栈中间环节看到的是”不真实的”大包。理解这一点,是避免 Netfilter 规则误判、TC 整形突发、隧道性能下降等陷阱的关键。

参考文献

  1. Linux 内核源码 include/net/gro.hinclude/net/gso.h(GRO/GSO 核心实现)
  2. Linux 内核源码 include/linux/skbuff.h(skb_shared_info GSO 字段)
  3. Linux 内核源码 include/linux/netdev_features.h(NETIF_F_* 特性标志)
  4. Jonathan Corbet, “Generic receive offload”, LWN.net, 2007
  5. Jesper Dangaard Brouer, “XDP - eXpress Data Path”, Kernel Recipes, 2018
  6. Jakub Kicinski, “GRO improvements in recent kernels”, Netdev Conference, 2023

上一篇多队列与流量分发:RSS/RPS/RFS/XPS

下一篇网络子系统内存管理:sk_buff 分配、page pool 与 NUMA

同主题继续阅读

把当前热点继续串成多页阅读,而不是停在单篇消费。

2026-04-20 · linux / networking

【Linux 网络子系统深度拆解】发包路径全解:从 send() 到网线

一个用户态 send() 调用要走过 TCP 分段、IP 路由、Netfilter 钩子、Qdisc 排队、GSO 分段、驱动 DMA 映射六个阶段才能把数据送上网线。本文从 Linux 6.6 内核源码出发,逐函数拆解完整的 TX 发包路径,深入 TSQ 限流、Qdisc 调度、BQL 防膨胀、GSO/TSO 分段决策等核心机制。

2026-04-20 · linux / networking

【Linux 网络子系统深度拆解】收包路径全解:从 NIC 中断到 socket 接收队列

一个网络包从网卡 DMA 到用户态 recvmsg(),要走过硬中断、NAPI 轮询、GRO 聚合、协议分发、IP 路由、Netfilter 钩子、TCP/UDP 处理、socket 队列八个阶段。本文从 Linux 6.6 内核源码出发,逐函数拆解完整的 RX 收包路径,量化每一跳的 CPU 开销,并用 bpftrace 实测各阶段延迟分布。

2026-04-20 · linux / networking

【Linux 网络子系统深度拆解】sk_buff 全解:内核网络包的终极容器

sk_buff 是 Linux 内核网络栈的通用货币——每一个收到或发出的网络包,都必须装在这个容器里走完全程。本文从 Linux 6.6 内核源码出发,拆解 sk_buff 的内存布局、四大指针操作、clone 与 copy 的代价差异、skb_shared_info 的 fragment 机制,并用 bpftrace 实测 sk_buff 分配热点和生命周期。


By .