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,协议栈各层可以高效地添加或剥离协议头。
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 的典型处理流程:
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等函数封装了指针操作,并包含边界检查。