【推荐阅读】
需要多久才能看完linux内核源码?
概述Linux内核驱动之GPIO子系统API接口
一文了解Linux内核的Oops
一篇长文叙述Linux内核虚拟地址空间的基本概括
纯干货,linux内存管理——内存管理架构(建议收藏)
1 skb_clone() 函数
该函数就是单单克隆下 sk_buff 结构体,对 sk_buff 结构的数据区、分片结构体 skb_shared_info、分片结构体数据区等结果进行共享。
- skb:将要被克隆的sk_buff结构体(后面用父skb来称);
- gfp_mask:向内核申请分配内存的方式;
- 返回:新克隆出来的skb结构体(后面称其为子skb);
struct sk_buff *skb_clone(struct sk_buff *skb, gfp_t gfp_mask)
{
struct sk_buff *n;
if (skb_orphan_frags(skb, gfp_mask))
return NULL;
n = skb + 1;//取克隆的子skb指针
/*1.判断skb是否从skbuff_fclone_cache中分配
2.子skb尚未被克隆过
*/
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 {//不满足条件从skbuff_head_cache中申请skb
if (skb_pfmemalloc(skb))
gfp_mask |= __GFP_MEMALLOC;
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);//调用克隆接口,赋值结构体成员变量
}
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(head_frag);
C(data);
C(truesize);
/*来设置子skb的引用计数器为1,表明还有另外一个skb(其实就是父skb),
*防止子skb释放时连同共享数据区也一起释放掉。
*/
atomic_set(&n->users, 1);
/*
*这个简单的说就是,因为开始也讲过sk_buff的数据区和分片结构是一体的,连内存申请和释放都是一起的。
*而dataref是分片结构skb_shared_info中的一个 表示sk_buff的数据区和分片结构被多少skb共享的成员字段。
*这里调用atomic_inc()函数让该引用计数器自增,表明克隆skb对sk_buff数据区和分片结构的共享引用。
*/
atomic_inc(&(skb_shinfo(skb)->dataref));
skb->cloned = 1;//表明这是个克隆的skb结构体
return n;
#undef C
}
skb_clone() 函数功能就是简单的克隆一个 skb,然后共享其他数据。虽然可以提高效率,但是存在一个很大的缺陷,就是当有克隆 skb 指向共享数据区是,那么共享数据区的数据就不能被修改了。所以说如果只是让多个 skb 查看共享数据区内容,则可以用 skb_clone() 函数来克隆这几个 skb 出来,提高效率。
【文章福利】小编推荐自己的Linux内核技术交流群: 【977878001】整理一些个人觉得比较好得学习书籍、视频资料共享在群文件里面,有需要的可以自行添加哦!!!前100进群领取,额外赠送一份 价值699的内核资料包(含视频教程、电子书、实战项目及代码)
内核资料直通车:Linux内核源码技术学习路线+视频教程代码资料
学习直通车:Linux内核源码/内存调优/文件系统/进程管理/设备驱动/网络协议
2 pskb_copy() 函数
pskb_copy()函数:不仅拷贝sk_buff结构体,还拷贝sk_buff结构体指针data所指向的数据区(当然这个数据区包括了分片结构体,因为内存分配时,这两个结构体都是一起分配的,现在如果要重新为数据区分配内存的话,那自然也是一起分配了),但是分片结构体中所指的数据区是共享的。
- skb:将要被复制的 skb
- gfp_mask:内核内存申请时,内存分配的方式
- 返回:复制好的子 skb 结构
static inline struct sk_buff *pskb_copy(struct sk_buff *skb,
gfp_t gfp_mask)
{
return __pskb_copy(skb, skb_headroom(skb), gfp_mask);
}
struct sk_buff *__pskb_copy(struct sk_buff *skb, int headroom, gfp_t gfp_mask)
{
unsigned int size = skb_headlen(skb) + headroom;
struct sk_buff *n = __alloc_skb(size, gfp_mask,
skb_alloc_rx_flag(skb), NUMA_NO_NODE);
if (!n)
goto out;
/* Set the data pointer */
/*是让分配到的子skb中的data指针和tail指针指向同一个位置,
*为了后面改变data指针和tail指针来存放协议信息。
*/
skb_reserve(n, headroom);
/* Set the tail pointer and length */
/*使tail指针向高地址偏移(len - data_len)即是协议信息和应用层数据的长度和。
*以便存储各层协议和应用层数据。*/
skb_put(n, skb_headlen(skb));
/*Copy the bytes
*这是个内存拷贝的封装函数,就是从被拷贝的skb结构中的data指针指向的地方开始,
*偏移len(因为len = (data - tail) + data_len;所以这里本应该写成(data - tail)的,
*但考虑到此时分片结构数据区还没有数据,data_len为零。即是len = data-tail)个字节
*内容都拷贝到新复制到的skb结构体中去。即是:用被拷贝的skb中的数据区内容来为新拷贝
*的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;
if (skb_orphan_frags(skb, gfp_mask)) {
kfree_skb(n);
n = NULL;
goto out;
}
/*新的skb结构中的分片结构体指针指向 开始被拷贝的skb结构中分片结构体指针,
*就是让新的skb结构中的分片结构指针指向共享的分片结构数据区。
*skb_shinfo(n)->nr_frags = i;则是为新的skb结构体中的分片结构
*nr_frags成员字段(表示有多少个分片结构数据区)赋值;
*/
for (i = 0; i < skb_shinfo(skb)->nr_frags; i++) {
skb_shinfo(n)->frags[i] = skb_shinfo(skb)->frags[i];
skb_frag_ref(skb, i);
}
skb_shinfo(n)->nr_frags = i;
}
if (skb_has_frag_list(skb)) {//链表类型的分片数据
skb_shinfo(n)->frag_list = skb_shinfo(skb)->frag_list;
skb_clone_fraglist(n);
}
copy_skb_header(n, skb);
out:
return n;
}
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()函数去申请,自给自足,不让自己孩子成为某二代。
3 skb_copy() 函数
上面的pskb_copy()函数和skb_clone()函数类似:skb_clone()函数克隆出来的skb结构不能修改其共享数据区的数据,而pskb_copy()函数也是一样的,克隆出来的skb及数据区不能修改共享的分片结构数据区内容。所以如果想要修改分片结构数据区的内容,则必须要用skb_copy()函数来克隆skb结构体。skb_copy()函数是对skb结构体真正的完全复制拷贝。不仅是sk_buff结构体还有data指针指向的数据区(包括分片结构)以及分片结构中指针指向的数据区,都各自复制拷贝一份。
struct sk_buff *skb_copy(const struct sk_buff *skb, gfp_t gfp_mask)
{
int headerlen = skb_headroom(skb);
unsigned int size = skb_end_offset(skb) + skb->data_len;
struct sk_buff *n = __alloc_skb(size, gfp_mask,
skb_alloc_rx_flag(skb), NUMA_NO_NODE);
if (!n)
return NULL;
/* Set the data pointer */
skb_reserve(n, headerlen);
/* Set the tail pointer and length */
skb_put(n, skb->len);
//拷贝数据区、数组分片区、链表分片区
if (skb_copy_bits(skb, -headerlen, n->head, headerlen + skb->len))
BUG();
copy_skb_header(n, skb);
return n;
}
实现机制如下:
4 小结
以上就是有关sk_buff结构的一些克隆拷贝函数了。要知道什么时候该用哪个克隆拷贝函数,就必须知道各个函数的不同点。当然,首先还是来说下sk_buff结构及相关结构体,第一块是sk_buff自身结构体,第二块是sk_buff结构的数据区及分片结构体(他们始终在一起),第三块则是分片结构中的数据区。然后来总结下各个函数的不同点:
- skb_clone() 函数仅仅是克隆个sk_buff结构体,其他数据都是共享;
- pskb_copy() 函数克隆复制了sk_buff和其数据区(包括分片结构体),其他数据共享;
- skb_copy() 函数则是完全的复制拷贝函数了,把sk_buff结构体和其数据区(包括分片结构体)、分片结构的数据区都复制拷贝了一份。
为什么要定义这么多个复制拷贝函数呢? 其真正的原因是:不能修改共享数据。所以如果想要修改共享数据,只能把这份共享数据拷贝一份,因此就有了这几个不同的复制拷贝函数了。 选择使用哪个复制拷贝函数时就根据你所要修改的哪块共享数据区来定。