在第一篇 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()函数的实现原理图:
其实上面的方法:由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()函数的原理图:
其实上面的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()函数的原理图: