3. sk_buff 结构体深度解析
sk_buff 是 Linux 内核网络栈中最核心的数据结构,贯穿协议栈的每一层。在 Android 内核中,驱动与网络栈之间的数据交换、协议头操作、内存管理都围绕 sk_buff 展开。本章从内存布局、指针操作、分配释放、克隆与拷贝四个维度进行深度剖析。
关键认知:sk_buff 不是简单的“数据包缓冲区”,而是一个带有元数据、指针控制、引用计数的复合结构体。理解其内存布局是优化驱动数据通路的前提。
3.1 sk_buff 内存布局
sk_buff 结构体由两部分组成:控制结构体(sk_buff 自身)和数据缓冲区(sk_buff 的 data 指针指向的区域)。控制结构体包含协议栈各层所需的元数据,数据缓冲区则存储实际的网络数据包。
内存布局要点:sk_buff 的控制结构体与数据缓冲区在物理上可能不连续,但通过指针形成逻辑上的完整数据包描述。
| 内存区域 | 指针范围 | 用途 | 典型大小 |
|---|---|---|---|
| headroom | head → data | 协议头封装预留空间(如添加以太网头、IP 头) | 128 ~ 256 字节 |
| data 区域 | data → tail | 当前协议层可见的数据(协议头 + 载荷) | MTU 相关(通常 1500 字节) |
| tailroom | tail → end | 协议头解析或数据追加预留空间 | 128 ~ 256 字节 |
| frag 区域 | end 之后 | 分页数据(支持零拷贝、GSO、SG) | 动态 |
3.2 数据指针操作
sk_buff 通过四个核心指针(head、data、tail、end)管理数据缓冲区。协议栈各层通过移动 data 和 tail 指针来实现协议头的添加与剥离,无需拷贝数据。
核心原则:协议栈向下传递时,data 指针左移(添加头);协议栈向上传递时,data 指针右移(剥离头)。tail 指针在数据追加时右移。
驱动开发提示:在驱动中接收数据包时,通常先调用
skb_reserve(skb, headroom_size) 预留头部空间,然后通过 skb_put(skb, len) 将数据放入 data 区域。发送时使用 skb_push(skb, header_len) 添加协议头。
3.2.1 核心指针操作 API
| API 函数 | 操作 | 指针变化 | 典型场景 |
|---|---|---|---|
skb_put(skb, len) |
在 data 区域尾部添加数据 | tail 右移 len | 驱动接收数据后填充缓冲区 |
skb_push(skb, len) |
在 data 区域头部添加数据 | data 左移 len | 协议栈添加协议头 |
skb_pull(skb, len) |
从 data 区域头部移除数据 | data 右移 len | 协议栈剥离协议头 |
skb_reserve(skb, len) |
预留 headroom 空间 | data 和 tail 同时右移 len | 分配 skb 后预留头部空间 |
skb_trim(skb, len) |
截断 data 区域到指定长度 | tail 左移至 data+len | 接收超长包时截断 |
3.3 sk_buff 的分配与释放
sk_buff 的分配与释放涉及 slab 分配器、内存缓存和引用计数。Android 内核中,网络数据包的频繁分配与释放是性能关键路径。
3.3.1 分配路径
// 核心分配函数
struct sk_buff *alloc_skb(unsigned int size, gfp_t priority)
{
struct sk_buff *skb;
// 1. 从 skbuff_fclone_cache 或 skbuff_head_cache 分配 sk_buff 结构体
skb = kmem_cache_alloc_node(skbuff_head_cache, priority, ...);
// 2. 分配数据缓冲区(线性区域)
skb->head = kmalloc_reserve(size, priority, ...);
// 3. 初始化指针
skb->data = skb->head;
skb->tail = skb->head;
skb->end = skb->head + size;
// 4. 初始化引用计数
atomic_set(&skb->users, 1);
return skb;
}
// 驱动常用快捷分配
struct sk_buff *netdev_alloc_skb(struct net_device *dev, unsigned int length)
{
// 考虑设备对齐要求,分配 cacheline 对齐的 skb
return __netdev_alloc_skb(dev, length, GFP_ATOMIC);
}
驱动开发注意:在中断上下文或软中断中分配 sk_buff 必须使用
GFP_ATOMIC 标志,禁止使用 GFP_KERNEL,否则可能导致内核睡眠。
3.3.2 释放路径
void kfree_skb(struct sk_buff *skb)
{
// 1. 检查引用计数
if (!atomic_dec_and_test(&skb->users))
return; // 还有其他引用者,不释放
// 2. 释放分页数据(frags)
skb_release_data(skb);
// 3. 释放 sk_buff 结构体到 slab 缓存
kmem_cache_free(skbuff_head_cache, skb);
}
// 驱动常用释放
void dev_kfree_skb_any(struct sk_buff *skb)
{
// 根据上下文选择直接释放或延迟释放
if (in_irq() || irqs_disabled())
__dev_kfree_skb_irq(skb, ...);
else
kfree_skb(skb);
}
3.3.3 分配与释放性能对比
| 分配方式 | 延迟(典型值) | 适用场景 | 备注 |
|---|---|---|---|
alloc_skb() |
~200 ns | 通用分配 | 从 slab 缓存分配 |
netdev_alloc_skb() |
~180 ns | 驱动接收路径 | 考虑设备对齐 |
__netdev_alloc_skb() |
~160 ns | 高性能驱动 | 直接指定 GFP 标志 |
build_skb() |
~50 ns | 零拷贝接收 | 复用驱动预分配缓冲区 |
3.4 克隆与拷贝
sk_buff 的克隆(clone)与拷贝(copy)是网络栈中实现数据包共享与隔离的关键机制。克隆共享数据缓冲区,拷贝则创建独立的数据副本。
3.4.1 克隆(skb_clone)
struct sk_buff *skb_clone(struct sk_buff *skb, gfp_t gfp_mask)
{
struct sk_buff *n;
// 1. 仅分配 sk_buff 结构体(不分配数据缓冲区)
n = kmem_cache_alloc(skbuff_head_cache, gfp_mask);
if (!n)
return NULL;
// 2. 复制 sk_buff 控制结构体内容
memcpy(n, skb, sizeof(*n));
// 3. 增加数据缓冲区引用计数
atomic_inc(&(skb_shinfo(skb)->dataref));
// 4. 初始化克隆 skb 的引用计数
atomic_set(&n->users, 1);
return n;
}
克隆特性:
- 克隆后的 sk_buff 共享同一块数据缓冲区
- 修改克隆 skb 的 data/tail 指针不影响原始 skb
- 修改数据缓冲区内容会影响所有共享者
- 典型应用:网桥转发、数据包嗅探、多播分发
3.4.2 拷贝(skb_copy)
struct sk_buff *skb_copy(const struct sk_buff *skb, gfp_t gfp_mask)
{
struct sk_buff *n;
int headerlen;
// 1. 分配新的 sk_buff 结构体