sk_buff:Linux 内核为什么要给数据包套一个“档案袋”?
比特原点 · 网络技术系列 / 数据包的一生 03
在 Linux 内核里,数据包不是一串孤零零的字节,而是被装进 sk_buff 这个结构里一路传递。它像一份流动档案,记录了包的内容、来源、状态和后续处理线索。
上一篇我们讲了一包数据进入 Linux 的前半段:网卡收包、DMA 写内存、中断唤醒、NAPI 批量处理。
但当驱动把包交给内核协议栈时,Linux 并不是只拿着一段裸字节往上扔。
它会把这包数据装进一个结构里。
这个结构叫 sk_buff。
如果你把网络包理解成一份快递,那么 sk_buff 就不是快递盒本身,而是贴在快递上的整套档案袋:它记录包从哪来、到哪去、当前处理到哪一层、头部在哪里、数据在哪里、校验状态如何、走哪个设备、是否被克隆、是否被分片。
所以这篇的核心问题是:
为什么 Linux 内核不能只处理一段字节,而要给每个数据包套一个复杂的 sk_buff?
网络包不是静止的字节
很多人想象中的网络包,是一段固定字节:
以太网头、IP 头、TCP 头、应用数据,整整齐齐摆在那里。
但在 Linux 内核里,包是会移动、变形、剥皮、加头、克隆和排队的。
收包时,协议栈会一层层往里走。以太网层看完以太网头,IP 层看 IP 头,TCP 层看 TCP 头。每一层都要知道“我现在该从哪里开始读”。
发包时,方向反过来。应用先给数据,TCP 加 TCP 头,IP 加 IP 头,以太网层加以太网头,最后交给网卡。
如果每一层都重新复制一份数据,性能会很差。于是 Linux 需要一个结构,既能指向数据本体,又能灵活移动各种头部指针。
sk_buff 就是这个结构。
sk_buff 像一只可滑动的文件夹
sk_buff 里面最经典的一组概念,是几个指针:
你可以把它理解成一个缓冲区里的边界标记:head 指向缓冲区起点,end 指向缓冲区终点,data 指向当前协议层正在看的数据起点,tail 指向当前有效数据的末尾。协议栈不需要每次都搬动真实数据,很多时候只要调整这些边界,就能把“当前视角”从以太网头推进到 IP 头,再推进到 TCP 头。
这种设计的好处,是让协议处理更像移动书签,而不是每层都重抄一份文件。
发包时也类似,只是可能在数据前面预留空间,然后往前“推”出新的协议头。
这就是 sk_buff 的第一个价值:
它让协议栈可以少复制数据,多移动指针。
它还记录了数据包的“身世”
sk_buff 不只是指针集合,它还带着大量元数据。
比如这个包从哪个网卡进来、属于哪个协议、校验和是否已经被硬件验证、是否带 VLAN 信息、是否需要被路由转发、是否被防火墙、tc 或 eBPF 程序处理过、是否已经做过 GRO 合并、最后要走哪个输出设备。这些信息如果都靠每一层临时猜,协议栈会非常低效。内核需要一个统一的数据结构,让驱动、协议栈、流控、过滤、转发、socket 都能围绕它协作。
这就是第二个价值:
sk_buff 让数据包不只是数据,而是带上下文的数据。
没有上下文,内核只看到字节。
有了上下文,内核才知道这包字节在系统里处于什么位置。
为什么它会影响性能
既然 sk_buff 这么重要,它也会成为性能关键点。
构造 sk_buff 要成本。
分配和释放内存要成本。
克隆、引用计数、非线性缓冲区、分片、GSO、GRO,都要围绕它做协调。
所以高性能网络经常围绕一个问题展开:
能不能少分配、少复制、少碰缓存、少让 CPU 做重复工作?
比如 GRO 会把多个接收包合并成一个更大的 sk_buff,减少协议栈处理次数。
GSO/TSO 会让协议栈先按大包处理,晚一点再分段,减少 CPU 开销。
XDP 则更激进:在包还没变成完整 sk_buff 之前就提前处理,适合做早期丢包、重定向和负载均衡。
这也是为什么你会看到很多高性能网络技术都在谈“绕过 sk_buff”或“减少 sk_buff 开销”。
不是因为 sk_buff 设计差,而是因为它太通用。
通用意味着能处理复杂场景,也意味着极限性能下会有成本。
收包和发包都绕不开它
收包时,驱动从网卡接收队列拿到数据,构造 sk_buff,交给协议栈。
协议栈根据 sk_buff 里的协议头指针和元数据,逐层处理,最后把数据放进 socket 接收队列。
发包时,应用数据进入内核,TCP/IP 协议栈构造头部,路由、邻居子系统、qdisc、网卡驱动都围绕 sk_buff 操作。
转发时,它也很关键。包可能从一个网卡进来,被路由判断后从另一个网卡出去。中间可能经过 netfilter、tc、tunnel、VLAN、VXLAN、bridge、bond、team 等路径。
如果你学 Linux 网络,只记住命令,不理解 sk_buff,很多现象会很难解释。
比如抓包看到的校验和可能是错的,开启某些 offload 后包大小看起来会变大,GRO 会影响抓包观察,XDP 又为什么比传统协议栈更早。这些现象看似分散,背后都和 sk_buff、硬件 offload、协议栈处理位置有关。
sk_buff 的反常识
sk_buff 最反常识的地方在于,它不是网络包本身,而是 Linux 为了管理网络包而创造出来的“包的操作系统对象”。
一旦数据进入内核,就不再只是网线上的一串比特。它变成一个带身份、状态、位置、指针、历史和处理意图的对象。
这也是现代操作系统的典型风格:
硬件给你原始数据,内核把它包装成可管理的抽象。
文件不是磁盘扇区,进程不是 CPU 指令流,socket 不是网线,sk_buff 也不是裸包。
抽象让系统可扩展。
抽象也带来成本。
Linux 网络性能优化,很多时候就是在这两者之间找平衡。
最后用一句话收束:
sk_buff 是 Linux 网络协议栈里的数据包档案袋。没有它,协议栈很难协作;太依赖它,极限性能又会被它的通用性拖住。
我是 bitfan,关注比特原点。后面我会继续拆解底层数字世界,把那些平时藏在系统深处、但真正决定体验和性能的机制讲清楚。