Linux 内核网络核心:sk_buff 深度解析

本文档基于 Linux 内核开发者视角,深入剖析网络子系统最核心的数据结构 sk_buff (socket buffer)。我们将从内存布局、生命周期、软硬件交互以及性能优化机制(如 GRO, Zero Copy, BQL/TSQ)等多个维度进行全景式描述。

在 Linux 内核中,网络包并非存储在一个巨大的连续结构体中,而是被拆分为两部分,分别独立分配内存:

  1. 控制结构体 (struct sk_buff)
    • 俗称: “壳”或“遥控器”。
    • 位置: 分配在 Slab Cache (skbuff_head_cache) 中。
    • 作用: 包含管理数据包所需的元数据(如指针、长度、接口、协议、引用计数等)。它的大小固定(约 232~256 字节),随内核配置而变。
  2. 数据缓冲区 (Data Buffer)
    • 俗称: “肉”或“电视机”。
    • 位置: 通过 kmalloc 分配的连续物理内存块(线性区)或物理页(非线性区)。
    • 作用: 存放真实的协议头(MAC/IP/TCP)和用户负载。

sk_buff 通过四个核心指针管理线性数据区

text

[ struct sk_buff ]
      |
      +--- head ----------------------------------+
      |                                           |
      +--- data ---------+                        |
      |                  |                        |
      +--- tail -----------------------+          |
      |                  |             |          |
      +--- end ------------------------------------------------+
                         |             |          |            |
      [   Headroom     ] [   Data      ] [ Tailroom ] [ skb_shared_info ]
      ^                  ^             ^            ^          ^
      |                  |             |            |          |
    head               data          tail          end      end 之后
  • head:线性内存块的物理起始地址。
  • data:当前协议层有效数据的起始位置。随协议层变化移动(push/pull)。
  • tail:当前有效数据的结束位置。
  • end:线性内存块的物理结束地址。
  • skb_shared_info位于 end 之后。它不占用线性区空间,但紧随其后。核心成员是 frags[] 数组,用于挂载非线性区(物理页数据)。
  • 线性区 (head ~ end)
    • 必须存在。用于存放所有的协议头部(MAC, IP, TCP)。
    • 原因:CPU 访问协议头时需要使用结构体指针强转(如 (struct iphdr *)skb->data),要求内存物理连续。
    • 小包场景:Payload 也直接存放在这里。
  • 非线性区 (frags / Page Based)
    • 可选。存放大数据负载(如文件传输、视频流)。
    • 物理形态skb_shared_info->frags[] 数组指向一组分散的物理页(Page)。
    • 硬件要求:网卡必须支持 Scatter-Gather (SG) DMA 才能直接读取这些分散的页。
    • 优势:实现 Zero Copy(零拷贝)和 Jumbo Frames(巨型帧)的基础。
  1. 分配 (Alloc)
    • 调用 send() 时,内核根据 MSS 和 MAX_HEADER 计算大小,分配 sk_buff 和数据区。
    • 调用 skb_reserve,移动 datatail 指针,预留 Headroom
    • 用户数据被拷贝(或零拷贝映射)进来。
  2. 克隆 (Clone)
    • TCP 为了可靠传输(重传),在交给 IP 层前执行 skb_clone
    • Clone 特性:只复制 sk_buff 结构体,不复制数据区。新旧 skb 指向同一块内存。
    • Original skb:留在发送队列,data 指针指向 Payload 开头。
    • Clone skb:用于传输,data 指针在 Headroom 中前移(skb_push)以填充协议头。
  3. 填头 (Header Filling)
    • TCP 层skb_push 腾出空间 -> 强转指针 (struct tcphdr*)skb->data -> 填入 Seq/Port。
    • IP 层skb_push -> 填入 IP 头。
    • MAC 层skb_push -> 填入 MAC 头。
  4. 硬件交互 (Ring Buffer)
    • 驱动将线性区和 Frags 的物理地址填入 TX Ring Buffer 的描述符。
    • 按“门铃”(写寄存器),通知网卡 DMA 搬运。
    • 发送完成:指 DMA 将数据搬入网卡 SRAM,内核释放 skb 内存。
  1. 硬件接收
    • 数据到达,网卡 DMA 将其写入预先分配的 Ring Buffer 内存块(可能是 Page)。
    • 触发 硬中断,随后调度 软中断 (NAPI)
  2. GRO (Generic Receive Offload)
    • 在 NAPI 层,将同属于一条流的多个小包合并
    • 保留第一个包的 skb 壳,将后续包的物理页(Frags)挂到第一个包的 shinfo 上,丢弃后续包的壳。
    • 效果:协议栈看到一个巨大的包(哪怕超过 MTU),带有 gso_size 标记。
  3. 剥离头部 (Header Stripping)
    • 进入协议栈,执行 skb_pull
    • data 指针向后移动,依次跳过 MAC、IP、TCP 头。被跳过的部分变成 Headroom。
  4. 接收缓冲
    • 数据包被挂入 Socket 的接收队列(有序链表乱序红黑树)。
    • 接收窗口:并非物理连续内存,而是通过 sk_rmem_alloc 原子计数器限制的总字节数。

在此部分,我们将之前的讨论整理为问答形式,解答关键的技术细节。

A: 不全是。head/data/tail/end 是指针。但像 len, protocol 是普通变量。还有一个特殊的 cb[48] (Control Buffer),它是一个直接存放在结构体内的 48 字节数组。

  • 真实数据:存在于 head 指向的另一块独立分配的内存(线性区)或 frags 指向的物理页(非线性区)。
  • TCP Seq:在处理过程中,Seq 为了访问速度,被拷贝存放在 cb 数组中,而不是每次都去读 data 里的 TCP 头。

A: 这是一个误区。

  • head~end:是内核分配的线性内存容器的总大小。
  • data~tail:才是当前协议层看到的有效数据。
  • 非线性数据:对于大包,大部分 Payload 存储在 end 指针之后的 skb_shared_info->frags 指向的物理页中,根本不在 head~end 之间。
  • 校验和:存放在 data~tail 包含的协议头内部,绝不会在 tail 后面。

A:

  • 目的:为了高效实现 TCP 重传、抓包(tcpdump)等功能。
  • 机制skb_clone完全不拷贝数据区。它只申请一个新的 sk_buff 结构体(元数据),让其 head 指针指向原有的数据区。
  • 副作用:Clone 出来的 skb 对数据区是 只读 (Read-Only) 的。如果需要修改数据,必须触发 CoW(写时复制)。
  • 引用计数
    • users:管 sk_buff 结构体。
    • dataref:管数据区。Clone 时 dataref++

A:

  • 它不是存放数据包的环,而是存放 描述符 (Descriptor) 的数组。
  • 描述符:是一个很小的结构体,里面只有物理地址指针和状态位。
  • 工作流
    • RX:驱动把空闲内存地址填入描述符 -> 网卡 DMA 填数据 -> CPU 读走。
    • TX:驱动把数据地址填入描述符 -> 网卡 DMA 读数据 -> 网卡发送。

A:

  • 硬中断极短。只负责告诉 CPU “DMA 搬运完了”,然后关闭网卡中断,唤醒软中断。它不拷贝数据。
  • 软中断 (NAPI)干重活。轮询 Ring Buffer,分配 sk_buff(如果需要),执行 GRO 合并,运行 TCP/IP 协议栈逻辑,直到数据放入 Socket 队列。

A: 核心在于利用 skb_shared_info->frags

  • Sendfile:内核直接把磁盘读取到的 Page Cache(物理页)的地址挂到 frags 数组里,完全跳过 CPU memcpy 到线性区的步骤。
  • Splice:在两个 Socket 或 Pipe 之间传递物理页的指针/引用,而不是搬运数据。

A: 通常没有。

  • 它只代表数据已经完整进入了 网卡的 SRAM (Tx FIFO)
  • 内核此时释放内存 (kfree_skb)。
  • 隐患:Bufferbloat(缓冲区膨胀)。数据可能堵在网卡硬件里,导致 Ping 值虚高且内核 QoS 失效。

A: 它们是为了解决上述的 Bufferbloat 问题。

  • BQL (Byte Queue Limits):在驱动层。动态调整提交给网卡硬件队列的字节数,保证网卡不饿死但也不多留数据。
  • TSQ (TCP Small Queues):在 TCP 层。限制每个 Socket 已发送但未释放(还在网卡或 QDisc 排队)的字节数。
  • 效果:构建了从网卡到应用的背压 (Backpressure) 机制,将排队控制在内核可控的层级(QDisc),从而降低延迟。

A: 不是。

  • 接收缓冲区是一堆散落在内存各处的 sk_buff,通过链表(有序)和红黑树(乱序)组织。
  • 窗口控制:通过原子变量 sk->sk_rmem_alloc 统计所有 skb 的 truesize 总和来实现逻辑限制。

总结建议: 理解 sk_buff 的关键在于建立 “元数据(Controller) 与 数据(View) 分离” 以及 “线性(Linear) 与 非线性(Paged) 混合” 的立体视图。网络优化的本质,往往就是减少元数据的分配(GRO/XDP)和减少数据的拷贝(Zero Copy)。