写在前面

sk_buff 是 Linux 网络栈最核心、也最容易被误解的数据结构之一。它并不是“装着一个完整网络包的结构体”,而是贯穿驱动、协议栈、队列、Socket、Netfilter、tc、BPF 与硬件 offload 的控制对象和所有权载体

把 GRO/GSO/TSO、checksum 四态以及 RSS/RPS/RFS/XPS 放在同一性能模型下,解释 skb 元数据如何替代重复解析和拷贝。

本文以本地 Linux 5.10.209 源码为主线;涉及演进时对照 Linux 6.1.99,涉及网卡驱动时结合 Firefly RK3588 SDK Linux 5.10.198。本文是源码分析,不把尚未执行的 QEMU、tracepoint 或开发板实验写成已验证结论。

Linux sk_buff 深度解析(五):GRO、GSO、Checksum 与多核流调度

阅读前先建立四个问题

阅读任何 skb 代码路径,都建议反复追问:

  1. 当前 skb->data 指向哪一层协议头,linear 与 non-linear 数据分别在哪里?
  2. 哪些字段刚被修改,下一层为什么依赖这些字段?
  3. 当前谁持有 skb shell、head、frag page 以及 socket/dst/conntrack 等外部引用?
  4. 成功、失败、重试、redirect 和丢弃分支分别由谁消费或释放 skb?

本篇核心结论

  • GRO 在 RX 聚合,GSO/TSO 在 TX 延迟分段,二者通过 shared info 元数据衔接。
  • ip_summed 表达 checksum 的责任归属,而不是简单的“正确/错误”。
  • hash、CPU 和 queue mapping 把包内容映射为处理位置与发送队列。
  • offload 的本质是延后工作或把工作交给硬件,但软件始终要维护可验证的语义。

1. GRO、GSO 与 TSO 的对称关系

结论摘要

GRO 在 RX 方向把同流小包聚合成大 skb,减少后续协议栈每包开销;GSO 在 TX 方向用一个大 skb 描述多个线速报文,把分段推迟到软件出口或网卡;TSO 是 TCP GSO 的硬件执行形式。三者都依赖 skb 的非线性布局、header offset、checksum 状态和 skb_shared_info 中的 GSO 元数据。

一、收发方向的对称关系

1
2
RX: many wire packets → GRO → one large skb → protocol stack
TX: one large skb → GSO/TSO → many wire packets

它们优化的不是字节复制本身,而是把固定 per-packet 成本摊薄:协议解析、路由、Netfilter、socket queue、qdisc、descriptor 等。

二、GRO 入口

Linux 5.10:

  • gro_list_prepare()kernel-5.10/net/core/dev.c:5877
  • dev_gro_receive()kernel-5.10/net/core/dev.c:5991
  • napi_gro_receive()kernel-5.10/net/core/dev.c:6166

驱动交入 skb 后,GRO 根据设备、协议、hash、header 和协议回调判断:

  • 是否与已有 skb 同流;
  • 当前包能否 merge;
  • 旧 flow 是否必须 flush;
  • 当前 skb 是 held、merged、normal 还是 drop。

三、napi_gro_cb

NAPI_GRO_CB(skb) overlay 在 skb->cb 上,保存:

  • same_flow
  • flush
  • free
  • count
  • age
  • last
  • checksum/GSO 相关临时状态。

GRO 期间 cb 属于 GRO;交给下一层后不能再假设这些字段有效。

四、GRO 合并后的数据形态

GRO 不只有一种布局:

  1. 把新数据页合入 head skb 的 frags[]
  2. 使用 frag_list 串联完整 skb;
  3. 调整 len/data_len/truesize
  4. 设置 GSO size/segs/type,让后续知道逻辑分段边界。

是否使用 frags 或 frag_list 取决于协议回调、输入 skb 布局、frag 数量、feature 和实现路径。不能把“GRO 后一定是 frag_list”当作通用结论。

五、flow 匹配与 flush

典型匹配条件包括:

  • 同一接收设备/虚拟设备语义;
  • L2/L3/L4 header 与 flow key 一致;
  • TCP seq 连续、flag 合法;
  • checksum 状态兼容;
  • 不超过 frag/GSO/长度限制;
  • 没有要求立即 flush 的协议事件。

TCP FIN/RST、序列不连续、header option 差异、超限、时间/批次结束等会触发 flush。

六、GSO 元数据

位于 skb_shared_info

  • gso_size:每个 segment 的 payload/MSS 规模;
  • gso_segs:预计 segment 数;
  • gso_type:TCPv4/TCPv6/UDP tunnel/partial/fraglist 等类型。

skb_is_gso() 通常依据 gso_size。大 skb 仍保持完整逻辑 len,网卡或软件分段器根据 header offset 和 GSO metadata 产生各 segment。

七、软件 GSO

关键路径:

1
2
3
4
5
validate_xmit_skb()
→ skb_gso_segment()/__skb_gso_segment()
→ protocol gso_segment callback
→ tcp_gso_segment()
→ skb_segment()

位置:

  • __skb_gso_segment()kernel-5.10/net/core/dev.c:3391
  • validate_xmit_skb()kernel-5.10/net/core/dev.c:3659
  • tcp_gso_segment()kernel-5.10/net/ipv4/tcp_offload.c:54
  • skb_segment()kernel-5.10/net/core/skbuff.c:3819

skb_segment() 尽量 clone/share page 数据并为每个 segment 构造独立 header,避免完整 payload copy。segment 链通过 skb next 返回。

八、TSO

如果设备 feature 支持对应 GSO type,网络栈可把大 skb 原样交给网卡:

  • descriptor 提供 MSS、header 长度、checksum 等;
  • 网卡生成多个 wire packet;
  • 驱动只管理一个逻辑 skb 的 DMA/completion。

若 feature 不支持,validate_xmit_skb() 先软件 GSO,再逐个 segment 发送。

九、checksum 关系

GSO skb 通常是 CHECKSUM_PARTIAL:header 中 checksum 字段留给软件分段器或网卡基于每个 segment 计算。GRO 输入 checksum 状态必须正确传播,否则聚合后一次错误标记会影响多个逻辑包。

十、5.10 与 6.1 GRO 组织差异

5.10 GRO 主实现位于 net/core/dev.c,NAPI 使用相对简单的 GRO list。6.1 把实现拆到 net/core/gro.c,并在 struct napi_struct 中使用:

1
struct gro_list gro_hash[GRO_HASH_BUCKETS];

GRO_HASH_BUCKETSkernel-6.1/include/linux/netdevice.h:339 为 8。先按 hash 分桶减少同一 list 的线性比较,改善多 flow 批次成本。

十一、关键不变量

  1. GSO type 必须与外层/内层协议和 feature 匹配。
  2. header offset 必须能定位每个 segment 需要复制/修改的 header。
  3. gso_size/gso_segs/len 必须一致或能由 helper 修正。
  4. frag 数不能超过上限。
  5. segment/merge 过程必须维护 page ref、truesize 和 checksum 状态。
  6. GRO merge 消费当前 skb 后,调用者不能继续访问。

十二、常见误区

  1. GRO 不是 LRO;GRO 是协议栈可控的软件聚合,保持更严格语义。
  2. GSO 不等于硬件 TSO;不支持时可软件分段。
  3. 大 skb 不代表线上存在一个超 MTU Ethernet frame。
  4. GRO 后不一定全部 linear,也不一定总是 frag_list。
  5. gso_segs 可能需要重新计算,不能在所有路径视作绝对可信。
  6. checksum offload 是 GSO 正确性的组成部分,不是独立附加功能。

字段与所有权统一核对表

对象或字段 主要修改者/使用者 所有权与释放要点
GRO cb/count GRO receive/complete 聚合链持有成员或合并后的 skb
gso_size/gso_segs/type 协议 GSO segment 前由大 skb 携带分段契约
frags/frag_list 聚合与分段 page/子 skb 引用随 segment 转移

最小调用链

1
2
3
4
5
小 skb 进入 GRO
→ flow 匹配并聚合
→ GRO complete 写 GSO 元数据
→ output/GSO segment
→ software 或 TSO 发送各 segment

2. Checksum 不是布尔值:四态责任模型

Checksum 不是布尔值:四态责任模型

结论摘要

ip_summed 不是“checksum 是否正确”的布尔值,而是网络栈与驱动之间的责任状态机。RX 常见 NONE、UNNECESSARY、COMPLETE;TX 常见 PARTIAL。csumcsum_start/csum_offset 通过 union 复用,具体解释由 ip_summed 决定。GSO、隧道和多层 checksum 又通过 csum_level、inner header 和 gso type 扩展该模型。

一、四个状态

定义:kernel-5.10/include/linux/skbuff.h:221-225

CHECKSUM_NONE

网络栈不能依赖硬件 checksum 结果。可能是:

  • 驱动未验证;
  • 协议不支持 offload;
  • 硬件报告失败/未知;
  • 状态被修改后失效。

L4 通常需要软件验证。

CHECKSUM_UNNECESSARY

驱动或上层已经确认协议 checksum 有效,协议栈无需再次计算。它不表示 skb->csum 保存了完整 checksum 数值,只表示验证责任已经完成。

CHECKSUM_COMPLETE

RX 硬件提供了从某范围计算的完整 checksum 值,保存在 skb->csum。协议层可结合 pseudo header 验证或在 pull/encapsulation 时增量调整。它与 UNNECESSARY 的区别是“有可继续使用的 checksum 数值”。

CHECKSUM_PARTIAL

TX 数据尚未完成 checksum,网卡或软件 helper 应从 csum_start 开始计算,并把结果写到 csum_start + csum_offset

二、union 解释

struct sk_buff 中:

1
2
3
4
5
6
7
union {
__wsum csum;
struct {
__u16 csum_start;
__u16 csum_offset;
};
};
  • RX COMPLETE 时解释为 csum
  • TX PARTIAL 时解释为 start/offset;
  • 不能同时把两种解释都当作有效。

csum_start 通常是相对 head 的 offset,不是相对当前 data 的稳定绝对地址。

三、RX 驱动责任

驱动只有在硬件明确验证相应协议和封装层时,才能设置 UNNECESSARY。常见限制:

  • 硬件只验证 IPv4 header,不代表 TCP/UDP checksum;
  • 分片、隧道、未知 extension header 可能使结果无效;
  • 错误状态应保持 NONE;
  • skb_checksum_none_assert() 只是调试断言当前为 NONE,不会把包变成已验证。

csum_level 表示连续已验证的额外 checksum 层数减一,用于隧道内外层,但最大值有限。

四、TX PARTIAL

典型设置:

1
2
3
ip_summed = CHECKSUM_PARTIAL
csum_start = transport header offset
csum_offset = checksum field 在 transport header 内偏移

skb_partial_csum_set() 位于 kernel-5.10/net/core/skbuff.c:4857,会检查 start/offset 是否落在线性 head 范围内,设置 transport header 和 PARTIAL 状态。对不可信输入必须校验,否则网卡 DMA 可能向越界位置写 checksum。

五、软件 fallback

skb_checksum_help() 位于 kernel-5.10/net/core/dev.c:3230。当设备不支持当前 checksum offload 时:

  1. 确保 checksum 写入位置可写/线性;
  2. 软件计算 checksum;
  3. 写入协议 checksum 字段;
  4. 把状态转换为不再需要硬件处理。

skb_csum_hwoffload_help() 位于 kernel-5.10/net/core/dev.c:3648,结合设备 features 决定继续硬件 offload 或软件 fallback。

六、pull/push 与 checksum

改变 data 视图时,COMPLETE checksum 可能需要通过增量 helper 调整已经移除/加入的字节范围。直接修改 packet bytes 则会使原 checksum 状态失效,必须:

  • 增量更新 checksum;
  • 或降级为 NONE/重新计算;
  • PARTIAL 时更新 start/offset 和 header offset。

因此修改 header 的 NAT、tc/BPF、隧道代码通常同时调用 checksum helper。

七、GSO/TSO

GSO skb 通常保持 PARTIAL。每个 segment 的 TCP/UDP length、sequence、IP length 等不同,分段器或网卡基于 segment header 计算最终 checksum。

设备是否支持某个 GSO type,不自动意味着支持所有封装 checksum;feature negotiation 必须同时匹配 outer/inner checksum 能力。

八、隧道 checksum

隧道可能同时存在:

  • outer UDP/IP checksum;
  • inner TCP/UDP checksum。

skb 通过 outer/inner header offset、encapsulation、GSO type、csum_level 和 PARTIAL 字段表达当前最外层待 offload checksum。部分硬件支持两层 offload,部分只支持一层,网络栈需要 fallback。

九、字段变化表

场景 ip_summed union 后续责任
普通 RX 未验证 NONE 不可信 L4 软件验证
RX 已验证 UNNECESSARY 通常不使用 L4 跳过重复验证
RX 完整和 COMPLETE csum 协议结合 pseudo header
TX offload PARTIAL start/offset 网卡或软件 helper
软件 fallback 完成 NONE/已完成语义 checksum 字段已写 驱动无需 offload

十、常见误区

  1. UNNECESSARY 不是 checksum 值为零。
  2. COMPLETE 不是“已经完全验证”,它提供可验证的和。
  3. PARTIAL 不是坏包,而是明确的 TX 工作合同。
  4. 修改 header 后不能保留旧 checksum 状态不管。
  5. IPv4 header checksum 与 TCP/UDP checksum 是不同层。
  6. GSO feature 和 checksum feature 必须组合检查。

字段与所有权统一核对表

对象或字段 主要修改者/使用者 所有权与释放要点
ip_summed RX 驱动、协议栈、TX 编码校验和责任状态
csum COMPLETE 验证 随 pull/push 和协议处理调整
csum_start/offset PARTIAL TX 设备或软件 helper 最终完成

最小状态调用链

1
2
3
4
5
6
RX descriptor/checksum metadata
→ driver 设置 ip_summed/csum/csum_level
→ GRO 与 L3/L4 验证或调整状态
→ 转发/本地发送重新决定 CHECKSUM_PARTIAL
→ GSO/软件 checksum helper 或网卡完成最终 checksum
→ skb 进入下一持有者或被释放

3. RSS、RPS、RFS 与 XPS 的多核流调度

RSS、RPS、RFS 与 XPS 的多核流调度

结论摘要

skb->hash 是硬件 RSS 与软件 flow dissector、GRO、RPS/RFS、reuseport 等机制之间的公共 flow identity。RSS 在网卡接收前选硬件 RX queue;RPS 在 skb 已构造后选协议栈 CPU;RFS 在 RPS 基础上参考应用 socket CPU;XPS 在发送侧选 TX queue。它们解决的层次不同,不能互相等同。

一、hash 来源

硬件 RSS

网卡解析包头并计算 hash,按 indirection table 选择 RX queue。驱动读取 descriptor hash,通过 skb_set_hash() 写入:

  • skb->hash
  • l4_hash 类型;
  • sw_hash=0

软件 hash

若驱动没有提供,skb_get_hash() 检查 hash 是否为零,必要时调用 __skb_get_hash(),见:

  • kernel-5.10/include/linux/skbuff.h:1388-1393
  • kernel-5.10/net/core/flow_dissector.c:1607

flow dissector 从 L2/L3/L4 key 计算对称或规范 hash,并设置 sw_hash=1

二、l4_hash 与 sw_hash

  • l4_hash=1:hash 基于规范四元组/传输端口,可用于更强的 flow 粒度;
  • l4_hash=0:可能只有 L3 或其它 hash;
  • sw_hash=1:软件计算;
  • sw_hash=0:通常来自硬件或外部设置。

hash 相同不保证包绝对属于同一流,仍可能碰撞;它是快速分桶索引,不替代完整 header 比较。

三、RSS

1
2
3
4
5
packet arrives
→ NIC computes hash
→ indirection table selects RX queue
→ queue IRQ/NAPI CPU
→ driver builds skb and records hash/queue

RSS 在 skb 产生之前完成,影响 DMA ring 和中断负载。它依赖硬件多队列。

四、RPS

get_rps_cpu() 位于 kernel-5.10/net/core/dev.c:4359。大致流程:

  1. 获取/计算 skb hash;
  2. 查看 RX queue 的 rps_map
  3. 根据 hash 选择候选 CPU;
  4. 可结合 RFS flow table;
  5. 若目标 CPU 不同,enqueue_to_backlog() 排入目标 CPU。

enqueue_to_backlog() 位于 kernel-5.10/net/core/dev.c:4572。目标 CPU 的 softnet_data 使用 input_pkt_queue 接收远端 enqueue,再由 backlog NAPI 交换到 process_queue 批量处理。

RPS 发生在 skb 已分配之后,会增加跨 CPU enqueue、IPI/cacheline 成本,但可让单队列网卡利用多核协议栈。

五、RFS

RFS 目标是让 flow 在运行其应用线程的 CPU 上处理,提高 socket/cache locality。

核心结构:

  • rps_sock_flow_table:全局 hash → 应用最近 CPU;
  • rps_dev_flow_table:设备 RX queue flow 状态;
  • rps_dev_flow:记录 CPU 与 last_qtail,避免乱序迁移。

socket 收发路径通过 rps_record_sock_flow() 更新应用 CPU。get_rps_cpu() 比较 flow table,在安全队列边界后迁移 CPU,避免同一 TCP flow 因 CPU 切换产生重排。

六、XPS

XPS 是发送侧 queue steering:

1
2
3
4
5
socket/CPU sends skb
→ netdev_pick_tx()/get_xps_queue()
→ 根据 CPU/socket/flow map 选择 TX queue
→ skb->queue_mapping
→ qdisc/driver ring

目的包括:

  • 让 CPU 固定使用局部 TX queue;
  • 减少多个 CPU 对同一 queue lock/cacheline 竞争;
  • 保持 flow 与 queue 稳定;
  • 配合 IRQ/RSS 建立 RX/TX affinity。

七、queue_mapping

RX 时驱动可用 skb_record_rx_queue() 记录来源 queue;TX 时 queue_mapping 表示选择的发送 queue。字段复用但路径语义不同。

routing、tc redirect、设备层级变化后 queue mapping 可能需要重置或重新选择,不能把输入 queue 机械当作输出 queue。

八、四者对比

机制 执行位置 选择对象 是否需要 skb 主要目标
RSS 网卡硬件 RX queue 硬件分流/IRQ 多队列
RPS RX core 软件 协议栈 CPU 软件多核处理
RFS RPS + socket 反馈 应用所在 CPU socket cache locality
XPS TX queue selection TX queue 发送队列局部性/降竞争

九、GRO 与 hash

GRO 使用 hash 快速寻找候选 flow,但仍由协议回调确认 header/sequence。硬件 hash 类型错误可能降低聚合效率或错误分桶;软件可在必要时重新计算。

6.1 GRO hash bucket 把同一 NAPI 的 flow 按 hash 分桶,进一步体现 skb->hash 的跨子系统价值。

十、性能与顺序约束

  • RSS/RPS map 应结合 NUMA、IRQ affinity、应用绑核;
  • RPS 过宽会增加 IPI 和 cache miss;
  • 同 flow CPU 迁移必须考虑 backlog qtail,避免乱序;
  • XPS queue 选择需稳定,TCP 乱序/锁竞争都受影响;
  • hash collision 只影响候选分桶,协议逻辑仍应验证完整 flow。

十一、常见误区

  1. RSS 与 RPS 都“分流”,但一个在硬件 skb 前,一个在软件 skb 后。
  2. RFS 不是让应用线程直接执行 NAPI,而是让协议栈 CPU 靠近应用。
  3. XPS 选 TX queue,不是选发送 CPU。
  4. queue_mapping 不是永久属性,跨设备 redirect 后可能失效。
  5. hash 不一定含端口,需看 l4_hash
  6. 开启所有 RPS CPU 不一定更快,可能增加跨核成本。

字段与所有权统一核对表

对象或字段 主要修改者/使用者 所有权与释放要点
hash/l4_hash RSS 或 flow dissector RPS/RFS/GRO/reuseport 读取
queue_mapping XPS/queue select TX qdisc 与驱动队列读取
CPU backlog/flow table RPS/RFS enqueue 后目标 CPU 持有 skb

最小调用链

1
2
3
4
驱动/RSS 或 flow dissector 产生 hash
→ RPS/RFS 选择 RX CPU/socket locality
→ TX XPS/queue selection
→ qdisc 与设备队列

源码阅读路线

建议不要从 struct sk_buff 声明开始逐字段背诵,而应使用下面的阅读顺序:

  1. 先在入口函数确认 skb 是新分配、clone、队列取出还是从驱动 buffer 构造;
  2. 沿成功主线记录 data/len/data_len、header offset、checksum、hash、queue mapping 等字段变化;
  3. 再回到每个失败分支,确认 skb、page、DMA、socket 和记账引用是否回滚;
  4. 最后对照 5.10、6.1 或 BSP 的同名 symbol,区分语义变化、性能优化和 vendor backport。

本篇总结

  1. GRO 在 RX 聚合,GSO/TSO 在 TX 延迟分段,二者通过 shared info 元数据衔接。
  2. ip_summed 表达 checksum 的责任归属,而不是简单的“正确/错误”。
  3. hash、CPU 和 queue mapping 把包内容映射为处理位置与发送队列。
  4. offload 的本质是延后工作或把工作交给硬件,但软件始终要维护可验证的语义。

sk_buff 的难点从来不只是字段多,而是同一个对象不断改变数据视图、元数据状态和持有者。只要把“布局、字段、所有权、释放”四条线同时画出来,绝大多数网络栈源码都会从零散函数变成可推导的生命周期。

系列目录

  1. Linux sk_buff 深度解析(一):数据结构、四指针与核心字段
  2. Linux sk_buff 深度解析(二):非线性数据、引用计数与生命周期
  3. Linux sk_buff 深度解析(三):从网卡 DMA 到 Socket 的 RX 路径
  4. Linux sk_buff 深度解析(四):从 sendmsg 到 TX completion
  5. Linux sk_buff 深度解析(五):GRO、GSO、Checksum 与多核流调度(本文)
  6. Linux sk_buff 深度解析(六):Netfilter、tc、eBPF 与 XDP
  7. Linux sk_buff 深度解析(七):VLAN、隧道、分片与重组
  8. Linux sk_buff 深度解析(八):Socket 内存、零拷贝、丢包与版本演进