2. sk_buff 数据结构深度剖析:头部、数据区、分片与元数据管理

sk_buff 是 Linux 网络子系统中最重要的核心数据结构,它贯穿协议栈的每一层,承载着从套接字到网卡驱动的全部网络数据。理解 sk_buff 的内部布局、数据管理机制和元数据字段,是进行队列调度优化和驱动发包协同的前提。本章将从内存布局、数据指针、分片管理和关键元数据四个维度进行深度剖析。

2.1 sk_buff 整体内存布局

每个 sk_buff 实例在内存中由两部分组成:控制结构体(struct sk_buff)和数据缓冲区(skb->data 指向的线性内存区域)。控制结构体本身不存储数据载荷,而是通过指针和偏移量管理数据区、分片(frag_list)和分片页(frags)。

关键设计原则:sk_buff 通过“头部预留 + 数据共享 + 分片零拷贝”机制,避免数据在协议层间移动时的冗余拷贝,提升吞吐性能。
sk_buff 内存布局概览 struct sk_buff next / prev dev / sk head / data / tail / end len / truesize cb[] (控制块) destructor_arg frag_list / nr_frags skb_shared_info data 线性数据区 head ────────── │ 头部预留空间 │ data ────────── │ 协议头 + 载荷 │ tail ────────── │ 尾部填充空间 │ end ────────── frags 分片页数组 skb_shared_info nr_frags = N frags[0] → page frags[1] → page ... frags[N-1] → page frag_list (链表)

2.2 数据指针体系:head / data / tail / end

sk_buff 通过四个核心指针管理线性数据区,实现协议头的动态添加和移除,无需移动数据内容:

指针 指向位置 作用 典型操作
head 数据区起始 标记整个缓冲区的边界起点 skb_reserve() 预留头部空间
data 当前协议层数据起始 指向有效载荷的起始位置 skb_push() / skb_pull()
tail 当前协议层数据结束 指向有效载荷的末尾 skb_put() / skb_trim()
end 数据区末尾 标记整个缓冲区的边界终点 skb_shared_info 紧接其后
性能提示:在驱动发包路径中,data 指针的移动次数直接影响 cache 命中率。优化时尽量保持 data 指针在协议层间连续移动,避免跨 cacheline 跳跃。

2.3 分片管理:frags 与 frag_list

当数据包超过线性区容量或需要零拷贝传递时,sk_buff 使用两种分片机制:

  • frags[] 数组(页分片):存储在 skb_shared_info 结构中,每个 frag 指向一个物理页框(struct page)及偏移量。适用于大包发送(如 TCP GSO)和接收(GRO 合并)。
  • frag_list 链表(skb 分片):将多个 sk_buff 实例链接成一个逻辑包。适用于 IP 分片重组或某些虚拟设备(如 bridge)的转发场景。
sk_buff 分片管理结构 主 sk_buff 线性区 (小载荷) nr_frags = 3 frag_list → NULL frags[] 数组 frags[0] → Page A frags[1] → Page B frags[2] → Page C 分片使用场景 • frags[]: 大包发送 (GSO) 零拷贝,直接引用用户页 • frag_list: IP 分片重组 多个 skb 链接成逻辑包 • 驱动发包时遍历 frags 构造 DMA 描述符

2.4 关键元数据字段

除了数据指针和分片管理,sk_buff 还包含大量元数据字段,用于控制包的处理路径、优先级和统计。以下是在队列调度和驱动发包中最重要的字段:

字段 类型 作用 优化关注点
len __u32 数据包总长度(线性区 + 所有分片) 驱动 DMA 长度计算
truesize __u32 sk_buff 实际占用的内存总量(含协议头开销) 内存压力控制、QoS 限速
priority __u32 包的优先级(SO_PRIORITY 设置) 队列调度分类(tc)
mark __u32 防火墙标记(netfilter) 路由策略、QoS 分类
hash __u32 包哈希值(RSS / flow hash) 多队列分发、CPU 亲和性
queue_mapping __u16 发送队列索引(XPS / 驱动队列选择) 驱动发包队列绑定
skb_mstamp_ns u64 时间戳(纳秒,用于 pacing / BQL) Byte Queue Limits 计算
destructor_arg void * 析构回调参数(socket 内存释放) 零拷贝发送完成通知

2.5 skb_shared_info 结构

在 sk_buff 数据区的末尾(end 指针之后),紧跟着一个 skb_shared_info 结构。该结构不直接属于线性数据区,而是通过 end 指针偏移访问。其核心成员包括:

  • nr_frags:当前分片页的数量(最大 MAX_SKB_FRAGS,通常为 17)。
  • frags[]:分片页描述符数组,每个元素包含 page、page_offset、size。
  • frag_list:指向下一个 sk_buff 的链表指针(用于分片链)。
  • tx_flags:发送标志位(如 SKBTX_HW_TSTAMP 硬件时间戳)。
  • gso_segs:GSO 分段后的段数(用于统计和 TSO 卸载)。
常见陷阱:驱动在遍历 frags 时,必须检查 nr_frags 是否超过硬件支持的 DMA 段数。如果超过,需要先线性化(skb_linearize()),否则会导致 DMA 描述符溢出或硬件异常。

2.6 数据操作 API 速查

以下是在协议栈和驱动中常用的 sk_buff 数据操作函数,理解它们对 data/tail 指针的影响是正确编程的基础:

函数 操作 指针变化 典型用途
skb_reserve(skb, len) 在 data 前预留空间 data += len; tail += len 协议头预留(L2/L3/L4)
skb_put(skb, len) 在 tail 后添加数据 tail += len; len += len 接收数据填充
skb_push(skb, len) 在 data 前添加协议头 data -= len; len += len 发送时添加头部
skb_pull(skb, len) 从 data 后移除协议头 data += len; len -= len 接收时剥离头部
skb_trim(skb, len) 截断数据到指定长度 tail = data + len 接收截断
skb_copy_bits 从 skb 拷贝指定偏移数据 无变化 协议头解析

2.7 驱动发包视角的 sk_buff 遍历

在网卡驱动发送函数(如 ndo_start_xmit)中,驱动需要将 sk_buff 中的数据映射到 DMA 描述符。典型的遍历逻辑如下:

static netdev_tx_t my_start_xmit(struct sk_buff *skb, struct net_device *dev) {
    struct my_priv *priv = netdev_priv(dev);
    struct my_tx_desc *desc;
    dma_addr_t dma;
    int i, frags;

    // 1. 映射线性数据区
    dma = dma_map_single(dev->dev.parent, skb->data, skb_headlen(skb), DMA_TO_DEVICE);
    desc = &priv->tx_ring[priv->tx_next];
    desc->addr = dma;
    desc->len = skb_headlen(skb);

    // 2. 遍历分片页
    frags = skb_shinfo(skb)->nr_frags;
    for (i = 0; i < frags; i++) {
        const skb_frag_t *frag = &skb_shinfo(skb)->frags[i];
        dma = skb_frag_dma_map(dev->dev.parent, frag, 0,
                               skb_frag_size(frag), DMA_TO_DEVICE);
        desc = &priv->tx_ring[(priv->tx_next + 1 + i) % TX_RING_SIZE];
        desc->addr = dma;
        desc->len = skb_frag_size(frag);
    }

    // 3. 处理 frag_list(如果有)
    struct sk_buff *frag_skb = skb_shinfo(skb)->frag_list;
    while (frag_skb) {
        // 递归或链式映射 frag_list 中的每个 skb
        // 注意:frag_list 场景较少,驱动需谨慎处理
        frag_skb = frag_skb->next;
    }

    // 4. 触发 DMA 发送
    priv->tx_next = (priv->tx_next + 1 + frags) % TX_RING_SIZE;
    // ... 写 doorbell 寄存器
    return NETDEV_TX_OK;
}
优化要点:在遍历 frags 时,尽量保持 DMA 映射和描述符写入的 cache 局部性。对于多队列网卡,利用 skb->queue_mapping 直接选择发送队列,避免在驱动中再次计算哈希。

2.8 本章小结

sk_buff 的数据结构设计体现了 Linux 网络栈对性能和灵活性的极致追求:

  • 指针体系实现了零拷贝的协议头操作;
  • 分片管理支持大包零拷贝和高效内存利用;
  • 元数据字段为队列调度、QoS、时间戳和硬件卸载提供了基础。