Linux sk_buff 深度解析(五):GRO、GSO、Checksum 与多核流调度
写在前面
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 或开发板实验写成已验证结论。
阅读前先建立四个问题
阅读任何 skb 代码路径,都建议反复追问:
- 当前
skb->data指向哪一层协议头,linear 与 non-linear 数据分别在哪里? - 哪些字段刚被修改,下一层为什么依赖这些字段?
- 当前谁持有 skb shell、head、frag page 以及 socket/dst/conntrack 等外部引用?
- 成功、失败、重试、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 | RX: many wire packets → GRO → one large skb → protocol stack |
它们优化的不是字节复制本身,而是把固定 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 不只有一种布局:
- 把新数据页合入 head skb 的
frags[]; - 使用
frag_list串联完整 skb; - 调整
len/data_len/truesize; - 设置 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 | validate_xmit_skb() |
位置:
__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_BUCKETS 在 kernel-6.1/include/linux/netdevice.h:339 为 8。先按 hash 分桶减少同一 list 的线性比较,改善多 flow 批次成本。
十一、关键不变量
- GSO type 必须与外层/内层协议和 feature 匹配。
- header offset 必须能定位每个 segment 需要复制/修改的 header。
gso_size/gso_segs/len必须一致或能由 helper 修正。- frag 数不能超过上限。
- segment/merge 过程必须维护 page ref、truesize 和 checksum 状态。
- GRO merge 消费当前 skb 后,调用者不能继续访问。
十二、常见误区
- GRO 不是 LRO;GRO 是协议栈可控的软件聚合,保持更严格语义。
- GSO 不等于硬件 TSO;不支持时可软件分段。
- 大 skb 不代表线上存在一个超 MTU Ethernet frame。
- GRO 后不一定全部 linear,也不一定总是 frag_list。
gso_segs可能需要重新计算,不能在所有路径视作绝对可信。- 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 | 小 skb 进入 GRO |
2. Checksum 不是布尔值:四态责任模型
结论摘要
ip_summed 不是“checksum 是否正确”的布尔值,而是网络栈与驱动之间的责任状态机。RX 常见 NONE、UNNECESSARY、COMPLETE;TX 常见 PARTIAL。csum 与 csum_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 | union { |
- 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 | ip_summed = CHECKSUM_PARTIAL |
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 时:
- 确保 checksum 写入位置可写/线性;
- 软件计算 checksum;
- 写入协议 checksum 字段;
- 把状态转换为不再需要硬件处理。
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 |
十、常见误区
- UNNECESSARY 不是 checksum 值为零。
- COMPLETE 不是“已经完全验证”,它提供可验证的和。
- PARTIAL 不是坏包,而是明确的 TX 工作合同。
- 修改 header 后不能保留旧 checksum 状态不管。
- IPv4 header checksum 与 TCP/UDP checksum 是不同层。
- GSO feature 和 checksum feature 必须组合检查。
字段与所有权统一核对表
| 对象或字段 | 主要修改者/使用者 | 所有权与释放要点 |
|---|---|---|
ip_summed |
RX 驱动、协议栈、TX | 编码校验和责任状态 |
csum |
COMPLETE 验证 | 随 pull/push 和协议处理调整 |
csum_start/offset |
PARTIAL TX | 设备或软件 helper 最终完成 |
最小状态调用链
1 | RX descriptor/checksum metadata |
3. 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 | packet arrives |
RSS 在 skb 产生之前完成,影响 DMA ring 和中断负载。它依赖硬件多队列。
四、RPS
get_rps_cpu() 位于 kernel-5.10/net/core/dev.c:4359。大致流程:
- 获取/计算 skb hash;
- 查看 RX queue 的
rps_map; - 根据 hash 选择候选 CPU;
- 可结合 RFS flow table;
- 若目标 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 | socket/CPU sends skb |
目的包括:
- 让 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。
十一、常见误区
- RSS 与 RPS 都“分流”,但一个在硬件 skb 前,一个在软件 skb 后。
- RFS 不是让应用线程直接执行 NAPI,而是让协议栈 CPU 靠近应用。
- XPS 选 TX queue,不是选发送 CPU。
queue_mapping不是永久属性,跨设备 redirect 后可能失效。hash不一定含端口,需看l4_hash。- 开启所有 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 | 驱动/RSS 或 flow dissector 产生 hash |
源码阅读路线
建议不要从 struct sk_buff 声明开始逐字段背诵,而应使用下面的阅读顺序:
- 先在入口函数确认 skb 是新分配、clone、队列取出还是从驱动 buffer 构造;
- 沿成功主线记录
data/len/data_len、header offset、checksum、hash、queue mapping 等字段变化; - 再回到每个失败分支,确认 skb、page、DMA、socket 和记账引用是否回滚;
- 最后对照 5.10、6.1 或 BSP 的同名 symbol,区分语义变化、性能优化和 vendor backport。
本篇总结
- GRO 在 RX 聚合,GSO/TSO 在 TX 延迟分段,二者通过 shared info 元数据衔接。
ip_summed表达 checksum 的责任归属,而不是简单的“正确/错误”。- hash、CPU 和 queue mapping 把包内容映射为处理位置与发送队列。
- offload 的本质是延后工作或把工作交给硬件,但软件始终要维护可验证的语义。
sk_buff 的难点从来不只是字段多,而是同一个对象不断改变数据视图、元数据状态和持有者。只要把“布局、字段、所有权、释放”四条线同时画出来,绝大多数网络栈源码都会从零散函数变成可推导的生命周期。
系列目录
- Linux sk_buff 深度解析(一):数据结构、四指针与核心字段
- Linux sk_buff 深度解析(二):非线性数据、引用计数与生命周期
- Linux sk_buff 深度解析(三):从网卡 DMA 到 Socket 的 RX 路径
- Linux sk_buff 深度解析(四):从 sendmsg 到 TX completion
- Linux sk_buff 深度解析(五):GRO、GSO、Checksum 与多核流调度(本文)
- Linux sk_buff 深度解析(六):Netfilter、tc、eBPF 与 XDP
- Linux sk_buff 深度解析(七):VLAN、隧道、分片与重组
- Linux sk_buff 深度解析(八):Socket 内存、零拷贝、丢包与版本演进
