网卡驱动发包流程与Linux协议栈sk_buff队列调度 - 第3节

3.1 sk_buff 的基本定义

sk_buff(套接字缓冲区)是 Linux 内核网络子系统中最核心的数据结构,贯穿整个协议栈的收发包流程。每一个待发送或已接收的网络数据包,在内核中都以一个 sk_buff 实例为载体进行传递和操作。

从驱动开发的角度看,sk_buff 是网卡驱动与协议栈之间的数据交换契约:驱动从 Ring Buffer 中取出数据后填充 sk_buff 并上送协议栈;协议栈将待发送数据封装为 sk_buff 后交由驱动进行 DMA 传输。

核心要点:sk_buff 是一个“元数据 + 数据指针”的复合结构,本身不直接存储大量数据,而是通过指针管理数据缓冲区。

3.1.1 结构体概览

sk_buff 定义在 include/linux/skbuff.h 中,其成员可大致分为以下几类:

分类 关键成员 作用
缓冲区指针 head, data, tail, end 管理线性数据区的起始、有效数据、空闲位置和结束
分页数据 data_len, nr_frags, frag_list 管理分页数据区的长度、片段数量和链表
协议信息 protocol, transport_header, network_header, mac_header 记录各层协议头偏移
队列管理 next, prev, list 用于将 sk_buff 链接到队列中
设备信息 dev, sk 关联的网络设备和套接字

3.2 线性数据区与分页数据区

一个 sk_buff 所管理的数据缓冲区由两部分组成:线性数据区和分页数据区。这种设计是为了兼顾小包的高效处理和大包的零拷贝需求。

3.2.1 线性数据区

线性数据区是一块连续的内存区域,由 head 和 end 指针界定其范围。协议头、控制信息以及较小的数据载荷都存储在此区域中。线性数据区的特点是访问速度快,适合频繁修改的协议头操作。

  • 分配方式:通过 kmalloc 或 netdev_alloc_skb 分配。
  • 典型大小:通常为 1.5KB(标准以太网 MTU)或更大(如 2KB + 对齐)。
  • 适用场景:TCP 小段、ACK 包、DNS 查询等小包。

3.2.2 分页数据区

分页数据区由多个内存页片段(fragments)组成,用于承载大块数据,例如文件传输中的 TCP 负载。分页数据区通过 skb_shared_info 结构管理,该结构位于 end 指针之后。

  • 分配方式:通过 alloc_page 分配页面,或直接引用用户空间页面(零拷贝)。
  • 管理结构:skb_shared_info 包含 nr_frags(片段数量)和 frags[](片段数组)。
  • 适用场景:大文件发送、视频流、大数据块传输。
驱动开发提示:在编写网卡驱动时,需要根据 skb->data_len 判断是否包含分页数据。如果 data_len > 0,则必须遍历 skb_shinfo(skb)->frags[] 来获取所有数据页,不能仅依赖线性区。

3.3 head/data/tail/end 指针的语义与操作

这四个指针是操作 sk_buff 线性数据区的核心工具,理解它们的语义是掌握协议栈数据流的关键。

sk_buff 线性数据区指针布局 线性数据缓冲区(由 head 和 end 界定) head 分配起始 data 有效数据起始 tail 有效数据结束 end 分配结束 有效数据区:data ~ tail 头部预留(headroom) 尾部预留(tailroom)

3.3.1 指针语义

指针 语义 指向位置 典型操作
head 缓冲区起始地址 分配的内存块起点 skb_reserve() 预留头部空间
data 有效数据起始地址 协议头开始处 skb_push() 添加协议头
tail 有效数据结束地址 数据载荷末尾 skb_put() 添加数据
end 缓冲区结束地址 分配的内存块终点 检查剩余空间

3.3.2 核心操作函数

内核提供了一系列内联函数来操作这四个指针,驱动开发中经常用到:

// 预留头部空间:将 data 和 tail 同时向后移动 len 字节
static inline void skb_reserve(struct sk_buff *skb, int len)
{
    skb->data += len;
    skb->tail += len;
}

// 在数据区尾部添加数据:将 tail 向后移动 len 字节,返回原 tail 位置
static inline unsigned char *skb_put(struct sk_buff *skb, unsigned int len)
{
    unsigned char *tmp = skb->tail;
    skb->tail += len;
    return tmp;
}

// 在数据区头部添加协议头:将 data 向前移动 len 字节,返回新 data 位置
static inline unsigned char *skb_push(struct sk_buff *skb, unsigned int len)
{
    skb->data -= len;
    return skb->data;
}

// 从数据区头部移除数据:将 data 向后移动 len 字节
static inline void skb_pull(struct sk_buff *skb, unsigned int len)
{
    skb->data += len;
}
常见错误:在调用 skb_push() 之前必须确保 data - head >= len,否则会破坏缓冲区边界。同样,skb_put() 需要确保 end - tail >= len。驱动开发中应使用 skb_headroom() 和 skb_tailroom() 检查剩余空间。

3.3.3 指针关系与空间计算

通过四个指针的差值可以快速计算各区域大小:

  • 头部预留空间(headroom):skb->data - skb->head
  • 有效数据长度(len):skb->tail - skb->data(线性区部分)
  • 尾部预留空间(tailroom):skb->end - skb->tail
  • 总缓冲区大小:skb->end - skb->head

3.4 分页数据区的管理结构

当数据包包含分页数据时,sk_buff 通过 skb_shared_info 结构来管理。该结构位于 end 指针之后,通过 skb_shinfo() 宏访问。

sk_buff 完整数据区结构(线性 + 分页) 线性数据区(head ~ end) skb_shared_info nr_frags | frags[] | frag_list frags[0]:Page 1 frags[1]:Page 2 frags[2]:Page 3 frags[3]:Page 4 分页数据区特点 • 每个 frag 指向一个独立的内存页,不要求物理连续 • nr_frags 最大值为 MAX_SKB_FRAGS(通常为 17)

3.4.1 skb_shared_info 关键字段

字段 类型 说明
nr_frags unsigned int 当前使用的分页片段数量
frags[MAX_SKB_FRAGS] skb_frag_t 分页片段数组,每个元素包含页指针、偏移和长度
frag_list sk_buff * 用于链接多个 sk_buff 的链表(GSO 分段场景)
gso_size unsigned int GSO 分段大小(用于硬件分片卸载)

3.5 驱动开发中的实际应用

在网卡驱动的发包路径中,sk_buff 的指针操作是高频动作。以下是一个典型的发包流程中指针变化示例:

// 1. 协议栈构造 sk_buff,data 指向 IP 头
//    head = 0x1000, data = 0x1040, tail = 0x1100, end = 0x1800

// 2. 驱动添加以太网头(14 字节)
skb_push(skb, 14);
//     data = 0x1032, tail = 0x1100(不变)
//     此时 data 指向完整的以太网帧头

// 3. 检查是否需要分片(如果数据大于 MTU)
if (skb->len > mtu) {
    // 硬件支持 TSO 时,设置 gso_size 后直接提交
    skb_shinfo(skb)->gso_size = mss;
} else {
    // 小包直接通过 DMA 发送
    dma_addr = dma_map_single(dev, skb->data, skb_headlen(skb), DMA_TO_DEVICE);
}

// 4. 如果包含分页数据,需要遍历 frags
for (i = 0; i < skb_shinfo(skb)->nr_frags; i++) {
    frag = &skb_shinfo(skb)->frags[i];
    dma_addr = skb_frag_dma_map(dev, frag, 0, skb_frag_size(frag), DMA_TO_DEVICE);
    // 将 dma_addr 填入 TX 描述符
}
关键指标:在驱动开发中,skb_headlen(skb) 返回线性区有效数据长度(tail - data),skb->len 返回总数据长度(线性区 + 分页区)。当 skb->data_len 为 0 时,所有数据都在线性区。

3.6 本章小结

  • sk_buff 是 Linux 网络数据包在内核中的标准表示形式,驱动开发必须熟练掌握其结构。
  • 线性数据区由 head/data/tail/end 四个指针管理,分别对应缓冲区起始、有效数据起始、有效数据结束和缓冲区结束。
  • skb_push()、skb_put()、skb_pull()、skb_reserve() 是操作这四个指针的核心函数。
  • 分页数据区通过 skb_shared_info 管理,用于承载大块数据,驱动在发送大包时必须遍历 frags[] 数组。
  • 理解指针语义和空间计算是避免缓冲区溢出、正确实现 DMA 映射的基础。