3. sk_buff 结构体深度解析

sk_buff 是 Linux 内核网络栈中最核心的数据结构,贯穿协议栈的每一层。在 Android 内核中,驱动与网络栈之间的数据交换、协议头操作、内存管理都围绕 sk_buff 展开。本章从内存布局、指针操作、分配释放、克隆与拷贝四个维度进行深度剖析。

关键认知:sk_buff 不是简单的“数据包缓冲区”,而是一个带有元数据、指针控制、引用计数的复合结构体。理解其内存布局是优化驱动数据通路的前提。

3.1 sk_buff 内存布局

sk_buff 结构体由两部分组成:控制结构体(sk_buff 自身)和数据缓冲区(sk_buff 的 data 指针指向的区域)。控制结构体包含协议栈各层所需的元数据,数据缓冲区则存储实际的网络数据包。

内存布局要点:sk_buff 的控制结构体与数据缓冲区在物理上可能不连续,但通过指针形成逻辑上的完整数据包描述。
sk_buff 内存布局全景 sk_buff 控制结构体 struct sk_buff *next struct sk_buff *prev struct sock *sk ktime_t tstamp __u16 len __u16 data_len __u16 mac_len __u32 priority __u8 protocol __u8 pkt_type sk_buff_data_t transport_header sk_buff_data_t network_header sk_buff_data_t mac_header atomic_t users 数据缓冲区(线性区域 + 分页区域) headroom(头部预留空间) data 区域(协议头 + 载荷) tailroom(尾部预留空间) frag_list / frags(分页数据) head data tail end headroom 用于协议头封装,tailroom 用于协议头解析,frag 区域支持零拷贝
内存区域 指针范围 用途 典型大小
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 指针在数据追加时右移。
sk_buff 数据指针操作流程 初始分配 headroom | data区域 | tailroom head data tail end 添加 L2 头 L2头 | headroom | data区域 | tailroom head data tail end 添加 L3 头 L3头 | L2头 | data区域 head data tail end 接收路径(剥离头) L2头 | L3头 | L4头 | 载荷 head data tail/end 剥离 L2 头 L3头 | L4头 | 载荷 head data tail/end 剥离 L3 头 L4头 | 载荷 head data tail/end
驱动开发提示:在驱动中接收数据包时,通常先调用 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)是网络栈中实现数据包共享与隔离的关键机制。克隆共享数据缓冲区,拷贝则创建独立的数据副本。

sk_buff 克隆 vs 拷贝 原始 sk_buff users = 1 数据缓冲区 dataref = 1 skb_clone() 克隆 sk_buff users = 1 数据缓冲区(共享) dataref = 2 skb_copy() 拷贝 sk_buff users = 1 数据缓冲区(独立) dataref = 1 克隆:仅复制 sk_buff 控制结构体,共享数据缓冲区(dataref++),适合多路径分发 拷贝:复制 sk_buff 控制结构体 + 数据缓冲区,完全独立,适合数据修改场景

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 结构体