3. sk_buff分配与释放
sk_buff 是 Linux 网络子系统中最重要的数据结构之一,它贯穿整个网络协议栈,承载着从驱动层到应用层的所有网络数据。理解 sk_buff 的分配、释放以及引用计数机制,是编写高效、无内存泄漏网络驱动的基石。
3.1 sk_buff 核心数据结构概览
sk_buff 结构体极其庞大(通常超过 200 字节),但其核心布局围绕“数据指针”和“头部预留”设计。驱动开发者最关心的字段如下:
| 字段 | 类型 | 说明 |
|---|---|---|
head |
unsigned char * |
指向数据缓冲区的起始地址(分配时固定) |
data |
unsigned char * |
指向当前有效数据的起始地址(可移动) |
tail |
unsigned char * |
指向当前有效数据的结束地址 |
end |
unsigned char * |
指向数据缓冲区的结束地址(分配时固定) |
len |
__u32 |
当前有效数据长度(tail - data) |
truesize |
__u32 |
包括 sk_buff 结构体本身在内的总内存占用 |
users |
refcount_t |
引用计数,决定何时释放 |
dev |
struct net_device * |
接收或发送该 skb 的网络设备 |
data、tail、len 三个指针。不要直接修改 head 和 end,它们由分配函数设定。
3.2 分配函数:alloc_skb 与 dev_alloc_skb
内核提供两个主要的 sk_buff 分配接口,驱动开发者必须根据上下文选择正确的函数。
3.2.1 alloc_skb
alloc_skb 是底层分配函数,直接调用 __alloc_skb。它从 slab 缓存中分配 sk_buff 结构体,并分配独立的数据缓冲区。
#include <linux/skbuff.h>
struct sk_buff *alloc_skb(unsigned int size, gfp_t priority);
- size:数据缓冲区大小(字节)。内核会自动对齐到
SKB_DATA_ALIGN。 - priority:内存分配标志,常用
GFP_KERNEL(可睡眠)或GFP_ATOMIC(原子上下文)。 - 返回值:成功返回 sk_buff 指针,失败返回
NULL。
GFP_ATOMIC。使用 GFP_KERNEL 会导致内核调度,引发死锁。
3.2.2 dev_alloc_skb
dev_alloc_skb 是专为网络设备驱动设计的封装函数,它在 alloc_skb 的基础上增加了 16 字节头部预留(NET_SKB_PAD),并强制使用 GFP_ATOMIC。
static inline struct sk_buff *dev_alloc_skb(unsigned int length)
{
return __dev_alloc_skb(length, GFP_ATOMIC);
}
- 适用场景:中断处理函数、NAPI 的 poll 回调、软中断上下文。
- 头部预留:分配后
data指针自动偏移 16 字节,为上层协议头预留空间。 - 性能优势:减少驱动代码中手动调用
skb_reserve的步骤。
3.2.3 分配函数对比
| 特性 | alloc_skb | dev_alloc_skb |
|---|---|---|
| 头部预留 | 无(需手动 skb_reserve) |
自动预留 16 字节 |
| 内存标志 | 调用者指定 | 强制 GFP_ATOMIC |
| 典型上下文 | 进程上下文、初始化 | 中断、软中断、NAPI |
| 失败处理 | 检查 NULL |
检查 NULL |
dev_alloc_skb。它既保证了原子性,又自动预留了头部空间,避免后续协议栈处理时重新调整指针。
3.3 释放函数:kfree_skb 与 dev_kfree_skb
释放 sk_buff 时,内核提供了两个主要接口。理解它们的区别对避免内存泄漏至关重要。
3.3.1 kfree_skb
kfree_skb 是核心释放函数。它首先检查引用计数,只有当引用计数降为 0 时,才真正释放 sk_buff 结构体和数据缓冲区。
void kfree_skb(struct sk_buff *skb);
- 引用计数检查:内部调用
kfree_skb_reason,递减引用计数。 - 副作用:如果 skb 被多个用户共享(如克隆),仅减少引用计数,不释放内存。
- 调试支持:内核配置
CONFIG_SKB_EXTENSIONS或CONFIG_DEBUG_NET时,会记录释放原因。
3.3.2 dev_kfree_skb
dev_kfree_skb 是 kfree_skb 的别名,专为驱动层设计。两者在功能上完全等价。
#define dev_kfree_skb(a) kfree_skb(a)
dev_kfree_skb 有 dev_kfree_skb_irq 和 dev_kfree_skb_any 变体,现代内核已统一为 kfree_skb。驱动代码中直接使用 kfree_skb 即可。
3.4 引用计数机制
sk_buff 的引用计数机制允许多个消费者共享同一个数据包,而无需复制数据。这在协议栈的多个层次同时处理同一包时(如桥接、抓包、多播)极为高效。
3.4.1 引用计数存储
引用计数存储在 sk_buff 结构体的 users 字段中,类型为 refcount_t(原子操作)。
struct sk_buff {
...
refcount_t users;
...
};
3.4.2 引用计数操作函数
| 函数 | 操作 | 说明 |
|---|---|---|
skb_get(skb) |
增加引用计数 | 获取一个新的引用,保证 skb 不会被释放 |
kfree_skb(skb) |
减少引用计数 | 引用计数为 0 时释放 |
skb_clone(skb, gfp) |
创建克隆,共享数据 | 新 skb 与原始 skb 共享数据缓冲区,各自独立引用计数 |
pskb_copy(skb, gfp) |
复制数据到新 skb | 完全复制数据,不共享 |
3.4.3 引用计数生命周期
一个典型的 skb 从驱动分配到协议栈处理完毕的引用计数变化如下:
- 分配:
dev_alloc_skb返回时,users初始化为 1。 - 驱动提交:调用
netif_receive_skb时,内核内部可能调用skb_get增加引用。 - 协议栈处理:TCP/IP 层处理时,可能再次克隆(如送往多个 socket)。
- 最终释放:所有消费者调用
kfree_skb,引用计数归零,内存回收。
netif_receive_skb 后,驱动不再拥有该 skb 的所有权,不应再读取或修改其内容。
3.5 sk_buff 分配与释放流程
下面的 SVG 图展示了驱动收包路径中 sk_buff 从分配到释放的完整生命周期:
3.6 驱动开发中的常见陷阱
- 内存泄漏:在错误路径中忘记调用
kfree_skb。例如,DMA 映射失败后未释放已分配的 skb。 - UAF(Use-After-Free):在调用
netif_receive_skb后继续访问 skb 数据。所有权已转移,应置 skb 指针为NULL。 - 上下文错误:在中断上下文使用
GFP_KERNEL分配 skb,导致内核调度崩溃。 - 头部预留不足:手动分配时未调用
skb_reserve,导致上层协议无法添加头部。
CONFIG_SKB_EXTENSIONS 和 CONFIG_DEBUG_NET,可以在 /sys/kernel/debug/skb/ 下查看 skb 分配和释放的统计信息,帮助定位泄漏。
3.7 本章小结
sk_buff 的分配与释放是网络驱动开发的基础操作。驱动开发者必须牢记:
- 收包路径使用
dev_alloc_skb,确保原子性和头部预留。 - 提交 skb 后立即放弃所有权,不再访问。
- 引用计数机制允许多个消费者共享数据,但驱动通常只持有单一引用。
- 使用
kfree_skb释放,内核会自动处理引用计数归零时的内存回收。