Linux内核学习轨迹第八部:核心数据结构sk_buff全字段深度解析(第三节)
3. 核心数据结构sk_buff全字段深度解析
3.1 sk_buff的核心设计思想
1.零拷贝核心设计:预留头尾空间
2.全链路通用抽象
3.分层协议头兼容
4.高效内存管理
5.多核可扩展性
6.全功能可扩展
3.2 sk_buff全字段深度拆解
struct sk_buff {
// ===================== 1. 基础管理核心字段 =====================
struct sk_buff *next; // 链表下一个节点,用于将skb加入各种队列
struct sk_buff *prev; // 链表上一个节点
struct sock *sk; // 所属的Socket传输控制块,对应用户态的Socket
ktime_t tstamp; // 数据包到达/发送的时间戳,用于延迟统计、TC流量控制
netdev_features_t features; // 网卡硬件卸载特性标记,如校验和卸载、TSO/GSO分段卸载
struct net_device *dev; // 数据包进出的网卡设备结构体
unsigned int len; // 数据包总长度:data区长度 + 分片长度(线性区+非线性区)
unsigned int data_len; // 非线性数据区长度(分片数据)
__u16 mac_len; // 链路层MAC头长度
__u16 hdr_len; // 所有协议头的总长度(MAC+IP+TCP/UDP)
union {
__wsum csum; // 数据包校验和
__u32 csum_offset; // 校验和偏移量,用于硬件校验和卸载
};
__u32 hash; // 数据包的哈希值,用于RSS多队列分发、连接跟踪
__be16 vlan_proto; // VLAN协议类型,通常为0x8100
__u16 vlan_tci; // VLAN标签控制信息,包含VLAN ID、优先级
refcount_t users; // 【核心】引用计数,管理skb的生命周期
unsigned int truesize; // skb占用的总内存大小:结构体大小 + 数据缓冲区大小
bool cloned; // 标记该skb是否是克隆出来的
bool nohdr; // 标记是否没有协议头空间
pkt_type_t pkt_type; // 数据包类型:单播、广播、多播、本机包等
__u8 ip_summed; // 校验和状态:无需校验、校验完成、需要软件校验
// ===================== 2. 数据包缓冲区核心指针 =====================
// 【最核心的四个指针,决定了数据包的内存布局】
unsigned char *head; // 数据缓冲区的起始地址(整个缓冲区的头部)
unsigned char *data; // 数据包有效数据的起始地址
unsigned char *tail; // 数据包有效数据的结束地址
unsigned char *end; // 数据缓冲区的结束地址(整个缓冲区的尾部)
// ===================== 3. 分层协议头专用指针 =====================
// 【标准协议头指针,替代老旧的h/nh/th联合体】
int mac_header; // 链路层MAC头在data区的偏移量,-1表示无
int network_header; // 网络层IP头在data区的偏移量,-1表示无
int transport_header; // 传输层TCP/UDP头在data区的偏移量,-1表示无
// ===================== 4. 路由与转发控制字段 =====================
union {
__be32 daddr; // 目的IPv4地址
struct in6_addr daddr6; // 目的IPv6地址
};
union {
__be32 saddr; // 源IPv4地址
struct in6_addr saddr6; // 源IPv6地址
};
int skb_iif; // 数据包入站网卡的ifindex索引
__u32 mark; // 数据包标记,用于Netfilter/TC/路由策略
__u32 skb_priority; // 数据包QoS优先级,对应TC流量控制
struct dst_entry *dst; // 路由项缓存,记录数据包的路由信息
// ===================== 5. 网络命名空间与安全字段 =====================
struct net *ns; // 所属的网络命名空间,容器/虚拟化隔离的核心
struct sec_path *sp; // IPsec安全路径,用于加密/解密处理
__u32 secmark; // 安全标记,用于SELinux/AppArmor安全策略
// ===================== 6. 扩展功能与控制块 =====================
char cb[48]; // 【核心扩展】控制块缓冲区,各层协议私有存储
unsigned char *cb_data; // 扩展控制块指针,用于eBPF/XDP扩展
struct skb_shared_info *shinfo; // 共享信息结构体,用于分片、克隆、时间戳
} __randomize_layout;
3.3 核心字段深度解析
3.3.1 最核心的四个缓冲区指针
|
指针 |
核心含义 |
固定/可变 |
|
head |
整个数据缓冲区的 起始地址 ,缓冲区分配后就固定不变 |
固定 |
|
end |
整个数据缓冲区的 结束地址 ,缓冲区分配后就固定不变 |
固定 |
|
data |
数据包 有效数据的起始地址 ,各层添加/移除协议头时会移动 |
可变 |
|
tail |
数据包 有效数据的结束地址 ,添加尾部数据时会移动 |
可变 |
- 缓冲区总大小 = end - head,分配skb时确定,全程固定;
- 有效数据总长度 = tail - data,对应skb->len的线性区长度;
- headroom(头部空闲空间)= data - head,用于各层添加协议头;
- tailroom(尾部空闲空间)= end - tail,用于添加尾部数据、填充对齐。
3.3.2 引用计数users与生命周期管理
- 调用alloc_skb()分配skb时,引用计数初始化为1;
- 调用skb_get()时,引用计数+1,用于多场景共享同一个skb;
- 调用kfree_skb()时,引用计数-1,只有当引用计数降到0时,内核才会释放skb结构体和数据缓冲区;
- 克隆skb(skb_clone())时,只会复制skb结构体,共享数据缓冲区,引用计数+1,不会复制数据,实现零拷贝;
- 只有当需要修改共享的数据缓冲区时,才会执行复制(skb_copy()),也就是「写时复制」机制。
3.3.3 分层协议头指针
- mac_header:链路层MAC头的偏移量,网卡驱动收到数据包时填充,对应以太网头;
- network_header:网络层IP头的偏移量,网络层处理时填充,对应IPv4/IPv6头;
- transport_header:传输层TCP/UDP头的偏移量,传输层处理时填充,对应TCP/UDP头。
// 获取以太网MAC头指针
struct ethhdr *eth_hdr(const struct sk_buff *skb);
// 获取IPv4头指针
struct iphdr *ip_hdr(const struct sk_buff *skb);
// 获取TCP头指针
struct tcphdr *tcp_hdr(const struct sk_buff *skb);
// 获取UDP头指针
struct udphdr *udp_hdr(const struct sk_buff *skb);
3.3.4 控制块cb[48]:协议栈的私有存储
- 各层协议可以自由使用这个缓冲区,存储自己的私有数据,无需额外分配内存;
- 生命周期仅在当前层处理期间有效,跨层处理时会被覆盖,比如链路层使用后,网络层可以重新覆盖使用;
- 典型场景:TCP协议用它存储序列号、拥塞控制信息;TC流量控制用它存储分类结果;Netfilter用它存储连接跟踪信息。
3.3.5 共享信息结构体shinfo
- 数据包的分片信息(非线性区的分片数组),用于IP分片、TSO/GSO大段卸载;
- 数据缓冲区的引用计数,克隆skb时共享;
- 时间戳、硬件卸载相关的扩展信息;
- destructor函数指针,用于skb释放时的自定义清理逻辑。
3.4 sk_buff的完整内存布局
┌─────────────────────────────────────────────────────────────────┐
│ struct sk_buff 结构体本身(slab缓存分配) │
└───────────────────────────────┬─────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────┐
│ 数据缓冲区(kmalloc/DMA分配,全程固定大小) │
├───────────┬───────────────────┬─────────────────┬───────────────┤
│ headroom │ 有效数据data区 │ tailroom空闲区 │ shinfo共享区 │
│ (data-head)│ (tail-data) │ (end-tail) │ (end之后) │
│ 预留头部空间│ 各层协议头+载荷 │ 预留尾部空间 │ 分片/克隆信息 │
└───────────┴───────────────────┴─────────────────┴───────────────┘
◄─────────────────── end - head 缓冲区总大小 ────────────────────►
- 网卡驱动收包:分配skb时,预留足够的headroom(通常32字节+),把数据包从DMA缓冲区拷贝到data区,设置tail指向数据末尾,填充mac_header,然后交给链路层;
- 链路层处理:解析以太网头,无需拷贝数据,直接通过eth_hdr()获取MAC头,处理VLAN标签后,将data指针向后移动MAC头长度,把数据包交给网络层;
- 网络层处理:通过ip_hdr()获取IP头,解析路由信息,处理IP分片,无需拷贝数据,将data指针向后移动IP头长度,交给传输层;
- 传输层处理:通过tcp_hdr()/udp_hdr()获取传输层头,校验端口号,处理序列号、重传,无需拷贝数据,将data指针向后移动传输层头长度,最终把载荷数据放入Socket的接收队列;
- 发送流程反向操作:应用层写入数据后,各层从headroom中预留空间,向前移动data指针,添加对应的协议头,全程无需拷贝数据。
3.5 sk_buff的完整生命周期与核心操作API
阶段1:分配skb
|
API |
核心作用 |
适用场景 |
|
struct sk_buff *alloc_skb(unsigned int size, gfp_t priority) |
最基础的分配函数,分配指定大小的skb和数据缓冲区 |
通用场景,协议栈各层使用 |
|
struct sk_buff *dev_alloc_skb(unsigned int size) |
网卡驱动专用分配函数,自动预留16字节的headroom,使用原子内存分配 |
网卡驱动收包场景,中断上下文使用 |
|
struct sk_buff *netdev_alloc_skb(struct net_device *dev, unsigned int size) |
带网卡设备的分配函数,支持per-CPU缓存,性能更高 |
现代网卡驱动收包场景 |
|
struct sk_buff *skb_clone(struct sk_buff *skb, gfp_t priority) |
克隆skb,仅复制结构体,共享数据缓冲区,引用计数+1 |
多路径转发、防火墙镜像、不需要修改数据的场景,零拷贝 |
|
struct sk_buff *skb_copy(const struct sk_buff *skb, gfp_t priority) |
完整复制skb,包括结构体和数据缓冲区,生成独立的新skb |
需要修改数据内容、且原skb被共享的场景 |
阶段2:预留空间与数据填充
|
API |
核心作用 |
使用场景 |
|
void skb_reserve(struct sk_buff *skb, int len) |
预留headroom,将 data 和 tail 指针同时向后移动len字节 |
分配skb后,提前预留协议头空间,网卡驱动必用 |
|
unsigned char *skb_put(struct sk_buff *skb, unsigned int len) |
扩展tailroom,将 tail 指针向后移动len字节,返回移动前的tail指针 |
向数据包尾部添加数据,填充载荷时使用 |
|
unsigned char *skb_push(struct sk_buff *skb, unsigned int len) |
扩展data区头部,将 data 指针向前移动len字节,返回新的data指针 |
各层添加协议头时使用,发送流程核心API |
|
unsigned char *skb_pull(struct sk_buff *skb, unsigned int len) |
收缩data区头部,将 data 指针向后移动len字节,返回新的data指针 |
各层解析完协议头后,移除协议头,接收流程核心API |
|
void skb_trim(struct sk_buff *skb, unsigned int len) |
收缩data区尾部,将 tail 指针向前移动,把数据包截断到len字节 |
移除数据包尾部的多余数据 |
// 分配1500字节的skb,对应以太网MTU
struct sk_buff *skb = dev_alloc_skb(1500);
// 预留2字节的headroom,让IP头4字节对齐(以太网头14字节,14+2=16)
skb_reserve(skb, 2);
// 从DMA缓冲区拷贝1500字节数据到skb的data区
unsigned char *data = skb_put(skb, 1500);
dma_copy_from_device(data, rx_dma_buffer, 1500);
阶段3:协议栈各层处理
- 各层通过标准API获取协议头,解析协议字段;
- 通过skb_pull()移除已处理的协议头,交给下一层;
- 通过skb_push()添加新的协议头,交给上一层;
- 路由、防火墙、TC流量控制处理,修改skb的mark、优先级等字段;
- 分片与重组:当数据包超过MTU时,通过skb_frag_t实现IP分片,无需拷贝数据。
阶段4:数据包发送/接收完成
- 接收流程:传输层处理完成后,skb被放入Socket的接收队列,等待用户态读取,用户态读取完成后,释放skb;
- 发送流程:网卡驱动发送完成后,触发中断,调用dev_kfree_skb()释放skb。
阶段5:克隆与复制
阶段6:释放skb
|
API |
核心作用 |
适用场景 |
|
void kfree_skb(struct sk_buff *skb) |
基础释放函数,引用计数-1,为0时释放内存 |
协议栈各层通用场景 |
|
void dev_kfree_skb(struct sk_buff *skb) |
网卡驱动专用释放函数,中断上下文安全 |
网卡驱动发送完成/收包失败场景 |
|
void consume_skb(struct sk_buff *skb) |
正常消费释放,和kfree_skb功能一致,仅用于统计区分 |
数据包正常处理完成后释放 |
3.6 工程实践与避坑指南
3.6.1 高频致命问题与避坑指南
1. headroom不足导致的内核panic
- 分配skb时,必须通过skb_reserve()提前预留足够的headroom,至少预留ETH_HLEN + MAX_IP_HDR_LEN + MAX_TCP_HDR_LEN(约100字节);
- 调用skb_push()前,必须检查skb_headroom(skb)是否大于等于要添加的长度;
- 网卡驱动收包时,必须预留足够的headroom,避免后续协议栈添加VLAN、VXLAN等隧道头时空间不足。
2. skb内存泄漏
- 严格遵循「谁分配,谁释放」的原则,所有分配的skb,无论处理成功还是失败,都必须有对应的释放路径;
- 克隆skb时,必须保证每个克隆的skb都有对应的释放操作;
- 排查方法:开启内核slub调试,通过kmemleak检测内存泄漏,定位未释放的skb;
- 监控告警:配置slab缓存使用率监控,skbuff_head_cache对象数异常增长时触发告警。
3. 引用计数错误导致的use-after-free内核panic
- 禁止直接修改skb->users引用计数,必须使用内核提供的skb_get()和kfree_skb()标准API,内部已经处理了内存屏障和竞态;
- 多CPU场景下,skb在不同CPU之间传递时,必须保证引用计数正确,避免一个CPU释放了skb,另一个CPU还在访问;
- 开启内核CONFIG_DEBUG_SKB调试选项,提前发现引用计数错误。
4. 克隆与复制混淆导致的数据修改异常
- 仅当不需要修改数据时,使用skb_clone()零拷贝克隆;
- 当需要修改数据包内容时,必须先调用skb_cow()(copy on write),检查是否需要复制数据缓冲区,再进行修改;
- 禁止直接修改克隆后的skb的数据区,必须遵循写时复制原则。
5. 中断上下文使用睡眠分配导致的死锁
- 中断上下文分配skb时,必须使用GFP_ATOMIC优先级,或者使用dev_alloc_skb()/netdev_alloc_skb(),内部已经使用了原子分配;
- 进程上下文可以使用GFP_KERNEL,获得更高的分配成功率。
3.6.2 性能优化最佳实践
- 使用per-CPU缓存分配skb:现代网卡驱动使用netdev_alloc_skb(),利用per-CPU缓存分配skb,减少锁竞争,提升分配效率;
- 优先使用克隆而非复制:不需要修改数据的场景,一律使用skb_clone()零拷贝克隆,避免数据拷贝带来的CPU开销;
- 合理预留headroom:分配skb时一次性预留足够的headroom,避免后续协议处理时需要重新分配、拷贝数据;
- 开启硬件卸载特性:利用网卡的TSO/GSO、校验和卸载特性,减少skb的分片与校验和计算开销,降低CPU占用;
- 避免频繁分配/释放skb:高并发场景下,使用skb池缓存空闲的skb,避免频繁的slab分配/释放,减少内存碎片与CPU开销。
3.6.3 问题排查常用工具
- slabtop:查看skb相关的slab缓存使用情况,排查内存泄漏;
- kmemleak:内核内存泄漏检测工具,定位未释放的skb;
- trace-cmd/perf:跟踪skb的分配、克隆、释放流程,定位引用计数错误;
- dropwatch:监控内核中skb被丢弃的位置,定位丢包问题;
- CONFIG_DEBUG_SKB:内核编译选项,开启skb的调试功能,提前发现越界、引用计数错误。
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐



所有评论(0)