sk_buff整理笔记(四、克隆与复制)

时间:2021-06-10 11:02:29

        在第一篇 sk_buff 整理笔记(一、数据结构)中已经对sk_buff的相关结构的常用成员字段进行了详细的分析,这里就不再赘述了。但前面的sk_buff结构成员字段对这篇sk_buff的克隆与拷贝非常重要,尤其是那几幅图,所以如果看此文时有不明白的地方,可以到第一篇中去查找下相关成员字段分析解释。根据前面已经说过sk_buff结构体是有三大块(其实是有四块结构体连接而成的,只是sk_buff数据区和分片结构体连在一起,可以当做一块来讲)结构体连成的。那复制拷贝的时候,这几块结构体复制的呢?首先来简要的说下几个复制拷贝函数对应的数据块拷贝情况。


skb_clone()函数:

        首先要分析的就是单单克隆下sk_buff结构体,对sk_buff结构的数据区、分片结构体skb_shared_info、分片结构体数据区等结果进行共享。此方法通过skb_clone()函数来实现。

skb_clone()函数实现:

struct sk_buff *skb_clone(struct sk_buff *skb, gfp_t gfp_mask)
{
	struct sk_buff *n;

	n = skb + 1;
	if (skb->fclone == SKB_FCLONE_ORIG &&
	    n->fclone == SKB_FCLONE_UNAVAILABLE) {
		atomic_t *fclone_ref = (atomic_t *) (n + 1);
		n->fclone = SKB_FCLONE_CLONE;
		atomic_inc(fclone_ref);
	} else {
		n = kmem_cache_alloc(skbuff_head_cache, gfp_mask);
		if (!n)
			return NULL;

		kmemcheck_annotate_bitfield(n, flags1);
		kmemcheck_annotate_bitfield(n, flags2);
		n->fclone = SKB_FCLONE_UNAVAILABLE;
	}

	return __skb_clone(n, skb);
}

函数关键代码分析:

        第01行代码:struct sk_buff *skb_clone(struct sk_buff *skb,gfp_t gfp_mask);这个函数的第一个参数是将要被克隆的skb结构体(后面用父skb来称),第二个参数是向内核申请分配内存的方式;返回的是新克隆出来的skb结构体(后面称其为子skb);

        第05行代码:n = skb + 1;这个要连续到sk_buff整理笔记(三、内存申请和释放),因为要被克隆的父skb一般在skbuff_fclone_cache缓存池中申请,若申请成功,则返回一对skb内存空间(即是父子skb),且空间为连续的。所以n = skb + 1;表示的是把事先申请好的skb内存空间给将要返回的克隆结构n;

        第06--07行代码:首先判断父skb是否是在skbuff_fclone_cache缓存池上分配的(确保返回了两个skb),然后判断子skb是否被克隆(如果被克隆了,则不能再用了)。

        第08行代码:如果上面(第06和第07行代码)两个条件皆满足,则开始获取skb的引用计数器变量了。在内存申请和释放那篇讲过,在skbuff_fclone_cache缓存池上申请的skb,不仅会返回两个skb空间,而且还会在子skb后增加一个字节,用来存放skb的引用计数器的。这里正是从子skb后面提取出skb的引用计数器。

        第09--10行代码:接着设置子skb的fclone,表明其是子skb,从父skb中克隆而来的;最后是增加skb的引用计数器的值,因为多了一个克隆的skb。这样做的目的是防止系统不知道还有其他克隆的skb也在共享sk_buff结构的数据区,而提前释放造成出错。

        第12行代码:这行代码是在上面(第06和07行代码)那两个条件不都满足的情况下,再到skbuff_head_cache缓存池上去申请(因为还不能确定这个skb是否要被克隆,所以到skbuff_head_cache缓存池上去申请),最后做些设置操作就不细分析了。

       

        在skb_clone()函数最后一行调用了_skb_clone(n,skb);其实skb_clone()函数实现的仅仅是克隆一个skb内存空间,而一些数据拷贝复制则是用_skb_clone()函数来完成。所以_skb_clone()函数主要是实现从父skb中把相关成员字段拷贝到子skb中去。

_skb_clone()函数实现:

static struct sk_buff *__skb_clone(struct sk_buff *n, struct sk_buff *skb)
{
#define C(x) n->x = skb->x

	n->next = n->prev = NULL;
	n->sk = NULL;
	__copy_skb_header(n, skb);


	C(len);
	C(data_len);
	C(mac_len);
	n->hdr_len = skb->nohdr ? skb_headroom(skb) : skb->hdr_len;
	n->cloned = 1;
	n->nohdr = 0;
	n->destructor = NULL;
	C(tail);
	C(end);
	C(head);
	C(data);
	C(truesize);
	atomic_set(&n->users, 1);

	atomic_inc(&(skb_shinfo(skb)->dataref));
	skb->cloned = 1;

	return n;
#undef C
}
函数关键代码分析:

        第01行代码:struct sk_buff *_skb_clone(struct st_buff *n,struct sk_buff *skb);第一个参数:n是表示未填充好成员字段的子skb;第二个参数:skb是父skb;返回的是从父skb成员字段复制填充好的子skb;

        第03行代码:#define C(x) n->x = skb->x 这是个关键性代码,用把父skb中某个成员字段复制到子skb中;

        第05行代码:让前驱后继指针都为NULL,因为这是个单独的sk_buff结构体,没有在sk_buff链表上。

        第06行代码:调用了_copy_skb_header(h,skb);这是用来复制成员变量的,可以自己看看源码,都是赋值操作,没什么难的。

        第22行代码:调用atomic_set(&n->users,1);来设置子skb的引用计数器为1,表明还有另外一个skb(其实就是父skb),防止子skb释放时连同共享数据区也一起释放掉。

        第24行代码:atomic_inc(&(skb_shinfo(skb)->dataref));这个简单的说就是,因为开始也讲过sk_buff的数据区和分片结构是一体的,连内存申请和释放都是一起的。而dataref是分片结构skb_shared_info中的一个 表示sk_buff的数据区和分片结构被多少skb共享的 成员字段。这里调用atomic_inc()函数让该引用计数器自增,表明克隆skb对sk_buff数据区和分片结构的共享引用。
        第25行代码:skb->cloned = 1;表明这是个克隆的skb结构体。

        第28行代码:#undef C  这行代码是和第03行代码对应的,第03行代码定义的是个宏,其作用域和变量不一样,宏是从定义的地方开始到代码块结束都是有效的。没有什么局部之分,所以到第28行函数结束时取消了第03行代码定义的宏的作用范围。下面看skb_clone()函数的实现原理图:

        sk_buff整理笔记(四、克隆与复制)

        其实上面的方法:由skb_clone()函数克隆一个skb,然后共享其他数据。虽然可以提高效率,但是存在一个很大的缺陷,就是当有克隆skb指向共享数据区是,那么共享数据区的数据就不能被修改了。所以说如果只是让多个skb查看共享数据区内容,则可以用skb_clone()函数来克隆这几个skb出来,提高效率。但如果涉及到某个skb要修改sk_buff结构的数据区,则必须要用下面这几个函数来克隆拷贝出skb。


pskb_copy()函数:

        开始时已经分析过,sk_buff结构可以分成三大块来处理。上面的skb_clone()函数已经处理了第一大块:sk_buff自身结构体,那么现在通过pskb_copy()函数来处理第二大块:不仅拷贝sk_buff结构体,还拷贝sk_buff结构体指针data所指向的数据区(当然这个数据区包括了分片结构体,因为内存分配时,这两个结构体都是一起分配的,现在如果要重新为数据区分配内存的话,那自然也是一起分配了),但是分片结构体中所指的数据区是共享的。

pskb_copy()函数实现:

struct sk_buff *pskb_copy(struct sk_buff *skb, gfp_t gfp_mask)
{
	struct sk_buff *n;
#ifdef NET_SKBUFF_DATA_USES_OFFSET
	n = alloc_skb(skb->end, gfp_mask);
#else
	n = alloc_skb(skb->end - skb->head, gfp_mask);
#endif
	if (!n)
		goto out;

	skb_reserve(n, skb->data - skb->head);
	skb_put(n, skb_headlen(skb));
	skb_copy_from_linear_data(skb, n->data, n->len);

	n->truesize += skb->data_len;
	n->data_len  = skb->data_len;
	n->len	     = skb->len;

	if (skb_shinfo(skb)->nr_frags) {
		int i;

		for (i = 0; i < skb_shinfo(skb)->nr_frags; i++) {
			skb_shinfo(n)->frags[i] = skb_shinfo(skb)->frags[i];
			get_page(skb_shinfo(n)->frags[i].page);
		}
		skb_shinfo(n)->nr_frags = i;
	}

	if (skb_has_frags(skb)) {
		skb_shinfo(n)->frag_list = skb_shinfo(skb)->frag_list;
		skb_clone_fraglist(n);
	}

	copy_skb_header(n, skb);
out:
	return n;
}
函数关键代码分析:

        第01行代码:pskb_copy(struct sk_buff,gfp_t gfp_mask);第一个参数,是将要被复制的skb;第二个参数,是内核内存申请时,内存分配的方式。返回的是复制好的子skb结构。

        第04--08行代码:有条件编译,其本质是调用alloc_skb()函数来为sk_buff结构及数据区(包括分片结构)分配内存空间,具体的分配步骤请看sk_buff整理笔记(三、内存申请和释放)

        第12行代码:skb_reserve(n, skb->data - skb->head);  是让分配到的子skb中的data指针和tail指针指向同一个位置,为了后面改变data指针和tail指针来存放协议信息。

        第13行代码:skb_put(n,skb_headlen(skb));使tail指针向高地址偏移(len - data_len)即是协议信息和应用层数据的长度和。以便存储各层协议和应用层数据。

        第14行代码:这是个内存拷贝的封装函数,就是从被拷贝的skb结构中的data指针指向的地方开始,偏移len(因为len = (data - tail) + data_len;所以这里本应该写成(data - tail)的,但考虑到此时分片结构数据区还没有数据,data_len为零。即是len = data-tail)个字节内容都拷贝到新复制到的skb结构体中去。即是:用被拷贝的skb中的数据区内容来为新拷贝的skb结构的数据区填充。

        第16--18行代码:是一些长度的设置。n->truesize是表示所有长度:truesize = (data - tail) + sizeof(sk_buff) + data_len;因为内存分配是已经把data-tail和sizeof(sk_buff)长度都赋值给了truesize,所以现在只需要再加上分片结构的数据区数据长度就可以了。其他的长度只是对应拷贝下值就可以。

        第20行代码:是判断分片结构数据区是否有数据,有的换就要作为共享数据进行处理。skb_shinfo(skb)宏其实就是skb->end;返回的是分片结构的开始位置。

        第21--27行代码:是让新的skb结构中的分片结构体指针指向 开始被拷贝的skb结构中分片结构体指针,就是让新的skb结构中的分片结构指针指向共享的分片结构数据区。skb_shinfo(n)->nr_frags = i;则是为新的skb结构体中的分片结构nr_frags成员字段(表示有多少个分片结构数据区)赋值;

       第30--33行代码:因为分片结构数据区有两种数据,这里暂时叫做链表数据和数组数据(其实是两种不同的数据类型)。上面第21--27行代码是处理数组类型的数据共享问题;而现在这几行代码则是处理链表类型的数据。首先是判断链表指针是否为NULL,然后就是为新skb结构的分片结构指针赋值等。

      第35行代码:是复制一些结构体成员变量。首先呢是调用_copy_skb_header(new,old)函数来为新skb自身结构成员拷贝赋值,然后是对其数据区的一些成员变量来赋值。最后还为一些常用的分片结构赋值。   


      pskb_copy()函数实现其实不难,主要是分配skb及数据区内存----》对数据区拷贝赋值----》处理分片结构数据区内存----》为其他成员变量拷贝赋值。下面是pskb_copy()函数的原理图:

sk_buff整理笔记(四、克隆与复制)

        其实上面的skb_clone()函数和pskb_copy()函数就像高富帅和屌丝男一样:skb_clone()函数就是那种富二代、官二代、星二代(当然了,虽然有些人不是什么二代,但人家有干爹),想要什么他老爸早在开始的时候就已经准备好了(skb_clone()函数是使用父skb内存申请时准备好的skb内存空间);而pskb_copy()函数就不一样了,什么东西都必须自己去挣、去争(pskb_copy()函数是自己去调用skb_alloc()函数来申请)。如果挣不到或者争不到,那没办法直接game over。但是唯一值得高兴的是,pskb_copy()函数申请内存时,其实是可以选择:alloc_skb_fclone()函数来申请的,可以让自己孩子成为某二代嘛(因为skb_clone()函数这个二代用的空间就是从alloc_skb_fclone()函数申请时返回的子skb),当然了,这个得自己去封装了。内核选择的还是低调做法,用alloc_skb()函数去申请,自给自足,不让自己孩子成为某二代。


skb_copy()函数:

        上面的pskb_copy()函数和skb_clone()函数类似:skb_clone()函数克隆出来的skb结构不能修改其共享数据区的数据,而pskb_copy()函数也是一样的,克隆出来的skb及数据区不能修改共享的分片结构数据区内容。所以如果想要修改分片结构数据区的内容,则必须要用skb_copy()函数来克隆skb结构体。skb_copy()函数是对skb结构体真正的完全复制拷贝。不仅是sk_buff结构体还有data指针指向的数据区(包括分片结构)以及分片结构中指针指向的数据区,都各自复制拷贝一份。

skb_copy()函数实现:

struct sk_buff *skb_copy(const struct sk_buff *skb, gfp_t gfp_mask)
{
	struct sk_buff *n;
#ifdef NET_SKBUFF_DATA_USES_OFFSET
	n = alloc_skb(skb->end + skb->data_len, gfp_mask);
#else
	n = alloc_skb(skb->end - skb->head + skb->data_len, gfp_mask);
#endif
	if (!n)
		return NULL;

	skb_reserve(n, headerlen);
	skb_put(n, skb->len);

	if (skb_copy_bits(skb, -headerlen, n->head, headerlen + skb->len))
		BUG();

	copy_skb_header(n, skb);
	return n;
}
函数关键代码分析:

        如果看了pskb_copy()函数的实现会发现:skb_copy()函数的实现和pskb_copy()函数的实现非常相似。可以看下pskb_copy()函数的关键代码分析,而第15行代码是调用了

skb_copy_bits(skb,-headerlen,n->head,headerlen + skb->len)函数来对函数区及分片结构的数据区进行拷贝赋值。看下面skb_copy()函数的原理图:


        sk_buff整理笔记(四、克隆与复制)       


总结:

        以上就是有关sk_buff结构的一些克隆拷贝函数了。要知道什么时候该用哪个克隆拷贝函数,就必须知道各个函数的不同点。
        当然,首先还是来说下sk_buff结构及相关结构体,第一块是sk_buff自身结构体,第二块是sk_buff结构的数据区及分片结构体(他们始终在一起),第三块则是分片结构中的数据区。
        然后来总结下各个函数的不同点:
        skb_clone()函数仅仅是克隆个sk_buff结构体,其他数据都是共享;
        pskb_copy()函数克隆复制了sk_buff和其数据区(包括分片结构体),其他数据共享;
        skb_copy()函数则是完全的复制拷贝函数了,把sk_buff结构体和其数据区(包括分片结构体)、分片结构的数据区都复制拷贝了一份。
        为什么要定义这么多个复制拷贝函数呢? 其真正的原因是:不能修改共享数据。所以如果想要修改共享数据,只能把这份共享数据拷贝一份,因此就有了这几个不同的复制拷贝函数了。 选择使用哪个复制拷贝函数时就根据你所要修改的哪块共享数据区来定。