2. sk_buff结构体详解
在 Linux 网络驱动开发中,struct sk_buff(套接字缓冲区)是最核心的数据结构。它贯穿整个网络协议栈,承载着从网卡收包到应用层交付、以及从应用层发包到网卡发送的完整生命周期。本章将深入剖析 sk_buff 的核心字段、数据指针管理机制以及头部与尾部操作,为后续理解 NAPI 收包流程奠定基础。
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 最精妙的设计在于通过四个指针管理数据缓冲区,实现了高效的头部/尾部空间预留和操作。下图展示了四指针在缓冲区中的布局关系:
四指针的核心规则如下:
- head 和 end:在 sk_buff 分配时确定,指向整个数据缓冲区的边界,永不移动。
- data 和 tail:指向当前有效数据包的起始和结束位置,随协议层操作移动。
- len:始终等于
tail - data,表示有效数据长度。 - headroom:
data - head,预留的头部空间,用于添加 L2/L3/L4 头部。 - tailroom:
end - tail,预留的尾部空间,用于添加数据或协议尾部。
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:调用
dev_alloc_skb()分配缓冲区,通常预留 2 字节(NET_IP_ALIGN)或更多 headroom。 - 预留头部空间:调用
skb_reserve(skb, headroom_size)设置 data 和 tail 的起始位置。 - DMA 写入数据:硬件将数据包写入
skb->tail指向的位置,驱动随后调用skb_put(skb, pkt_len)更新 tail 指针。 - 设置协议信息:调用
eth_type_trans(skb, dev)填充 protocol 和 pkt_type 字段。 - 交付协议栈:调用
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 的影响。
- 驱动收包流程中如何正确使用这些操作来构建和交付数据包。
- 使用辅助宏进行边界检查,避免缓冲区溢出等常见错误。