3. sk_buff 分配与释放:alloc_skb、dev_alloc_skb、kfree_skb、引用计数机制

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 从驱动分配到协议栈处理完毕的引用计数变化如下:

  1. 分配:dev_alloc_skb 返回时,users 初始化为 1。
  2. 驱动提交:调用 netif_receive_skb 时,内核内部可能调用 skb_get 增加引用。
  3. 协议栈处理:TCP/IP 层处理时,可能再次克隆(如送往多个 socket)。
  4. 最终释放:所有消费者调用 kfree_skb,引用计数归零,内存回收。
驱动常见错误: 驱动在提交 skb 后继续访问 skb 数据。正确的做法是:调用 netif_receive_skb 后,驱动不再拥有该 skb 的所有权,不应再读取或修改其内容。

3.5 sk_buff 分配与释放流程

下面的 SVG 图展示了驱动收包路径中 sk_buff 从分配到释放的完整生命周期:

sk_buff 生命周期:从分配到释放 dev_alloc_skb users = 1, 头部预留 16B 驱动填充数据 skb_put / skb_reserve netif_receive_skb 所有权转移给内核 协议栈处理 TCP/IP 解析 skb_clone / skb_get users 递增 kfree_skb users--, 归零时释放 关键规则: 1. 驱动调用 netif_receive_skb 后,不再拥有 skb 所有权。 2. 引用计数为 0 时,kfree_skb 才会真正释放内存。 3. 克隆 skb 共享数据缓冲区,但各自维护引用计数。 4. 中断上下文必须使用 dev_alloc_skb(GFP_ATOMIC)。

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 释放,内核会自动处理引用计数归零时的内存回收。