4. sk_buff 结构体详解(下):链表管理与克隆机制
在 Linux 内核协议栈中,sk_buff 并非孤立存在。网络数据包在协议层之间传递、在队列中等待调度、被多个消费者共享时,必须依赖高效的链表管理和引用计数机制。本章深入剖析 sk_buff 的链表组织方式、sk_buff_head 结构的设计意图,以及克隆(clone)与拷贝(copy)的底层实现差异。
4.1 sk_buff 链表结构:next/prev 与 sk_buff_head
每个 sk_buff 结构体内部包含两个链表指针:
struct sk_buff {
/* 链表指针 */
struct sk_buff *next; /* 指向链表中的下一个 sk_buff */
struct sk_buff *prev; /* 指向链表中的上一个 sk_buff */
/* 所属链表头 */
struct sk_buff_head *list; /* 指向该 sk_buff 所属的链表头 */
/* ... 其他字段 ... */
};
然而,内核并不直接使用 sk_buff 的 next/prev 作为链表头,而是定义了一个专用的 sk_buff_head 结构:
struct sk_buff_head {
/* 链表节点,必须放在头部,用于类型转换 */
struct sk_buff *next;
struct sk_buff *prev;
__u32 qlen; /* 队列中 sk_buff 的数量 */
spinlock_t lock; /* 保护链表的自旋锁 */
};
qlen(队列长度)和 lock(自旋锁),而普通 sk_buff 节点不包含这些字段,从而节省内存。通过 list 指针,每个 sk_buff 都能快速找到其所属队列的锁和长度信息。
下图展示了 sk_buff_head 与 sk_buff 节点之间的双向链表关系:
4.2 链表操作 API
内核提供了一套标准的 sk_buff_head 操作函数,驱动和协议栈代码通过它们管理数据包队列:
| 函数 | 功能 | 是否加锁 |
|---|---|---|
skb_queue_head_init() |
初始化 sk_buff_head,将 next/prev 指向自身,qlen 置 0 | 否(初始化时) |
skb_queue_head() |
将 sk_buff 插入到队列头部 | 是(内部加锁) |
skb_queue_tail() |
将 sk_buff 追加到队列尾部 | 是(内部加锁) |
skb_dequeue() |
从队列头部取出一个 sk_buff | 是(内部加锁) |
skb_dequeue_tail() |
从队列尾部取出一个 sk_buff | 是(内部加锁) |
skb_queue_len() |
返回队列长度(直接读取 qlen) | 否(读操作) |
skb_queue_splice() |
将一个队列的所有节点拼接到另一个队列 | 需要调用者保证 |
skb_queue_head() 等函数内部已加锁,但 skb_queue_splice() 等批量操作需要调用者自行持有锁。驱动开发中,如果同时操作多个队列,务必使用 spin_lock_bh() 保护临界区,避免与软中断(如 NET_RX_SOFTIRQ)产生死锁。
4.3 引用计数与克隆机制
网络数据包在协议栈中经常需要被多个消费者同时处理。例如,一个接收到的数据包既要递交给原始套接字(raw socket),又要进入正常的 TCP/IP 协议栈。如果直接拷贝整个 sk_buff 及其数据缓冲区,开销极大。Linux 内核通过引用计数和克隆机制实现高效的共享。
4.3.1 引用计数:dataref
每个 sk_buff 的数据缓冲区(skb_shared_info 区域)维护一个引用计数字段 dataref:
struct skb_shared_info {
atomic_t dataref; /* 引用计数 */
/* ... 其他字段(frags, frag_list 等) ... */
};
当多个 sk_buff 结构共享同一个数据缓冲区时,dataref 记录共享该缓冲区的 sk_buff 数量。初始值为 1,每克隆一次增加 1,每释放一个引用减少 1。当 dataref 降为 0 时,数据缓冲区才被真正释放。
4.3.2 克隆(Clone) vs 拷贝(Copy)
内核提供了两种共享数据的方式:
| 特性 | skb_clone() | skb_copy() |
|---|---|---|
| 数据缓冲区 | 共享(不拷贝数据) | 独立(完整拷贝数据) |
| sk_buff 结构 | 新分配一个 sk_buff 结构 | 新分配 sk_buff + 数据缓冲区 |
| 引用计数变化 | dataref 增加 1 | dataref 不变(新缓冲区) |
| 修改数据 | 不允许(共享数据只读) | 允许(独立副本) |
| 内存开销 | 低(仅 200~300 字节) | 高(数据包大小 + 结构体) |
| 典型场景 | 多消费者只读路径(tcpdump、原始套接字) | 需要修改数据包内容(NAT、防火墙) |
skb_clone() 被大量用于流量控制(如 Qdisc 队列克隆)、多播转发、以及 tcpdump 抓包。驱动开发者应优先使用克隆而非拷贝,除非确实需要修改数据包内容。
下图展示了克隆与拷贝在内存布局上的本质区别:
4.4 克隆的深层机制:skb_shared_info 与 destructor_arg
当调用 skb_clone() 时,内核执行以下关键步骤:
- 分配新的 sk_buff 结构:从 slab 缓存中分配一个新的
sk_buff实例。 - 复制元数据:将原始
sk_buff中的指针(如head、data、tail、end)和标志位复制到新结构中。 - 增加引用计数:通过
atomic_inc(&skb_shinfo(skb)->dataref)将dataref加 1。 - 设置克隆标志:在新
sk_buff的cloned字段置 1,表示这是一个克隆副本。 - 设置 destructor_arg:克隆的
sk_buff的destructor_arg指向原始sk_buff,用于在释放时回调通知。
/* skb_clone 核心逻辑(简化版) */
struct sk_buff *skb_clone(struct sk_buff *skb, gfp_t gfp_mask)
{
struct sk_buff *n;
n = alloc_skb(0, gfp_mask); /* 只分配 sk_buff 结构,不分配数据缓冲区 */
if (!n)
return NULL;
/* 复制 sk_buff 元数据 */
memcpy(n, skb, sizeof(*n));
/* 增加数据缓冲区引用计数 */
atomic_inc(&(skb_shinfo(skb)->dataref));
/* 标记为克隆 */
n->cloned = 1;
/* 设置 destructor_arg 指向原始 skb */
n->destructor_arg = skb;
/* 初始化链表指针 */
n->next = n->prev = NULL;
n->list = NULL;
return n;
}
dev_queue_xmit() 内部会调用 skb_clone() 来为每个传输队列创建副本。如果驱动实现了自定义的 ndo_start_xmit 函数,并且需要将数据包分发给多个硬件队列,应优先使用 skb_clone() 而非 skb_copy(),以避免不必要的内存拷贝开销。
4.5 引用计数与释放流程
当 sk_buff 被释放时(通过 kfree_skb() 或 dev_kfree_skb()),内核执行以下逻辑:
- 检查克隆标志:如果
cloned为 1,则调用skb_release_data()。 - 减少引用计数:
atomic_dec(&skb_shinfo(skb)->dataref)。 - 判断是否释放数据缓冲区:如果
dataref降为 0,则释放skb->head指向的数据缓冲区。 - 释放 sk_buff 结构:将
sk_buff结构归还 slab 缓存。
void kfree_skb(struct sk_buff *skb)
{
if (!skb)
return;
/* 如果 skb 是克隆的,减少数据缓冲区引用计数 */
if (skb->cloned)
skb_release_data(skb);
/* 释放 sk_buff 结构本身 */
kmem_cache_free(skbuff_cache, skb);
}
static void skb_release_data(struct sk_buff *skb)
{
struct skb_shared_info *shinfo = skb_shinfo(skb);
/* 减少引用计数,如果降为 0 则释放数据缓冲区 */
if (atomic_dec_and_test(&shinfo->dataref))
skb_free_head(skb); /* 释放 skb->head 指向的内存 */
}
4.6 本章小结
| 知识点 | 关键内容 |
|---|---|
| sk_buff_head | 专用链表头结构,包含 qlen(队列长度)和 lock(自旋锁),与普通 sk_buff 节点区分。 |
| 链表操作 API | skb_queue_head/tail、skb_dequeue、skb_queue_len 等,内部已加锁,支持多核并发。 |
| 引用计数 dataref | 位于 skb_shared_info 中,记录共享数据缓冲区的 sk_buff 数量,降为 0 时释放缓冲区。 |
| skb_clone() | 共享数据缓冲区,仅分配 sk_buff 结构,dataref 加 1,适用于只读场景。 |
| skb_copy() | 独立分配数据缓冲区,完整拷贝数据,适用于需要修改数据包的场景。 |
| 释放流程 | kfree_skb() 先检查克隆标志,减少 dataref,必要时释放数据缓冲区,最后释放 sk_buff 结构。 |