网卡驱动发包流程与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 线性数据区的核心工具,理解它们的语义是掌握协议栈数据流的关键。
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() 宏访问。
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 映射的基础。