4. sk_buff 数据操作:skb_push、skb_pull、skb_put、skb_reserve、数据拷贝与共享

sk_buff 是 Linux 网络子系统中最重要的数据结构,它承载着从网络设备到协议栈、再到用户空间的全部网络数据。理解 sk_buff 的数据操作函数,是编写正确、高效的网络驱动的基础。本章将深入剖析 skb_push、skb_pull、skb_put、skb_reserve 四个核心指针操作函数,以及数据拷贝与共享机制。

4.1 sk_buff 内存布局与指针体系

每个 sk_buff 实例管理着一块连续的数据缓冲区,并通过四个关键指针来定位数据的不同区域:

指针 含义 指向位置
head 缓冲区起始地址 分配内存的最前端
data 当前有效数据的起始地址 协议头处理时动态移动
tail 当前有效数据的结束地址 数据追加时向后移动
end 缓冲区结束地址 分配内存的最末端

这四个指针将缓冲区划分为三个关键区域:

  • headroom:head 到 data 之间的预留空间,用于协议头封装。
  • data room:data 到 tail 之间的有效数据区域。
  • tailroom:tail 到 end 之间的可用空间,用于追加数据。
sk_buff 缓冲区内存布局 headroom data room(有效数据) tailroom head data tail end

4.2 核心数据操作函数

四个核心函数分别对应着数据指针的不同移动方向,它们共同构成了 sk_buff 数据操作的基础。

4.2.1 skb_put:向尾部追加数据

skb_put(skb, len) 将 tail 指针向后移动 len 字节,扩大有效数据区域。该函数返回移动前 tail 的位置,供调用者填充数据。常用于驱动在接收数据后,将数据从硬件缓冲区拷贝到 sk_buff 中。

// 驱动接收路径典型用法
struct sk_buff *skb = netdev_alloc_skb(dev, ETH_FRAME_LEN);
if (!skb)
    return -ENOMEM;

// 从硬件 FIFO 读取数据到 skb->data 区域
int pkt_len = read_from_hw_fifo(skb->data, max_len);

// 调整 tail 指针,标记有效数据长度
skb_put(skb, pkt_len);

// 此时 skb->len == pkt_len, skb->tail 指向数据末尾
注意:skb_put 不会检查 tailroom 是否足够。调用前必须确保 skb_tailroom(skb) >= len,否则会导致缓冲区溢出,破坏后续内存数据。

4.2.2 skb_push:向头部添加协议头

skb_push(skb, len) 将 data 指针向前移动 len 字节,在数据包前端预留空间。该函数返回移动后新的 data 指针,调用者可直接向该位置写入协议头。常用于协议栈向下传递时,各层添加自己的头部。

// 添加以太网头示例
struct ethhdr *eth = (struct ethhdr *)skb_push(skb, ETH_HLEN);
eth->h_proto = htons(ETH_P_IP);
memcpy(eth->h_source, dev->dev_addr, ETH_ALEN);
memcpy(eth->h_dest, dst_mac, ETH_ALEN);
危险操作:skb_push 要求 headroom 足够大。如果 headroom 不足,函数会触发 BUG_ON 导致内核崩溃。驱动开发中,应在分配 sk_buff 时通过 skb_reserve 预留足够的 headroom。

4.2.3 skb_pull:从头部移除数据

skb_pull(skb, len) 将 data 指针向后移动 len 字节,缩小有效数据区域。该函数返回移动前 data 指针的值。常用于协议栈向上传递时,剥离已处理完成的协议头。

// IP 层处理完 IP 头后,将 data 指针移向 TCP 头
struct iphdr *iph = (struct iphdr *)skb->data;
int ip_hdr_len = iph->ihl * 4;

// 剥离 IP 头,data 指向 TCP 头
__skb_pull(skb, ip_hdr_len);

// 此时 skb->data 指向 TCP 头起始位置

4.2.4 skb_reserve:预留头部空间

skb_reserve(skb, len) 在 sk_buff 分配后立即调用,将 data 和 tail 指针同时向前移动 len 字节,在缓冲区前端预留 headroom。该函数必须在填充数据之前调用,且只能调用一次。

// 分配 sk_buff 并预留 128 字节 headroom
struct sk_buff *skb = alloc_skb(ETH_FRAME_LEN + 128, GFP_ATOMIC);
if (!skb)
    return NULL;

// 预留 128 字节 headroom
skb_reserve(skb, 128);

// 此时 headroom = 128, tailroom = ETH_FRAME_LEN
// 后续 skb_push 最多可添加 128 字节协议头
sk_buff 核心操作函数示意图 初始状态: headroom (200B) data room (400B) skb_reserve(skb, 128): headroom (328B) data room (272B) data/tail 前移 128B skb_put(skb, 100): headroom (328B) data room (372B) tail 后移 100B skb_push(skb, 50): headroom (278B) data room (322B) data 前移 50B

4.3 数据拷贝与共享机制

网络数据包在协议栈中传递时,经常需要避免不必要的内存拷贝以提高性能。Linux 内核提供了多种数据共享与拷贝机制。

4.3.1 skb_clone:轻量级克隆

skb_clone(skb, gfp_mask) 创建一个新的 sk_buff 实例,但新实例与原始实例共享同一块数据缓冲区。克隆操作只复制 sk_buff 结构体本身(约 200-300 字节),不复制数据。克隆后的两个 sk_buff 通过引用计数共享数据,任何一方修改数据前必须确保自己是唯一引用者。

// 在桥接或数据包分发场景中使用
struct sk_buff *clone = skb_clone(skb, GFP_ATOMIC);
if (!clone) {
    // 克隆失败,原始 skb 继续处理
    goto original_path;
}

// clone 和 skb 共享同一数据缓冲区
// 各自可以独立修改 sk_buff 的指针(data/tail 等)
// 但不能同时修改数据内容

4.3.2 pskb_copy:部分拷贝

pskb_copy(skb, gfp_mask) 创建一个新的 sk_buff,并拷贝数据缓冲区的前面部分(通常是协议头区域)。该函数常用于协议转换场景,只需要修改头部而保留负载数据共享。

// NAT 或隧道封装场景
struct sk_buff *new_skb = pskb_copy(skb, GFP_ATOMIC);
if (!new_skb)
    return -ENOMEM;

// new_skb 拥有独立的头部区域,可以安全修改
// 负载数据仍与原始 skb 共享
struct iphdr *iph = (struct iphdr *)new_skb->data;
iph->saddr = new_saddr;  // 安全修改

4.3.3 skb_copy:完整拷贝

skb_copy(skb, gfp_mask) 创建一个全新的 sk_buff,并完整拷贝整个数据缓冲区。这是最安全的拷贝方式,但开销也最大。仅在确实需要独立数据副本时使用。

// 需要完全独立的数据副本时
struct sk_buff *copy = skb_copy(skb, GFP_KERNEL);
if (!copy)
    return -ENOMEM;

// copy 拥有完全独立的数据缓冲区
// 可以任意修改,不影响原始 skb

4.4 三种拷贝方式对比

函数 数据拷贝量 内存开销 适用场景
skb_clone 不拷贝数据 仅 sk_buff 结构体(~300B) 多播、桥接、数据包分发
pskb_copy 仅拷贝头部(通常 128-256B) 头部大小 + sk_buff 结构体 NAT、隧道封装、协议转换
skb_copy 完整拷贝整个数据缓冲区 完整数据大小 + sk_buff 结构体 需要完全独立数据副本
性能建议:在驱动和协议栈开发中,优先使用 skb_clone 和 pskb_copy。完整拷贝(skb_copy)应仅作为最后手段。对于 1500 字节的 MTU 数据包,skb_clone 比 skb_copy 快约 5-10 倍。

4.5 引用计数与数据安全

共享数据时,引用计数机制确保数据缓冲区在最后一个引用释放后才被回收。关键函数包括:

  • skb_get(skb):增加引用计数,标记新增一个使用者。
  • kfree_skb(skb):减少引用计数,当计数归零时释放数据缓冲区。
  • skb_shared(skb):检查 sk_buff 是否被多个实例共享。
  • skb_share_check(skb, gfp_mask):如果共享则自动克隆,返回可安全修改的 sk_buff。
// 安全修改共享 sk_buff 的标准模式
struct sk_buff *skb = receive_packet(dev);

// 检查是否被共享,如果共享则克隆
skb = skb_share_check(skb, GFP_ATOMIC);
if (!skb)
    return -ENOMEM;

// 此时 skb 是唯一引用,可以安全修改数据
unsigned char *data = skb->data;
data[0] = 0xFF;  // 安全修改
sk_buff 共享与拷贝机制对比 sk_buff A (clone) sk_buff B (clone) 共享数据缓冲区 refcount = 2 sk_buff C (pskb_copy) 独立头部 共享负载 sk_buff D (skb_copy) 独立数据缓冲区 refcount = 1

4.6 驱动开发中的最佳实践

关键经验:
  • 在驱动接收路径中,使用 netdev_alloc_skb 分配 sk_buff,该函数会自动考虑缓存对齐。
  • 分配时预留足够的 headroom(通常为 NET_IP_ALIGN + 16 字节),避免后续协议头封装时 headroom 不足。
  • 接收数据后立即调用 skb_put 设置数据长度,不要延迟操作。
  • 在将 sk_buff 传递给协议栈之前,确保 data 指针正确对齐(通常为 2 字节对齐)。
  • 使用 skb_share_check 替代手动检查引用计数,代码更简洁安全。
// 驱动接收函数的完整示例
static int my_rx_poll(struct napi_struct *napi, int budget) {
    struct my_priv *priv = container_of(napi, struct my_priv, napi);
    int work_done = 0;

    while (work_done < budget) {
        struct sk_buff *skb = netdev_alloc_skb(priv->dev, RX_BUF_SIZE);
        if (!skb)
            break;

        // 预留 2 字节对齐 + 16 字节额外 headroom
        skb_reserve(skb, NET_IP_ALIGN + 16);

        // 从硬件读取数据
        int pkt_len = hw_read_packet(priv->hw, skb->data, RX_BUF_SIZE);
        if (pkt_len <= 0) {
            dev_kfree_skb(skb);
            continue;
        }

        // 标记数据长度
        skb_put(skb, pkt_len);

        // 设置协议类型
        skb->protocol = eth_type_trans(skb, priv->dev);

        // 传递给协议栈
        netif_receive_skb(skb);
        work_done++;
    }

    return work_done;
}

4.7 常见错误与调试

错误模式 症状 根因 解决方案
headroom 溢出 内核 panic,data < head skb_push 时 headroom 不足 分配时加大 skb_reserve 预留
tailroom 溢出 内存损坏,后续 skb 数据异常 skb_put 时 tailroom 不足 检查 skb_tailroom 后再 put
共享数据误修改 数据包内容随机错误 未检查 skb_shared 直接修改 使用 skb_share_check 确保唯一
引用计数泄漏 内存泄漏,slab 缓存增长 kfree_skb 调用次数不足 检查所有路径确保释放
调试建议:在驱动开发初期,可以在关键操作点添加 skb_headroom(skb) 和 skb_tailroom(skb) 的打印,验证指针移动是否符合预期。使用 skb_orphan(skb) 可以断开 sk_buff 与 socket 的关联,避免在驱动层误操作 socket 相关字段。