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 之间的可用空间,用于追加数据。
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 指向数据末尾
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);
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 字节协议头
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 结构体 | 需要完全独立数据副本 |
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; // 安全修改
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 相关字段。