#220810 sk_buff 结构体及其基本操作¶
这个结构是什么干什么用的¶
sk_buff 结构体是内核协议栈中最重要的一个结构体,协议栈中大部分函数都会有一个 struct sk_buff *skb
的参数,这个结构体对应的变量名一般都是 skb,所以也叫 skb 结构体。网络包在协议栈中的上传下达都是通过这个结构体,每一个协议层中对网络包操作也就是操作 sk_buff 这个结构体。
虽然名字中带 buff 字样,但 sk_buff 本身并不保存任何网络包数据,它只是个元结构体(metadata struct),所有网络包数据都包含在它关联的各种 buffer 中。
sk_buff 是个特别复杂的结构体,光结构体定义就有 200 多行,本文只是简单的介绍下它的 基本 结构、 基本 操作。
https://elixir.bootlin.com/linux/v5.19/source/include/linux/skbuff.h#L1004
如何使用 sk_buff 构建网络包¶
第一步,创建一个新 skb:
skb = alloc_skb(size, GFP_KERNEL);
我们重点关注 skb 结构体中的以下几个字段:
struct sk_buff {
// ...
unsigned int len,
data_len;
// ...
sk_buff_data_t tail;
sk_buff_data_t end;
unsigned char *head,
*data;
// ...
}
刚创建好的 skb 内存结构大致如下所示:
head、data、tail 这三个指针一开始都指向一块内存 buffer 的开始地址,end 指向结束地址,len 为 0。这块 buffer 的后面是一个 struct skb_shared_info
结构体(暂时不解释这个结构体是干什么的,后面用到时解释)。
第二步,预留一部分空间给各种协议头(因为构建包的过程一般是用户数据先写,然后才加各种协议头,防止频繁重新分配内存)。
skb_reserve(skb, header_len);
这个函数主要是移动 data 和 tail 指针,如下图所示:
第三步,写入用户数据。skb_put 会移动 tail 指针并且增加 len。
unsigned char *data = skb_put(skb, user_data_len);
memcpy(data, user_data, user_data_len);
第四步,添加协议头。
struct tcphdr *tcp = (struct tcphdr*)skb_push(skb, sizeof(struct tcphdr));
tcp->source = htons(1025);
tcp->dest = htons(2095);
tcp->len = htons(user_data_len);
tcp->check = 0;
skb_push 的反操作是 skb_pull,这个操作在解包的时候经常使用,比如剥除掉刚才的 TCP 头:
skb_pull(skb, sizeof(struct tcphdr));
如上继续操作下去,一个完整的网络包就构建出来了。
线性 skb 和非线性 skb¶
上面的 skb 结构比较简单,所有的数据都在 head 和 end 之间,这个一般被称为线性 skb(linear),相对的,数据无法简单的在 head 和 end 存储下来的时候,就会用到非线性 skb,常见的非线性 skb 有以下 3 种:
第一种在发送大包(jumbo frame)的时候常用到,多余的数据存放在另外的物理页中,skb_shared_info 中有一个数组 frags,存放一组(页面、偏移、大小)结构体用来记录这些数据在哪。
第二种是 IP 数据分片(fragment)的时候用到的 frag_list。每片数据有各自自己的 skb 结构体,通过 skb->next 组成一个单链表,skb_shared_info 中的 frag_list 指针指向链表头。
最后一种是 gso 分段,当一个 TCP 数据包大小超过 mtu 后,会被切割成几个 <=mtu 大小的数据包,这几个包是通过简单的 skb->next 链接到一起,相关分段信息保存在第一个 skb->skb_shared_info 的 gso_segs 和 gso_size 里。
三个长度字段¶
len
是整个网络包数据的长度。data_len
是非线性区网络包数据的长度。
判断线性还是非线性 skb 的方式比较简单:
if (skb->data_len == 0) {
printk(" skb is linear, skb_len is %d", skb->len);
} else {
printk("skb is NOT linear, skb_len is %d", skb->len);
// len 为当前 skb 以及后续所有级联的 skb 中的数据长度
// data_len 中是后面级联的 skb 中的数据长度
printk("skb linear data len: %d", skb->len - skb->data_len);
}
协议栈每一层的入口函数开头都会有类似下面的一段(以 tcp 层为例), pskb_may_pull
函数可以确保线性区有足够长度的数据(不够从非线性区拿),这样方便处理。
int tcp_v4_rcv(struct sk_buff *skb)
{
...
if (!pskb_may_pull(skb, sizeof(struct tcphdr)))
goto discard_it;
...
}
除了这两个长度之外,skb 中还有一个 truesize
长度字段。
truesize
是网络包数据加上元数据结构体struct sk_buff
的大小,这个字段主要用于后面 socket buffer 统计计算(accounting)用。
skb 在放入某个 socket 的接收队列的时候,会调用 skb_set_owner_r
函数,这个函数会将 skb 消耗的内存记到这个 socket 的读 buffer 头上。发送 skb 的时候记到写 buffer 头上。
static inline void skb_set_owner_r(struct sk_buff *skb, struct sock *sk)
{
skb->sk = sk;
skb->destructor = sock_rfree;
atomic_add(skb->truesize, &sk->sk_rmem_alloc);
}
void sock_rfree(struct sk_buff *skb)
{
struct sock *sk = skb->sk;
atomic_sub(skb->truesize, &sk->sk_rmem_alloc);
}
从统计使用的字段来看,socket buffer 计算已使用的 buffer 大小的时候,不仅仅算了网络包的大小,还包括了存储这个数据包而使用的元数据结构的大小。
skb_clone、skb_copy 复制 skb 的差别¶
skb_clone 是浅拷贝,skb_clone 只是复制了 sk_buff 这个元结构体,其它关联的 buffer 都没复制,而是通过 skb_shared_info 中的 dataref 作为数据 buffer 的引用计数,保证 kfree_skb 的时候不会误删除还有引用的数据 buffer。
skb_clone 出来的数据 buffer 是不能修改的,如果要修改就得使用 skb_copy 系函数,skb_copy 是深拷贝,完全复制了 sk_buff 元结构体以及关联的数据 buffer。另外还有一个 pskb_copy(p stands for partial?),只复制 head 指向的 buffer,不复制 skb_shared_info 中 frags 和 frag_list 关联的 buffer。用哪个视具体情况决定。
skb_realloc_headroom 不会释放原来的 skb¶
skb headroom 如果不够用,可以通过 skb_realloc_headroom 扩大 headroom,skb_realloc_headroom 内部是通过 skb_clone 或者 skb_copy 完成的,但这个函数内部并没有处理原来的 skb,这个和 C 函数的 realloc 不太一样。所以这个函数成功返回的话,需要通过 kfree_skb 释放原 skb,否则会有 skb 泄露。
struct sk_buff *skb_realloc_headroom(struct sk_buff *skb, unsigned int headroom)
{
struct sk_buff *skb2;
int delta = headroom - skb_headroom(skb);
if (delta <= 0)
skb2 = pskb_copy(skb, GFP_ATOMIC);
else {
skb2 = skb_clone(skb, GFP_ATOMIC);
if (skb2 && pskb_expand_head(skb2, SKB_DATA_ALIGN(delta), 0,
GFP_ATOMIC)) {
kfree_skb(skb2);
skb2 = NULL;
}
}
return skb2;
}
https://elixir.bootlin.com/linux/v5.19/source/net/core/skbuff.c#L1767
skb 不释放也就不会调用其析构函数 skb->destructor,有些资源释放是在这个析构函数里做的,之前写一个内核隧道模块的时候遇到的问题就是 ping 隧道对端 IP 大概 10 次左右之后就报 ping: sendmsg: No buffer space available
错误,查了半天发现有 skb 泄漏,没调析构函数 sock_wfree 。
References: