2. sk_buff结构体详解

在 Linux 网络驱动开发中,struct sk_buff(套接字缓冲区)是最核心的数据结构。它贯穿整个网络协议栈,承载着从网卡收包到应用层交付、以及从应用层发包到网卡发送的完整生命周期。本章将深入剖析 sk_buff 的核心字段、数据指针管理机制以及头部与尾部操作,为后续理解 NAPI 收包流程奠定基础。

关键认知: sk_buff 不仅仅是一个“数据包容器”,它通过一组精巧的指针(head、data、tail、end)实现了零拷贝头部/尾部操作,这是网络性能优化的基石。

2.1 sk_buff 核心字段概览

sk_buff 结构体定义在 include/linux/skbuff.h 中,字段数量超过 100 个。但从驱动开发视角,我们将其分为四大类:数据管理、协议信息、设备信息和控制信息。下表列出了驱动开发中最常接触的核心字段:

分类 字段名 类型 说明
数据管理 head unsigned char * 数据缓冲区的起始地址(不可移动)
data unsigned char * 当前有效数据的起始地址(可移动)
tail unsigned char * 当前有效数据的结束地址(可移动)
end unsigned char * 数据缓冲区的结束地址(不可移动)
协议信息 protocol __be16 L3 协议类型(如 ETH_P_IP、ETH_P_ARP)
pkt_type __u8 包类型(PACKET_HOST、PACKET_BROADCAST 等)
ip_summed __u8 校验和状态(CHECKSUM_NONE / UNNECESSARY / COMPLETE)
设备信息 dev struct net_device * 接收或发送该包的网络设备
skb_iif int 接收设备的 ifindex(用于路由决策)
控制信息 len unsigned int 当前有效数据长度(data 到 tail 的字节数)
cb char[48] 控制块,供驱动或协议层临时存储私有信息
驱动提示: 在驱动收包时,protocol 字段通常由 eth_type_trans() 函数填充;ip_summed 用于指示硬件是否已经计算了校验和,直接影响协议栈的处理路径。

2.2 数据指针管理:head、data、tail、end 四指针模型

sk_buff 最精妙的设计在于通过四个指针管理数据缓冲区,实现了高效的头部/尾部空间预留和操作。下图展示了四指针在缓冲区中的布局关系:

sk_buff 四指针模型与缓冲区布局 headroom 有效数据 (len = 320) tailroom head data tail end headroom 大小 数据长度 (len) tailroom 大小 head 和 end 固定不变,data 和 tail 随协议层操作而移动

四指针的核心规则如下:

  • head 和 end:在 sk_buff 分配时确定,指向整个数据缓冲区的边界,永不移动。
  • data 和 tail:指向当前有效数据包的起始和结束位置,随协议层操作移动。
  • len:始终等于 tail - data,表示有效数据长度。
  • headroom:data - head,预留的头部空间,用于添加 L2/L3/L4 头部。
  • tailroom:end - tail,预留的尾部空间,用于添加数据或协议尾部。
性能关键: 通过移动 data 指针(而不是拷贝数据)来实现协议头部的添加和剥离,这是 sk_buff 实现零拷贝操作的核心机制。

2.3 头部与尾部操作:push、pull、put、reserve

sk_buff 提供了一系列内联函数来操作 data 和 tail 指针,实现头部/尾部的添加和移除。这些操作是驱动开发中必须熟练掌握的基础技能。

2.3.1 头部操作:push 与 pull

skb_push():在数据包头部添加数据。将 data 指针向前移动,增加 headroom 的消耗。

static inline unsigned char *skb_push(struct sk_buff *skb, unsigned int len)
{
    skb->data -= len;
    skb->len  += len;
    return skb->data;
}

skb_pull():从数据包头部移除数据。将 data 指针向后移动,释放 headroom。

static inline unsigned char *skb_pull(struct sk_buff *skb, unsigned int len)
{
    skb->data += len;
    skb->len  -= len;
    return skb->data;
}

2.3.2 尾部操作:put 与 reserve

skb_put():在数据包尾部添加数据。将 tail 指针向后移动,消耗 tailroom。

static inline unsigned char *skb_put(struct sk_buff *skb, unsigned int len)
{
    skb->tail += len;
    skb->len  += len;
    return skb->tail - len;
}

skb_reserve():在 sk_buff 初始化时预留 headroom。将 data 和 tail 同时向后移动,但不改变 len。

static inline void skb_reserve(struct sk_buff *skb, int len)
{
    skb->data += len;
    skb->tail += len;
}
常见错误: 驱动开发中,如果在调用 skb_push() 前没有确保 headroom 足够(即 skb_headroom(skb) >= len),会导致 data 指针越过 head 边界,造成缓冲区溢出。同样,skb_put() 前需检查 skb_tailroom(skb) >= len。

2.4 驱动收包场景中的指针操作流程

在典型的网卡驱动收包过程中,sk_buff 的指针操作遵循以下标准流程。下图展示了从 DMA 接收数据到协议栈交付的完整指针变化:

驱动收包流程中的 sk_buff 指针变化 步骤 1:分配 sk_buff 并预留头部空间 headroom (预留) tailroom (空) head data/tail end 步骤 2:DMA 将数据写入 tailroom(skb_put) headroom 有效数据 (len = 400) 剩余 head data tail end 步骤 3:协议栈逐层剥离头部(skb_pull) headroom L2 头 L3 头 有效载荷 剩余 head data (L3) tail end

驱动收包的标准操作序列如下:

  1. 分配 sk_buff:调用 dev_alloc_skb() 分配缓冲区,通常预留 2 字节(NET_IP_ALIGN)或更多 headroom。
  2. 预留头部空间:调用 skb_reserve(skb, headroom_size) 设置 data 和 tail 的起始位置。
  3. DMA 写入数据:硬件将数据包写入 skb->tail 指向的位置,驱动随后调用 skb_put(skb, pkt_len) 更新 tail 指针。
  4. 设置协议信息:调用 eth_type_trans(skb, dev) 填充 protocol 和 pkt_type 字段。
  5. 交付协议栈:调用 netif_receive_skb() 或 napi_gro_receive(),协议栈通过 skb_pull() 逐层剥离头部。
驱动最佳实践: 在 eth_type_trans() 内部会自动调用 skb_pull(skb, ETH_HLEN) 剥离以太网头部,因此驱动在调用该函数前不应手动 pull。同时,skb->data 在交付协议栈时指向 L3 头部起始位置。

2.5 常用辅助宏与内联函数

为了方便驱动开发,内核提供了以下常用辅助宏,用于获取 sk_buff 的各个空间大小:

宏/函数 计算公式 返回值
skb_headroom(skb) skb->data - skb->head 头部可用空间(字节)
skb_tailroom(skb) skb->end - skb->tail 尾部可用空间(字节)
skb_headlen(skb) skb->len(线性数据长度) 当前有效数据长度(字节)
skb_end_pointer(skb) skb->end 缓冲区结束地址
skb_tail_pointer(skb) skb->tail 当前数据结束地址
安全检查: 在调用 skb_push() 或 skb_put() 之前,务必使用 skb_headroom() 和 skb_tailroom() 进行边界检查。内核提供了 skb_cow()(copy on write)函数,在 headroom 不足时自动重新分配缓冲区,但驱动层应尽量避免触发此路径以保持性能。

2.6 本章小结

sk_buff 的四指针模型是 Linux 网络子系统高效处理数据包的基石。通过本章的学习,你应该掌握:

  • head、data、tail、end 四个指针的语义和不变性规则。
  • skb_push/pull/put/reserve 四个核心操作对指针和 len 的影响。
  • 驱动收包流程中如何正确使用这些操作来构建和交付数据包。
  • 使用辅助宏进行边界检查,避免缓冲区溢出等常见错误。