Skip to content

第一百一十一章:sk_buff

问道期

涉及内核源码:

林小源在码头边捡到了一张卷起来的羊皮纸。

纸很薄,但展开后却出奇地大。上面没有文字,而是一连串刻度线——像一把可以伸缩的尺子。尺子的一端标着 head,另一端标着 end,中间有两个可移动的标记:datatail

一个穿着灰色长袍的文书坐在码头的石阶上,手里拿着一摞同样的羊皮纸,正往上面盖章。

"你是?"

"我是 sk_buff 的管事。" 文书推了推鼻梁上的小圆镜,"每一张羊皮纸,就是一个数据包。网络协议栈里流转的所有数据,都由我这些纸来承载。"

他拿起林小源手里的羊皮纸,指着刻度线:"headend 是缓冲区的两端——纸的物理边界。datatail 是当前数据的起止位置。中间的部分是有效数据。"

"为什么要四个指针?"

文书笑了:"你想想,数据包在协议栈里往下走的时候,每一层都要加自己的报文头——TCP 头、IP 头、MAC 头。如果每次都重新分配内存、复制数据,那得多慢?"

他在羊皮纸的 data 端前面画了一个方框:"加报文头的时候,只要把 data 往前移就行——。数据不用动,只是指针在走。到了对面,剥报文头的时候把 data 往后移——。同样,数据不用动。"

"那往后面加数据呢?"

"tail 往后移。" 文书把四个操作写在纸边,"push、pull、put——三个动作,覆盖了协议栈里所有的数据操作。没有拷贝,没有重分配,只有指针的移动。"

林小源拿起那张羊皮纸翻来覆去看——这就是数据包的肉身,协议栈里流转的所有数据都靠它承载。

林小源在文书身旁坐下,看着他往羊皮纸上盖章。

每张纸的顶部都盖着不同的印章:有的写着 ,有的写着 ,有的写着

"这些是协议类型。" 文书解释,"每张 sk_buff 还带一大堆元数据——关联的网络设备 、关联的 socket sk、数据长度 、校验和状态……你以后用到哪个就看哪个。"

他抽出一张纸,翻过来。背面画着一条链表——每张 sk_buff 的 指针串在一起,形成双向链表。

"发送队列、接收队列、重传队列——都是用 sk_buff 串起来的链表。 是链表头,里面有一个 记录队列长度。你往队列里加一个包、减一个包,都是链表操作。"

"为什么不直接用数组?"

"因为 sk_buff 的大小不固定——有的包大、有的包小,有的带分片、有的不带。链表灵活,不需要连续内存。而且内核还有 slab 分配器专门给 sk_buff 做缓存——,分配和释放都很快。"

林小源拿起一张空白的羊皮纸,试着用 在头部加了一段文字。data 指针果然往前移了,整段文字出现在了数据区的最前面。他又用 把它剥掉,data 指针回到原位,那段文字消失了——不是被覆盖,而是指针越过了它。

"它还在那里?"

"在,但 datatail 之间看不到它了。这就是 push 和 pull 的本质——改变视角,不改变数据。"

他忽然明白了——push 和 pull 的本质是移动指针,而不是搬动数据,这才是 sk_buff 高效的秘密。

黄昏时分,林小源站在码头高处,俯瞰整个协议栈。

他看到一摞 sk_buff 从应用层的方向流下来。每经过一层,就有一个身影冲上来,在羊皮纸的 data 端前面盖一个章、画一个方框。TCP 层盖了 20 个字节的章,IP 层盖了 20 个字节的章,MAC 层盖了 14 个字节的章。每盖一次,data 就往前移一截。

"这就是发送。" 文书站在他身旁,"从上往下,层层加头。sk_buff 就像一封信——每经过一个邮局,就在信封上贴一张邮票。到了目的地,反着来——一层一层撕掉邮票,露出里面的内容。"

接收方向,一摞 sk_buff 从网卡的方向飞上来。每经过一层,就有一个身影冲上去,把最前面的方框撕掉。MAC 层撕掉 14 字节,IP 层撕掉 20 字节,TCP 层撕掉 20 字节。每撕一次,data 就往后移一截。最后剩下的就是纯应用数据,交给了 socket。

"sk_buff 是协议栈的信使。" 文书说,"它从应用层出发,穿越整个协议栈,最后到达网卡。或者反过来,从网卡出发,穿越协议栈,最后到达应用层。整个过程中,sk_buff 本身不变——变的只是 datatail 的位置,以及那些层层叠叠的报文头。"

林小源看着那些羊皮纸在协议栈中穿梭,忽然觉得它们像一条条河流中的船——船身不变,但每经过一个渡口就多挂一面旗、卸一面旗。旗越来越多,然后越来越少,最终只剩下船本身。

sk_buff 穿越协议栈的方式让他想起了渡口的船——船身不变,每过一个渡口就多挂一面旗、卸一面旗,最后只剩船本身。

c
/*
 * struct sk_buff 的关键字段:
 *
 *   head — 数据缓冲区的起始
 *   data — 当前数据的起始
 *   tail — 当前数据的结束
 *   end — 数据缓冲区的结束
 *
 *   len — 数据长度
 *   protocol — 协议类型
 *   dev — 关联的网络设备
 *   sk — 关联的 socket
 *
 * sk_buff 的结构:
 *   ┌──────────┐
 *   │ head     │ 缓冲区起始
 *   ├──────────┤
 *   │ mac_hdr  │ MAC 头
 *   ├──────────┤
 *   │ net_hdr  │ IP 头
 *   ├──────────┤
 *   │ data     │ 当前数据
 *   ├──────────┤
 *   │ tail     │ 数据结束
 *   ├──────────┤
 *   │ end      │ 缓冲区结束
 *   └──────────┘
 *
 * sk_buff 的操作:
 *   alloc_skb — 分配
 *   kfree_skb — 释放
 *   skb_put — 添加数据到尾部
 *   skb_push — 添加数据到头部
 *   skb_pull — 移除头部数据
 */

printf("=== sk_buff — 网络数据包的核心 ===\n\n");

printf("sk_buff 的结构:\n");
printf("  ┌──────────┐\n");
printf("  │ head     │ 缓冲区起始\n");
printf("  ├──────────┤\n");
printf("  │ mac_hdr  │ MAC 头 (14 字节)\n");
printf("  ├──────────┤\n");
printf("  │ net_hdr  │ IP 头 (20 字节)\n");
printf("  ├──────────┤\n");
printf("  │ data     │ 当前数据\n");
printf("  ├──────────┤\n");
printf("  │ tail     │ 数据结束\n");
printf("  ├──────────┤\n");
printf("  │ end      │ 缓冲区结束\n");
printf("  └──────────┘\n\n");

printf("--- sk_buff 的生命周期 ---\n");
printf("1. 分配: alloc_skb()\n");
printf("2. 填充数据\n");
printf("3. 添加协议头\n");
printf("4. 发送或接收\n");
printf("5. 释放: kfree_skb()\n\n");

printf("--- sk_buff 操作 ---\n");
printf("skb_put(skb, len):\n");
printf("  在尾部添加数据\n");
printf("  tail 向后移动\n\n");
printf("skb_push(skb, len):\n");
printf("  在头部添加数据\n");
printf("  data 向前移动\n\n");
printf("skb_pull(skb, len):\n");
printf("  移除头部数据\n");
printf("  data 向后移动\n\n");

printf("--- 协议栈的 sk_buff ---\n");
printf("发送时:\n");
printf("  应用层数据\n");
printf("  → 添加 TCP 头 (skb_push)\n");
printf("  → 添加 IP 头 (skb_push)\n");
printf("  → 添加 MAC 头 (skb_push)\n");
printf("  → 发送到网卡\n\n");
printf("接收时:\n");
printf("  网卡接收数据\n");
printf("  → 去除 MAC 头 (skb_pull)\n");
printf("  → 去除 IP 头 (skb_pull)\n");
printf("  → 去除 TCP 头 (skb_pull)\n");
printf("  → 传递给应用层\n");

道藏笔记

内核启示

sk_buff 就是数据包的肉身——协议栈里流转的所有数据都靠它承载。

那四个指针是精髓:headend 是缓冲区的物理边界,datatail 是当前有效数据的起止。加报文头的时候 data 往前移,剥报文头的时候 data 往后移,往尾部加数据用 tail 往后移——三个动作覆盖了协议栈里所有的数据操作,全程没有拷贝、没有重分配,只有指针在走。

sk_buff 还带一大堆元数据:关联的网络设备 、关联的 socket sk、协议类型 、数据长度 。发送队列、接收队列、重传队列都是用 sk_buff 串起来的双向链表。内核还有 slab 分配器专门给 sk_buff 做缓存,分配和释放都很快。

发送时从上往下层层加头,接收时从下往上层层剥头——sk_buff 本身不变,变的只是 datatail 的位置。


破关试炼

sk_buff 之试

sk_buff 里执行 skb_put() 往尾部添加数据时,向后移动的是哪个指针?

答对后才能继续滑动和进入下一章。

以修仙之名,悟内核之道