写在前面

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

从 sk_buff 为什么不是一个简单 buffer 出发,系统解释 skb shell、linear head、shared info、四指针、header offset、核心字段与数据区操作。

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

Linux sk_buff 深度解析(一):数据结构、四指针与核心字段

阅读前先建立四个问题

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

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

本篇核心结论

  • skb 是控制对象,包数据可能分散在 linear head、page frags 和子 skb 中。
  • head <= data <= tail <= end 描述 linear head,不等于整个逻辑包。
  • 协议头使用相对 head 的 offset,因而能够承受 push/pull 和 head 重分配。
  • 读字段时要同时问:谁写入、谁读取、当前谁持有 skb。

1. 为什么 Linux 网络栈需要 sk_buff

问题

  1. Linux 网络栈为什么不只传递一个普通 buffer 指针?
  2. head/data/tail/end 分别表示什么?
  3. headroom、linear data、tailroom 和 skb_shared_info 如何布局?
  4. skb 本体、head buffer、page frag 为什么有不同生命周期?
  5. 一个新 skb 从分配、构造到释放的基本过程是什么?

结论摘要

struct sk_buff 不是“网络包数据本身”,而是网络包的控制对象和元数据容器。真正的线性数据通常位于独立的 head buffer 中,非线性 payload 还可能位于 page fragments 或其它 skb 组成的 frag_list 中。

贯穿本阶段的长度恒等式是:skb->len = skb_headlen(skb) + skb->data_len

四指针模型描述 head buffer 的线性部分:

1
head <= data <= tail <= end
  • head:已分配 head buffer 的起始地址;
  • data:当前协议层看到的线性数据起点;
  • tail:当前线性数据末尾;
  • end:线性可用区末尾,也是 skb_shared_info 的起始位置;
  • [head, data):headroom;
  • [data, tail):当前线性数据;
  • [tail, end):tailroom,仅在线性 skb 语义下可直接追加。

需要特别注意:在 64 位内核中,本地源码定义了 NET_SKBUFF_DATA_USES_OFFSET,所以 tailend 在结构体里实际是相对 head 的无符号 offset,而不是 C 指针。所谓“四指针模型”是概念模型;访问实际地址应使用 skb_tail_pointer()skb_end_pointer() 等 helper。

一、为什么不能只使用普通 buffer 指针

网络包经过驱动、二层、三层、四层、Netfilter、qdisc、socket 等多个子系统。仅传递 void *data 无法同时解决以下问题。

1. 协议头需要频繁增加和移除

收包时,协议栈会逐层解析并“移过”当前视图;发包时,会从 payload 开始逐层向前添加 TCP/UDP、IP 和 Ethernet header。

使用 data 指针和预留 headroom 后,可以通过移动指针表达当前协议层,不必每经过一层就复制整个包。

2. 网络包需要排队和调度

skb 自带 next/prev,可以进入 socket queue、backlog、qdisc、TCP 重传队列等。包数据不需要为了排队而改变布局。

3. 网络栈需要携带大量元数据

例如:

  • 输入/输出设备;
  • socket 所有者;
  • 路由 dst
  • MAC、network、transport header offset;
  • checksum offload 状态;
  • hash、RX/TX queue;
  • VLAN、GSO、timestamp、mark、priority;
  • destructor 和引用计数。

这些内容不是线速报文的一部分,不能直接塞入报文字节流。

4. 数据可能不是一段连续内存

高性能收发包会使用 page fragment、scatter-gather、GRO/GSO、零拷贝。skb 允许“一个逻辑网络包”由 linear data、frags[]frag_list 共同组成。

5. 元数据与数据需要独立共享

clone 可以创建新的 skb shell,同时共享 head buffer 或 page 数据。这样不同路径可以拥有独立的队列节点和协议元数据,而不复制完整 payload。

因此可以把 skb 理解为:

1
sk_buff = 包的控制块 + 数据布局描述 + 协议元数据 + 生命周期管理

二、三个不同的对象

初学时最重要的是不要把下面三个对象混在一起。

1
2
3
4
5
6
7
8
9
10
11
12
对象 1:struct sk_buff
- 从 skbuff_head_cache / skbuff_fclone_cache 分配
- 保存元数据、队列节点、四指针/offset、len、users 等

对象 2:linear head buffer
- 通常通过 kmalloc_reserve() 分配
- 包含 headroom、linear data、tailroom
- 尾部紧跟 struct skb_shared_info

对象 3:非线性数据 page / frag_list skb
- 由 skb_shared_info.frags[] 或 frag_list 描述
- 有独立的 page ref 或 skb ref

它们不是一次分配,也不是靠同一个引用计数保护。

三、linear head buffer 的内存布局

典型布局如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
低地址                                                               高地址

head data tail end
│ │ │ │
▼ ▼ ▼ ▼
+-------------+-------------------------+-----------------+------------------+
| headroom | linear data | tailroom | skb_shared_info |
+-------------+-------------------------+-----------------+------------------+
<------- skb_headlen ------>

skb_shared_info:
nr_frags
gso_size / gso_segs / gso_type
frag_list
dataref
destructor_arg
frags[MAX_SKB_FRAGS]

skb_shinfo(skb) 的定义直接把 skb_end_pointer(skb) 转换为 struct skb_shared_info *

1
2
#define skb_shinfo(SKB) \
((struct skb_shared_info *)(skb_end_pointer(SKB)))

位置见 kernel-5.10/include/linux/skbuff.h:1464

因此:

  • end 不是整块 kmalloc 内存的真正末尾;
  • end 是 linear 可用区的末尾;
  • end 开始存放 skb_shared_info
  • 分配 head buffer 时必须额外给 shared info 留空间。

四、四指针分别代表什么

1. head

head 是 head buffer 的基址。正常的 push/pull/put/reserve 不改变它,只有重新分配 head 的操作才可能改变它。

2. data

data 是当前线性数据起点,也是协议栈当前视图的起点。

  • skb_push()data 向低地址移动,通常用于添加协议头;
  • skb_pull()data 向高地址移动,通常用于移除或越过协议头。

data 不一定始终指向 Ethernet header、IP header 或 payload;它的含义取决于当前执行阶段。

3. tail

tail 表示 linear data 的结束位置,也就是下一个可追加字节的位置。skb_put() 会向后推进 tail 并增加 len

对于非线性 skb,tail 仍只描述 linear 区结束位置,不描述 page fragment 的末尾。

4. end

end 表示 linear 可用区的末尾,skb_shared_info 从这里开始。

在 64 位配置下:

1
2
skb_end_pointer(skb) = skb->head + skb->end;
skb_tail_pointer(skb) = skb->head + skb->tail;

对应:

  • kernel-5.10/include/linux/skbuff.h:606-614
  • kernel-5.10/include/linux/skbuff.h:1432-1439
  • kernel-5.10/include/linux/skbuff.h:2244-2258

五、必须记住的长度关系

1. headroom

1
headroom = data - head

实现位于 kernel-5.10/include/linux/skbuff.h:2401-2404

2. linear data 长度

1
skb_headlen(skb) = skb->len - skb->data_len

实现位于 kernel-5.10/include/linux/skbuff.h:2164-2167

3. 总长度

1
2
skb->len = linear data length + non-linear data length
= skb_headlen(skb) + skb->data_len

data_len == 0 表示 skb 是纯线性的;skb_is_nonlinear() 直接检查 data_len,见 kernel-5.10/include/linux/skbuff.h:2159-2162

4. tailroom

概念上:

1
tailroom = end - tail

但本地 5.10 的 skb_tailroom() 对非线性 skb 直接返回 0:

1
return skb_is_nonlinear(skb) ? 0 : skb->end - skb->tail;

kernel-5.10/include/linux/skbuff.h:2412-2415

这是一条重要语义:即使 head buffer 中从 tail 到 end 可能仍有物理空间,也不能简单假定普通 skb_put() 能安全用于非线性 skb。

5. truesize

truesize 不是报文长度,而是用于内存计量的近似实际占用。宏 SKB_TRUESIZE() 把以下内容计入:

  • 数据区容量;
  • 对齐后的 struct sk_buff
  • 对齐后的 struct skb_shared_info

定义见 kernel-5.10/include/linux/skbuff.h:238-241

因此:

1
truesize 通常大于 len

后续 socket 内存限制使用的是 truesize,而不是线速包长。

六、一个新 skb 的初始状态

__alloc_skb() 的关键步骤位于 kernel-5.10/net/core/skbuff.c:183-263

1. 分配 skb shell

1
skb = kmem_cache_alloc_node(cache, ...);

2. 为数据区和 shared info 分配连续内存

1
2
3
size = SKB_DATA_ALIGN(size);
size += SKB_DATA_ALIGN(sizeof(struct skb_shared_info));
data = kmalloc_reserve(size, ...);

然后用 SKB_WITH_OVERHEAD() 把 shared info 占用从 linear 可用容量中扣除。

3. 初始化四指针/offset

1
2
3
4
skb->head = data;
skb->data = data;
skb_reset_tail_pointer(skb);
skb->end = skb->tail + size;

所以刚分配完成时:

1
2
3
4
5
6
7
head == data == tail
len == 0
data_len == 0
headroom == 0
tailroom >= requested size
users == 1
shared_info.dataref == 1

4. 初始化两个不同的引用计数

1
2
refcount_set(&skb->users, 1);
atomic_set(&shinfo->dataref, 1);
  • users 保护 skb shell;
  • dataref 保护共享 head/data 区。

当前阶段只记住两者不是同一个引用计数;clone 细节放到阶段 4。

七、reserve、put、push、pull 如何改变模型

假设刚分配的 skb 状态为:

1
2
head = data = tail
len = 0

1. skb_reserve(skb, R)

1
2
3
data += R
tail += R
len 不变

结果是预留 R 字节 headroom。源码明确说明只允许用于空 skb,见 kernel-5.10/include/linux/skbuff.h:2433-2444

2. skb_put(skb, N)

1
2
tail += N
len += N

它在尾部扩展 linear data,并返回原 tail 地址供调用者写入,见 kernel-5.10/include/linux/skbuff.h:2291-2299

3. skb_push(skb, H)

1
2
data -= H
len += H

它消耗 headroom,在当前数据前增加 H 字节,常用于添加协议头,见 kernel-5.10/include/linux/skbuff.h:2347-2353

4. skb_pull(skb, H)

1
2
data += H
len -= H

它越过前 H 字节,使当前视图进入下一层协议,见 kernel-5.10/include/linux/skbuff.h:2355-2361

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
初始:
head/data/tail end
│ │
+-----------------------------------------------------+

reserve(64):
head data/tail end
│ │ │
+-------------+------------------------------------------+
headroom=64

put(100):
head data tail end
│ │ │ │
+-------------+---------------------------+---------------+
headroom=64 linear data=100 tailroom

push(20):
head data tail end
│ │ │ │
+------+----------------------------------+---------------+
44 linear data=120 tailroom

整个过程中没有搬移原有 payload,只修改地址/offset 和长度。

八、为什么 skb_shared_info 放在 end 后面

源码注释说明 shared info 是跨 clone 共享的不变量,位于 header data 的末尾,见 kernel-5.10/include/linux/skbuff.h:512-515

这种布局有几个好处:

  1. skb shell 与数据 buffer 可以独立分配和共享;
  2. frag/GSO/dataref 元数据天然跟随共享数据区;
  3. linear 数据可以从前向后增长,直到 end
  4. skb_shinfo(skb) 可以由 end 直接计算,不需要 skb 额外保存一个 shared-info 指针;
  5. 分配器可能给出比请求更大的 kmalloc 块,代码可以把 shared info 放到实际分配区尾部,最大化 linear 可用空间,见 kernel-5.10/net/core/skbuff.c:204-218

九、生命周期总览

当前只建立高层流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
分配 skb shell + head buffer


reserve headroom


put/push 构造数据和协议头


驱动 / 协议栈 / Netfilter / qdisc / socket 之间传递或排队

├── 可能 clone/copy
├── 可能增加 frags/frag_list
└── 可能绑定 dst/socket/conntrack/destructor


consume_skb() 或 kfree_skb()


skb_release_head_state() 释放 dst/destructor/conntrack/extensions


skb_release_data() 释放 frags/frag_list/head buffer


kfree_skbmem() 释放 skb shell

本地释放路径:

  • kernel-5.10/net/core/skbuff.c:598-626 — head/data 与 frags;
  • kernel-5.10/net/core/skbuff.c:661-680 — 外部状态和整体释放;
  • kernel-5.10/net/core/skbuff.c:691-713__kfree_skb() / kfree_skb()

这一流程再次证明:skb shell、外部状态、head buffer 和 frag page 是分层释放的。

十、阶段 0 的关键不变量

不变量 1:四指针顺序

1
head <= data <= tail <= end

如果 push 超过 headroom,可能破坏 head <= data;如果 put 超过 tailroom,可能越过 end。checked API 会做更多检查,但不能依赖非法调用自动恢复。

不变量 2:长度关系

1
2
len >= data_len
skb_headlen = len - data_len

__skb_pull() 中存在:

1
BUG_ON(skb->len < skb->data_len);

因为 pull 只能从当前 linear 头部移除数据,不能让总长度小于仍然挂载的非线性长度。

不变量 3:end 指向 shared info 起始位置

1
skb_shinfo(skb) == (struct skb_shared_info *)skb_end_pointer(skb)

任何错误越过 end 的写操作都会破坏 frag、GSO 或引用计数元数据,后果通常不是简单丢一个包,而是内存破坏。

不变量 4:data 是动态视图,不是固定协议头

不能看到 skb->data 就假定它永远指向 Ethernet/IP/TCP。必须结合当前调用点以及 MAC/network/transport header offset 判断。

不变量 5:逻辑包长度不等于 linear 长度

1
2
len != tail - data       // 非线性 skb 时通常不相等
len == tail - data // 仅可作为纯线性 skb 的典型关系

更通用的关系是:

1
linear length = skb_headlen(skb) = len - data_len

十一、常见误区

误区 1:“struct sk_buff 里面装着整个网络包”

错误。skb 主要保存元数据,linear 数据位于独立 head buffer,payload 还可能在 page 中。

误区 2:“四个成员在所有架构上都是指针”

错误。64 位本地内核中 tail/end 是 offset;四指针是概念模型。

误区 3:“end 是整块分配内存的最后一个字节”

错误。end 是 linear 可用区末尾,后面还有 skb_shared_info

误区 4:“tail 是整个包的末尾”

错误。对非线性 skb,tail 只是 linear data 的末尾。

误区 5:“len 等于 tail - data”

只对纯线性 skb 成立。非线性 skb 还要包含 data_len

误区 6:“kfree_skb 就是 kfree(skb)”

错误。它需要先处理 users,释放 dst、destructor、conntrack、extensions、frags、frag_list 和 head buffer,最后才释放 skb shell。

相关知识横向扩展

  • struct sk_buff_head 如何把 skb 组织为带锁队列;
  • header offset 为什么比裸指针更适合 head reallocation;
  • clone 时 dataref 如何拆分 full-data ref 与 payload-only ref;
  • page_pool 如何让 frag page 返回 RX 内存池;
  • GRO/GSO 为什么依赖 skb_shared_info
  • socket 为什么使用 truesize 做收发内存记账。

最小生命周期调用链

1
2
3
4
5
6
alloc_skb()/napi_alloc_skb()
→ 初始化 skb shell、linear head 与 skb_shared_info
→ reserve/put/push/pull 建立当前协议视图
→ 协议栈、队列或驱动转移 skb 所有权
→ clone/COW/segment 维护 users、dataref 与 page ref
→ consume_skb()/kfree_skb() 分层释放外部引用、数据区和 shell

字段与所有权统一核对表

对象或字段 表达内容 所有权与释放要点
skb shell / users 控制对象及其共享次数 最后一个 shell 引用归零后才释放结构体
head / dataref linear buffer 与尾部 shared info clone 可共享;最后一个 dataref 释放 head
frags[] / page ref 非线性 page payload shared info 记录片段,free 时逐页归还引用
frag_list 子 skb 链 父 skb 持有并在释放路径递归消费
sk、dst、nfct 外部子系统状态 各自有独立引用或 destructor,不能由 users 代替

2. struct sk_buff 字段地图

struct sk_buff 字段地图

结论摘要

struct sk_buff 不应按声明顺序死记。更有效的方式是按“谁在使用字段”分为:队列与调度、设备与 socket、外部引用、长度与数据布局、协议头位置、checksum/offload、流与队列选择、VLAN/隧道、临时控制块、引用计数与扩展九组。字段布局同时承担三类工程目标:热路径访问、结构体空洞填充和 clone 时批量复制。

一、队列与节点字段

struct sk_buff 的开头是:

1
2
struct sk_buff *next;
struct sk_buff *prev;

kernel-5.10/include/linux/skbuff.h:720-738。它们必须位于最前面,因为 struct sk_buff_head 的前两个成员同样是 next/prev,见 kernel-5.10/include/linux/skbuff.h:294-301。队列 helper 把 queue head 当作循环双链表哨兵,例如 __skb_queue_head() 会把 (struct sk_buff *)list 传给插入逻辑。

开头的 union 还允许同一块空间作为:

  • 普通 skb 双链表节点;
  • struct rb_node,供 netem、IPv4 defrag、TCP 等红黑树场景使用;
  • struct list_head

关键约束是一个 skb 在同一时刻不能被无约束地同时挂入多种容器;当前持有者必须清楚节点空间的用途。

二、设备、socket 与外部状态

字段 主要语义 生命周期关注点
dev 当前输入或输出网络设备 redirect、routing 后可能变化
sk 关联 socket clone 默认不复制 owner;由 destructor 配合记账
_skb_refdst 路由/dst 缓存及低位标志 clone 需增加引用,free 时 skb_dst_drop()
destructor skb head state 的释放回调 常见为 socket 收发内存回调
_nfct conntrack 引用与状态位 clone/copy/free 必须维护引用
extensions 可选 skb 扩展 先检查 active_extensions

__copy_skb_header() 明确不复制旧 sk,但会复制 dev、控制块、dst、extensions 和 Netfilter 状态,见 kernel-5.10/net/core/skbuff.c:939-985。因此“复制 skb header”并不等于机械复制整个结构体。

三、长度字段

1
2
3
4
5
unsigned int len;
unsigned int data_len;
__u16 mac_len;
__u16 hdr_len;
unsigned int truesize;
  • len:逻辑包总长度,包含 linear、frags 和 frag_list;
  • data_len:非线性部分长度;
  • mac_len:MAC header 长度,通常由 network header 与 MAC header offset 之差得到;
  • hdr_len:headerless clone 场景记录可写 header 空间;
  • truesize:内存计量值,不是线速包长。

核心关系:

1
2
skb_headlen(skb) = len - data_len
len = linear length + non-linear length

四、协议头字段为什么是 offset

主要字段:

1
2
3
4
5
6
transport_header
network_header
mac_header
inner_transport_header
inner_network_header
inner_mac_header

kernel-5.10/include/linux/skbuff.h:900-912。访问 helper 通过 head + offset 得到地址,例如:

1
skb_network_header(skb) = skb->head + skb->network_header;

kernel-5.10/include/linux/skbuff.h:2555-2626

使用 offset 而不是裸指针的主要价值:

  1. data 会被 push/pull,header 位置仍可独立保存;
  2. pskb_expand_head() 可能更换 head buffer,复制 offset 后无需逐个修正绝对指针;
  3. 16 位 offset 比 64 位指针节省结构体空间;
  4. inner/outer header 可以同时表达隧道封装。

需要区分:

  • skb_reset_network_header():把 network header 设为当前 data
  • skb_set_network_header(skb, n):设为当前 data + n
  • skb_network_header():从 head + offset 取地址。

五、临时控制块 cb[48]

cb[48] 定义于 kernel-5.10/include/linux/skbuff.h:749-755。源码注释给出的所有权规则是:

当前把 skb 排队的层拥有控制块;如果要跨层保留,必须明确复制或保存。

典型 overlay:

  • IPv4:IPCB(skb)struct inet_skb_parmkernel-5.10/include/net/ip.h:47-63,104
  • IPv6:IP6CB(skb)struct inet6_skb_parm
  • TCP:TCP_SKB_CB(skb)struct tcp_skb_cbkernel-5.10/include/net/tcp.h:838-899
  • GRO:NAPI_GRO_CB(skb)struct napi_gro_cbkernel-5.10/include/linux/netdevice.h:2487-2554
  • qdisc:struct qdisc_skb_cbkernel-5.10/include/net/sch_generic.h:420-429

这些结构共享同一 48 字节空间,所以不能同时假设多个 overlay 都有效。进入新子系统时,旧层 cb 内容可能被覆盖。

__copy_skb_header() 使用 memcpy(new->cb, old->cb, sizeof(old->cb)) 复制控制块,但这只保证字节被复制,不保证其跨层语义仍然有效。

六、checksum 与 offload 字段

主要字段包括:

  • ip_summed
  • csum,或 union 中的 csum_start/csum_offset
  • csum_validcsum_levelcsum_complete_swcsum_not_inet
  • shared info 中的 gso_size/gso_segs/gso_type

ip_summed 只有两位,编码 CHECKSUM_NONE/UNNECESSARY/COMPLETE/PARTIAL。它表达的是“checksum 责任和状态”,不是简单的布尔值。GSO 元数据放在 shared info,是因为它属于共享数据布局而不是某个独立 skb shell 的私有队列状态。

七、hash、CPU 与队列字段

字段 用途
hash 保存硬件 RSS 或软件 flow dissector hash
l4_hash hash 是否为规范的四元组/L4 hash
sw_hash hash 是否由软件计算
queue_mapping RX/TX 队列映射;TX 时影响 qdisc/设备队列
napi_id RX 来源 NAPI 标识
sender_cpu napi_id 复用空间,供 XPS 使用
skb_iif 输入接口 ifindex

这些字段把“包内容”转换为调度决策:在哪个 CPU 处理、进入哪个 backlog、选择哪个 TX queue。

八、VLAN、隧道和策略字段

  • protocol:驱动/二层解析得到的上层协议;
  • vlan_proto/vlan_tci/vlan_present:硬件剥离或 metadata 形式的 VLAN tag;
  • inner_protocol 与 inner header offset:隧道内层协议;
  • encapsulation:inner header 是否有效;
  • mark:策略路由、Netfilter、tc 等通用标记;
  • priority:排队优先级;
  • tc_indextc_skip_classifytc_at_ingressredirected:tc 数据路径状态。

注意 union 复用:markreserved_tailroom 共享空间,napi_idsender_cpu 共享空间。字段当前语义取决于所在路径。

九、headers_start/headers_end 的意义

headers_startheaders_end 是零长度数组标记,不保存数据。两者之间的字段在 clone/copy header 时由一次 memcpy() 批量复制,见:

  • 标记:kernel-5.10/include/linux/skbuff.h:798-919
  • 复制:kernel-5.10/net/core/skbuff.c:954-983

CHECK_SKB_FIELD() 用编译期断言确保 protocol、checksum、hash、priority、header offset、VLAN、mark 等字段位于批量复制范围内。

这说明字段排列不只是可读性问题,还与复制热路径、空洞和 ABI/KABI 约束相关。queue_mapping 因避免 16 位空洞而留在该区间外,单独复制。

十、结构尾部字段

结构尾部为:

1
tail, end, head, data, truesize, users, extensions

kernel-5.10/include/linux/skbuff.h:939-950__alloc_skb() 只清零到 tail 之前,然后显式初始化尾部,源码还警告不要随意在 tail 后增加字段,见 kernel-5.10/net/core/skbuff.c:221-241

在 64 位内核中 tail/end 是 offset,head/data 是指针。users 保护 skb shell;共享 head buffer 的引用计数位于 skb_shared_info.dataref

十一、字段修改者索引

字段组 典型写入者 典型读取者
dev/protocol 驱动、eth_type_trans()、routing/redirect RX core、L3、qdisc、driver
header offsets Ethernet/IP/TCP/隧道 helper 协议解析、checksum、BPF
hash 驱动 RSS、flow dissector GRO、RPS/RFS、socket reuseport
queue_mapping RX 驱动、TX queue selection qdisc、ndo_start_xmit
checksum RX driver、协议栈、GSO helper L3/L4、TX offload
cb 当前协议或队列层 同一层后续函数
sk/destructor socket ownership helper memory accounting/free
dst/nfct/extensions routing/Netfilter/扩展子系统 output、clone、free

十二、常见误区

  1. skb->data 不是固定协议头;header offset 才保存各层位置。
  2. cb 不是全局稳定元数据,不同层会覆盖它。
  3. queue_mapping 不是 CPU 编号,而是设备 RX/TX queue 语义。
  4. hash 不保证来自硬件,也不保证一定包含 L4 端口。
  5. markprioritytc_index 语义不同,不能互换。
  6. headers_start/end 不是缓存行边界,而是批量复制边界。
  7. 条件编译会改变结构体实际大小和字段存在性,不能只凭某个配置的 sizeof 推广到所有内核。

字段与所有权统一核对表

对象或字段 主要修改者/使用者 所有权与释放要点
data/tail/end 分配和 data helper linear head 布局;shell 私有
_skb_refdst/_nfct 路由、Netfilter 引用随 clone/copy/free 转移
sk/destructor/truesize socket owner helper destructor 归还 socket 记账

最小调用链

1
2
3
4
5
alloc/init
→ 子系统写入字段与 header offset
→ queue/protocol/driver 读取
→ clone/copy 复制规定区间
→ free 释放外部引用

3. 四指针、数据区与 Header 操作

四指针、数据区与 Header 操作

结论摘要

skb 数据操作分为三类:移动当前视图的 push/pull,扩展有效数据的 put,预留未来头部空间的 reserve。header helper 用相对 head 的 offset 保存协议头位置。只要操作不重新分配 head,已保存的 header offset 仍有效;pskb_expand_head()、COW、linearize 等可能更换或重排数据,调用者必须遵守 helper 的返回和指针失效规则。

一、基础操作的状态转换

skb_reserve

kernel-5.10/include/linux/skbuff.h:2433-2444

1
2
skb->data += len;
skb->tail += len;

前置条件是空 skb。它不改变 len,只把 headroom 增大、tailroom 减小。典型用途是在构造包之前为 L2/L3/L4 header 或对齐保留空间。

skb_put

checked 版本声明于 kernel-5.10/include/linux/skbuff.h:2291,unchecked __skb_put() 位于 2292-2299:

1
2
3
4
void *tmp = skb_tail_pointer(skb);
skb->tail += len;
skb->len += len;
return tmp;

它把原 tail 之后的空间纳入 linear data,返回新区域起点。__skb_put() 要求调用者已经证明空间足够,并用 SKB_LINEAR_ASSERT 拒绝非线性 skb。

skb_push

kernel-5.10/include/linux/skbuff.h:2347-2353

1
2
skb->data -= len;
skb->len += len;

它消耗 headroom,用于向前添加 header。调用前必须保证 headroom 足够。

skb_pull

kernel-5.10/include/linux/skbuff.h:2355-2365

1
2
3
skb->len -= len;
BUG_ON(skb->len < skb->data_len);
skb->data += len;

它只能从 linear 前部移除数据。若 pull 后总长度小于非线性长度,说明试图跨过不存在的 linear 数据,违反不变量。

二、checked 与 unchecked API

带双下划线的 helper 通常省略部分边界检查,适用于上层已证明不变量的热路径。普通 skb_put/push/pull 更适合作为外部 API,但也不能代替调用者理解前置条件。

典型规则:

  • __skb_put():必须有足够 tailroom,且 skb 为线性;
  • __skb_push():必须有足够 headroom;
  • __skb_pull():必须保证 len 合法;
  • skb_pull_inline()len > skb->len 时返回 NULL。

错误调用可能不是“返回失败”,而是破坏 shared info 或触发 BUG。

三、pskb_may_pull 与非线性 header

协议解析经常需要保证前 N 字节连续。例如读取完整 IPv4 header 前,不能假定它全部位于 linear 区。

pskb_may_pull() 位于 kernel-5.10/include/linux/skbuff.h:2384-2391

  1. len <= skb_headlen(skb),直接成功;
  2. len > skb->len,失败;
  3. 否则调用 __pskb_pull_tail(),从 frags/frag_list 把需要的数据复制或整理进 linear 区。

注意名字中的 pull 不代表它会像 skb_pull() 一样推进 data。它的目标是“保证从当前 data 开始的前 len 字节线性可读”。成功后 skb 布局可能改变,所以调用前缓存的 header 指针应重新获取。

pskb_pull() 则同时保证线性并真正推进 data、减少 len。

四、header offset helper

以 network header 为例:

1
2
3
4
5
6
7
8
9
skb_reset_network_header(skb)
skb->network_header = skb->data - skb->head;

skb_set_network_header(skb, off)
skb_reset_network_header(skb);
skb->network_header += off;

skb_network_header(skb)
return skb->head + skb->network_header;

kernel-5.10/include/linux/skbuff.h:2577-2590。MAC、transport、inner header 使用同样模式。

reset 与 set 的区别

  • reset:header 就在当前 data;
  • set(offset):header 在当前 data 之后 offset;
  • accessor:从 head 基址计算绝对地址。

push/pull 后 header offset 是否变化

普通 push/pull 只改 data,不会自动改已保存的 header offset。因此:

  • 已保存 header 仍指向原来的字节位置;
  • 当前 data 与 header 的相对距离发生变化;
  • 协议层需要在正确时机 reset/set 对应 header。

这正是 offset 独立于 data 的价值。

五、header 指针失效规则

以下操作通常不更换 head,仅移动 data/tail:

  • reserve;
  • put;
  • push;
  • pull。

它们不会使 skb_network_header(skb) 计算出的地址因 head 更换而失效,但调用者缓存的“相对 data 指针”和当前视图语义可能改变。

以下操作可能更换 head 或重排数据:

  • pskb_expand_head()
  • skb_cow_head()
  • skb_unclone()
  • __pskb_pull_tail()
  • skb_linearize()
  • BPF change head/adjust room 等。

调用这些函数后,应重新通过 ip_hdr()tcp_hdr()skb_network_header() 等 helper 获取指针,不保留旧的 struct iphdr *

六、pskb_expand_head

实现位于 kernel-5.10/net/core/skbuff.c:1622。它为 head buffer 增加 nhead/ntail,核心步骤包括:

  1. 检查 skb shell 不被多个 users 共享;
  2. 计算新 head 长度并分配新 buffer;
  3. 复制 linear 数据和 shared info;
  4. 对 frags、frag_list、zerocopy 和 clone 引用进行必要处理;
  5. 根据新 headroom 调整 data、tail、end 和各 header offset;
  6. 释放旧数据引用。

它可能改变 skb->headskb->data 的绝对地址,但保持逻辑包内容和 header offset 语义。

不能把它理解成单纯的 realloc():旧 head 可能被 clone,共享信息和 page refs 必须正确转移。

七、COW:写之前先获得独占数据

skb_cloned

cloned 位只表示“可能共享”,真正判断还需要查看 shared info 的 dataref。这是热路径先看位、再看原子计数的优化。

skb_cow

目标是确保:

  • 需要修改的数据区域可写;
  • 必要 headroom 足够;
  • 若数据被 clone,则扩展/复制得到独占 head。

skb_cow_head

它针对 header 写入场景,常用于 Netfilter NAT、隧道封装、tc/BPF 等将要修改或新增 header 的路径。若 head 已独占且 headroom 足够,返回 0;否则通过 pskb_expand_head() 创建可写 head。

COW 不意味着一定复制全部 payload。非线性 page 可能继续共享,关键是保证要写的 head/header 区域独占。

八、数据操作前置/后置条件表

API 前置条件 主要变化 可能失败 旧指针风险
skb_reserve 空 skb、空间足够 data/tail 同时前移 无返回值 data 相关指针变化
skb_put 线性、tailroom 足够 tail/len 增加 checked 路径可报错/BUG 原 tail 之后可写
skb_push headroom 足够 data 前移、len 增加 越界会破坏内存 data 变化
skb_pull len 足够、不能越过 linear data 前移、len 减少 NULL 或 BUG data 变化
pskb_may_pull 请求不超过总 len 必要数据线性化 内存不足返回 false head/header 指针可能失效
pskb_expand_head skb shell 独占等 更换/扩展 head -ENOMEM 所有绝对数据指针失效
skb_cow_head 指定所需 headroom 确保 header 可写 -ENOMEM 发生复制时失效

九、典型 RX/TX 使用模式

RX

1
2
3
4
驱动构造 linear Ethernet frame
→ eth_type_trans() 保存 MAC header 并 pull Ethernet header
→ IP 层 reset network header
→ L4 层 set/reset transport header

TX

1
2
3
4
5
先 reserve 足够 headroom
→ 写 payload / skb_put
→ skb_push 添加 L4 header
→ skb_push 添加 IP header
→ skb_push 添加 Ethernet header

真实代码可能使用专用 helper,但四指针变化遵循同一模型。

十、常见误区

  1. pskb_may_pull() 不是普通 skb_pull(),它主要保证连续可读。
  2. push/pull 不会自动替调用者重设所有协议 header offset。
  3. cloned 位不等于一定仍共享,必须结合 dataref helper。
  4. COW 不一定复制 frags 中的全部 payload。
  5. pskb_expand_head() 后旧的 iphdr * 不能继续使用。
  6. skb_put() 不是 memcpy;它只把空间纳入有效数据,调用者还需写入内容。
  7. 非线性 skb 不能直接按普通 tailroom 追加。

字段与所有权统一核对表

对象或字段 主要修改者/使用者 所有权与释放要点
data/tail/len push/pull/put/trim 调用者持有 skb,操作前必须满足空间不变量
header offsets reset/set helper head 重分配后 offset 保持可重定位
dataref COW/expand 共享 head 只有在获得独占可写副本后才能改写

最小调用链

1
2
3
4
reserve 建立 headroom
→ put/push/pull 改变当前数据视图
→ reset/set 保存 header offset
→ may_pull/COW/expand 保证可访问和可写

源码阅读路线

建议不要从 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. skb 是控制对象,包数据可能分散在 linear head、page frags 和子 skb 中。
  2. head <= data <= tail <= end 描述 linear head,不等于整个逻辑包。
  3. 协议头使用相对 head 的 offset,因而能够承受 push/pull 和 head 重分配。
  4. 读字段时要同时问:谁写入、谁读取、当前谁持有 skb。

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 内存、零拷贝、丢包与版本演进