#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 内存结构大致如下所示:

../_images/skb-alloc.svg

head、data、tail 这三个指针一开始都指向一块内存 buffer 的开始地址,end 指向结束地址,len 为 0。这块 buffer 的后面是一个 struct skb_shared_info 结构体(暂时不解释这个结构体是干什么的,后面用到时解释)。

第二步,预留一部分空间给各种协议头(因为构建包的过程一般是用户数据先写,然后才加各种协议头,防止频繁重新分配内存)。

skb_reserve(skb, header_len);

这个函数主要是移动 data 和 tail 指针,如下图所示:

../_images/skb-reserve.svg

第三步,写入用户数据。skb_put 会移动 tail 指针并且增加 len。

unsigned char *data = skb_put(skb, user_data_len);
memcpy(data, user_data, user_data_len);
../_images/skb-put.svg

第四步,添加协议头。

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;
../_images/skb-push.svg

skb_push 的反操作是 skb_pull,这个操作在解包的时候经常使用,比如剥除掉刚才的 TCP 头:

skb_pull(skb, sizeof(struct tcphdr));
../_images/skb-pull.svg

如上继续操作下去,一个完整的网络包就构建出来了。

线性 skb 和非线性 skb

上面的 skb 结构比较简单,所有的数据都在 head 和 end 之间,这个一般被称为线性 skb(linear),相对的,数据无法简单的在 head 和 end 存储下来的时候,就会用到非线性 skb,常见的非线性 skb 有以下 3 种:

第一种在发送大包(jumbo frame)的时候常用到,多余的数据存放在另外的物理页中,skb_shared_info 中有一个数组 frags,存放一组(页面、偏移、大小)结构体用来记录这些数据在哪。

../_images/skb-frags.svg

第二种是 IP 数据分片(fragment)的时候用到的 frag_list。每片数据有各自自己的 skb 结构体,通过 skb->next 组成一个单链表,skb_shared_info 中的 frag_list 指针指向链表头。

../_images/skb-fraglist.svg

最后一种是 gso 分段,当一个 TCP 数据包大小超过 mtu 后,会被切割成几个 <=mtu 大小的数据包,这几个包是通过简单的 skb->next 链接到一起,相关分段信息保存在第一个 skb->skb_shared_info 的 gso_segs 和 gso_size 里。

../_images/skb-gso-frags.svg

三个长度字段

  • 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 大小的时候,不仅仅算了网络包的大小,还包括了存储这个数据包而使用的元数据结构的大小。

http://vger.kernel.org/~davem/skb_sk.html

skb_clone、skb_copy 复制 skb 的差别

skb_clone 是浅拷贝,skb_clone 只是复制了 sk_buff 这个元结构体,其它关联的 buffer 都没复制,而是通过 skb_shared_info 中的 dataref 作为数据 buffer 的引用计数,保证 kfree_skb 的时候不会误删除还有引用的数据 buffer。

../_images/skb-clone.svg

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。用哪个视具体情况决定。

../_images/skb-copy.svg

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: