4. sk_buff结构体源码分析(上)

struct sk_buff 是 Linux 内核网络协议栈中最重要的数据结构,它贯穿整个网络数据包的收发路径。理解其核心字段的内存布局与语义,是掌握协议栈运作机制的基础。本章聚焦于 head、data、tail、end、len、truesize 这六个关键字段,分析它们如何协同描述一个数据包在内存中的位置与大小。

4.1 sk_buff 线性内存区域布局

每个 sk_buff 结构体都关联一段连续的内存区域,称为 线性数据区。四个指针字段 head、data、tail、end 将这片区域划分为三个逻辑段:协议头预留区、实际数据区、尾部可用空间。

核心概念: head 指向整块内存的起始位置,end 指向末尾。data 指向当前有效数据的起始位置,tail 指向有效数据的结束位置。通过移动 data 和 tail,协议栈各层可以高效地添加或剥离协议头。
sk_buff 线性内存区域布局 sk_buff 线性内存区域布局 协议头预留区 实际数据区 (data ~ tail) 尾部可用空间 head data tail end head 和 end 固定,data 和 tail 随协议层处理而移动

4.2 核心字段语义详解

下表汇总了六个核心字段的类型、含义及典型操作场景:

字段名 类型 含义 典型操作
head unsigned char * 指向线性数据区的起始地址,分配后不变 skb_reserve() 在 head 后预留空间
data unsigned char * 指向当前有效协议数据的起始位置 skb_push() 前移 data 添加协议头
tail unsigned char * 指向当前有效数据的结束位置 skb_put() 后移 tail 添加数据
end unsigned char * 指向线性数据区的末尾地址,分配后不变 用于边界检查,防止越界
len __u32 当前有效数据的长度,等于 tail - data 协议栈各层通过 skb->len 获取包长
truesize __u32 sk_buff 结构体及其关联数据的总内存占用 内存回收与统计,kfree_skb() 时使用

4.3 指针操作与数据长度关系

四个指针字段之间存在严格的数学关系,这些关系是协议栈操作的基础:

  • head ≤ data ≤ tail ≤ end:指针顺序保证内存访问安全。
  • len = tail - data:数据长度由 data 和 tail 的差值直接计算。
  • headroom = data - head:头部预留空间,用于添加协议头。
  • tailroom = end - tail:尾部可用空间,用于添加数据载荷。
编程提示: 内核提供了 skb_headroom() 和 skb_tailroom() 内联函数来获取 headroom 和 tailroom 的大小,避免直接操作指针。

4.4 len 与 truesize 的差异

len 和 truesize 是容易混淆的两个字段,它们的区别至关重要:

字段 计算方式 包含内容 典型值示例
len tail - data 仅包含有效载荷数据 一个 64 字节的 TCP 数据段,len = 64
truesize sizeof(*skb) + end - head + 分片内存 sk_buff 结构体 + 线性数据区 + 所有分片页 同上场景,truesize 可能为 512 或更大

truesize 通常远大于 len,因为它包含了协议头预留空间、结构体开销以及可能的非线性分片内存。内核内存管理子系统依赖 truesize 进行精确的内存记账和回收。

4.5 协议层处理中的指针移动

数据包在协议栈各层传递时,通过移动 data 和 tail 指针来实现协议头的添加与剥离,而无需复制数据。下图展示了从 L2 到 L4 的典型处理流程:

协议层处理中的指针移动 协议层处理中的 data / tail 指针移动 阶段1: L2 驱动接收 (data 指向 MAC 头) MAC头 IP头 + TCP头 + 数据 阶段2: IP 层处理 (data 移动到 IP 头) IP头 TCP头 + 数据 阶段3: TCP 层处理 (data 移动到 TCP 头) TCP头 应用数据 每经过一层,data 指针向前移动(skb_pull),剥离当前层协议头 发送方向则相反:data 向后移动(skb_push),添加协议头

4.6 关键操作函数与字段关系

内核提供了一系列内联函数来操作 sk_buff 的指针和长度字段,这些函数是协议栈编程的基础:

函数 操作 影响字段 使用场景
skb_reserve(skb, len) 将 data 和 tail 同时向后移动 len 字节 data += len, tail += len 分配后预留协议头空间
skb_push(skb, len) 将 data 向前移动 len 字节 data -= len, len += len 发送时添加协议头
skb_pull(skb, len) 将 data 向后移动 len 字节 data += len, len -= len 接收时剥离协议头
skb_put(skb, len) 将 tail 向后移动 len 字节 tail += len, len += len 添加数据到尾部
skb_trim(skb, len) 将 tail 向前移动到指定位置 tail = data + len, len 重新计算 截断数据包
注意: 所有指针操作函数都包含边界检查(如 skb_push 会检查 headroom 是否足够),如果空间不足会触发 BUG_ON。在编写内核模块时,务必确保调用前有足够的预留空间。

4.7 本章小结

  • head 和 end 定义了线性数据区的固定边界,分配后不变。
  • data 和 tail 定义了当前有效数据的范围,随协议层处理动态移动。
  • len 表示有效数据长度,truesize 表示总内存占用,两者差异反映了协议头预留和结构体开销。
  • 通过移动指针而非复制数据,协议栈实现了高效的协议头添加与剥离。
  • 内核提供的 skb_push/pull/put/reserve 等函数封装了指针操作,并包含边界检查。