承接上一篇blog--sk_buff整理笔记(二、操作函数),这篇是要来讲解下sk_buff结构的内存申请和释放函数。因为sk_buff结构是比较复杂的(并不是其本身结构复杂,而是其所指的数据区以及分片结构等,合在一起就变复杂了),所以在内存申请和释放时,就要搞清楚什么函数对应的申请分配或释放什么结构内存。这里不提倡自己用kmalloc()和kfree()函数来为sk_buff相关结构体申请内存及销毁,而要用内核提供好的一些函数去为这些结构体申请内存。内核开发的原则:尽量用内核定义好的数据和函数以及操作宏,不到迫不得已不要自行定义任何东西。这样做是为了接口统一,移植方便,容易维护。
第一、sk_buff结构的内存申请:
static inline struct sk_buff *alloc_skb(unsigned int size, gfp_t priority) { return __alloc_skb(size, priority, 0, -1); } static inline struct sk_buff *alloc_skb_fclone(unsigned int size, gfp_t priority) { return __alloc_skb(size, priority, 1, -1); } struct sk_buff *dev_alloc_skb(unsigned int length) { /* * There is more code here than it seems: * __dev_alloc_skb is an inline */ return __dev_alloc_skb(length, GFP_ATOMIC); } static inline struct sk_buff *__dev_alloc_skb(unsigned int length, gfp_t gfp_mask) { struct sk_buff *skb = alloc_skb(length + NET_SKB_PAD, gfp_mask); if (likely(skb)) skb_reserve(skb, NET_SKB_PAD); return skb; }
这几个函数都是在linux-2.6.32.63\include\linux\sk_buff.h文件中的,其实就函数也可以看得出这是内联函数。不记得在前面讲过没,对于简洁常用的函数一般都定义为内联函数,这样做是为了提高CPU工作效率和内存利用率。一般的函数调用要保存现场(在堆栈中保存调用函数时现场状态,包括地址,执行状态等);当调用函数完后,又要恢复现场状态(把开始保存的状态数据从堆栈中读取出来)。在调用函数和返回时,浪费了CPU很多时间,再个在存储时也会出现些碎片。所以再使用简洁函数时,一般定义为内联函数,在函数前面加个inline关键字。
讲了内联函数细节后,来看下上面这几个内存申请函数。发现这几个函数其实本质上都是封装了 __alloc_skb();函数,那么首先就要来分析下__alloc_skb()函数,然后再去分析下这几个函数的异同点,以方便在申请sk_buff结构时,该用哪个函数去申请。
struct sk_buff *__alloc_skb(unsigned int size, gfp_t gfp_mask, int fclone, int node) { struct kmem_cache *cache; struct skb_shared_info *shinfo; struct sk_buff *skb; u8 *data; cache = fclone ? skbuff_fclone_cache : skbuff_head_cache; /* Get the HEAD */ skb = kmem_cache_alloc_node(cache, gfp_mask & ~__GFP_DMA, node); if (!skb) goto out; size = SKB_DATA_ALIGN(size); data = kmalloc_node_track_caller(size + sizeof(struct skb_shared_info), gfp_mask, node); if (!data) goto nodata; /* * Only clear those fields we need to clear, not those that we will * actually initialise below. Hence, don't put any more fields after * the tail pointer in struct sk_buff! */ memset(skb, 0, offsetof(struct sk_buff, tail)); skb->truesize = size + sizeof(struct sk_buff); atomic_set(&skb->users, 1); skb->head = data; skb->data = data; skb_reset_tail_pointer(skb); skb->end = skb->tail + size; kmemcheck_annotate_bitfield(skb, flags1); kmemcheck_annotate_bitfield(skb, flags2); #ifdef NET_SKBUFF_DATA_USES_OFFSET skb->mac_header = ~0U; #endif /* make sure we initialize shinfo sequentially */ shinfo = skb_shinfo(skb); atomic_set(&shinfo->dataref, 1); shinfo->nr_frags = 0; shinfo->gso_size = 0; shinfo->gso_segs = 0; shinfo->gso_type = 0; shinfo->ip6_frag_id = 0; shinfo->tx_flags.flags = 0; skb_frag_list_init(skb); memset(&shinfo->hwtstamps, 0, sizeof(shinfo->hwtstamps)); if (fclone) { struct sk_buff *child = skb + 1; atomic_t *fclone_ref = (atomic_t *) (child + 1); kmemcheck_annotate_bitfield(child, flags1); kmemcheck_annotate_bitfield(child, flags2); skb->fclone = SKB_FCLONE_ORIG; atomic_set(fclone_ref, 1); child->fclone = SKB_FCLONE_UNAVAILABLE; } out: return skb; nodata: kmem_cache_free(cache, skb); skb = NULL; goto out; }首先来看下函数参数:第一个参数 unsigned int size,数据区的大小;第二个参数 gfp_t gfp_mask,有些blog说这是优先级,个人觉得是不对的。我们都知道内核动态分配函数kmalloc()里面要有两个参数,第一个是要分配的空间大小,第二个是内存分配方式(这里我们一般用GFP_KERNEL)。所以这里的gfp_t gfp_mask应该是内核动态分配方式的一个掩码(就是各种申请方式的集合);第三个参数 int fclone,表示在哪块分配器上分配;第四个参数 int node,用来表示用哪种区域去分配空间。
第9行代码:cache = fclone ? skbuff_fclone_cache : skbuff_head_cache;由传入的参数来决定用哪个缓冲区中的内存来分配(一般是用skbuff_head_cache缓存池来分配)。说到这里就插入的讲下缓存池的概念。
内核对于sk_buff结构的内存分配不是和一般的结构动态内存申请一样:只分配指定大小的内存空间。而是在开始的时候,在初始化函数skb_init()中就分配了两段内存(skbuff_head_cache和skbuff_fclone_cache )来供sk_buff后期申请时用,所以后期要为sk_buff结构动态申请内存时,都会从这两段内存中来申请(其实这不叫申请了,因为这两段内存开始就申请好了的,只是根据你要的内存大小从某个你选定的内存段中还回个指针给你罢了)。如果在这个内存段中申请失败,则再用内核中用最低层,最基本的kmalloc()来申请内存了(这才是真正的申请)。释放时也一样,并不会真正的释放,只是把数据清零,然后放回内存段中,以供下次sk_buff结构的申请。这是内核动态申请的一种策略,专门为那些经常要申请和释放的结构设计的,这种策略不仅可以提高申请和释放时的效率,而且还可以减少内存碎片的。(注意:上面提到的内存段中的段不是指内存管理中的段、页的段,而是表示块,就是两块比较大的内存)
第13行代码:skb = kmem_cache_alloc_node(cache, gfp_mask & ~__GFP_DMA, node);从指定段中为skb分配内存,分配的方式是去除在DMA内存中分配,因为DMA内存比较小,且有特定的作用,一般不用来分配skb。
第17行代码是调整sk_buff结构指向的数据区的大小。
第19行代码:data = kmalloc_node_track_caller(size + sizeof(struct skb_shared_info), gfp_mask, node);这也是关键性代码之一(这里和上面分配skb的分配方式不一样,这里允许有些数据可以用DMA内存来分配)。这也是从特殊的缓存池中分配内存的,如果看函数里面的参数不难发现,要分配的空间大小为:size + sizeof(struct skb_shared_info),前面的size是指skb结构体指向的数据区大小,而sizeof(struct skb_shared_info)则是为分片数据分配空间。因为分片结构就是在skb结构指向的数据区的下面,就是end指针的下一个字节,所以就一起分配。
第29行到42行代码则是为sk_buff的数据区初始化,第44行到第53行则是为分片结构进行初始化。
第55行开始就是另外个知识点了,和第9行代码有点关系。skbuff_fclone_cache和skbuff_head_cache两个块内存缓存池是不一样的。我们一般是在skbuff_head_cache这块缓存池中来申请内存的,但是如果你开始就知道这个skb将很有可能被克隆(至于克隆和复制将在下一篇bolg讲),那么你最好还是选择在skbuff_fclone_cache缓存池中申请。因为在这块缓存池中申请的话,将会返回2个skb的内存空间,第二个skb则是用来作为克隆时使用。(其实从函数名字就应该知道是为克隆准备的,fclone嘛)虽然是分配了两个sk_buff结构内存,但是数据区却是只有一个的,所以是两个sk_buff结构中的指针都是指向这一个数据区的。也正因为如此,所以分配sk_buff结构时也顺便分配了个引用计数器,就是来表示有多少个sk_buff结构在指向数据区(引用同一个数据区),这是为了防止还有sk_buff结构在指向数据区时,而销毁掉这个数据区。有了这个引用计数,一般在销毁时,先查看这个引用计数是否为0,如果不是为0,就让引用计数将去1;如果是0,才真正销毁掉这个数据区。
第56行代码就好理解了:用child结构体变量来指向第二sk_buff结构体内存地址。第57行代码就是获取到引用计数器,因为引用计数器是在第二个sk_buff结构体内存下一个字节空间开始的,所以用(child + 1)来获取到引用计数器的开始地址。后面的代码就比较好理解了,无非就是些设置性参数了。
好了,基本函数__alloc_skb()已经分析过了,那么现在来看下开始的那几个函数的异同点吧。
alloc_skb():是用来分配单纯的sk_buff结构内存的,一般都是使用这个;alloc_skb_fclone():这是用来分配克隆sk_buff结构的,因为这个分配函数会分配一个子skb用来后期克隆使用,所以如果能预见要克隆skb_buff结构,则使用这种方法会方便些。dev_alloc_skb():其实这个函数实质上是调用了alloc_skb()函数单纯分配了一个sk_buff内存。但是通常来说这是在驱动程序中申请sk_buff结构时,用到的申请函数,和一般的申请内存函数有点不一样,它是用GFP_ATOMIC的内存分配方式来申请的(一般我们用GFP_KERNEL),这是个原子操作,表示申请时不能被中断。其实还有个申请函数:netdev_alloc_skb(),这个没怎么研究,估计是专门为网络设备中使用sk_buff时,用来内存分配的吧。
第二、sk_buff结构的内存释放:
void kfree_skb(struct sk_buff *skb) { if (unlikely(!skb)) return; if (likely(atomic_read(&skb->users) == 1)) smp_rmb(); else if (likely(!atomic_dec_and_test(&skb->users))) return; trace_kfree_skb(skb, __builtin_return_address(0)); __kfree_skb(skb); }
void __kfree_skb(struct sk_buff *skb) { skb_release_all(skb); kfree_skbmem(skb); }
static void skb_release_all(struct sk_buff *skb) { skb_release_head_state(skb); skb_release_data(skb); }首先还是来说下 dev_kfree_skb()函数吧,这个函数和dev_alloc_skb()相对应的,一般也是在驱动程序中使用,其实现也是对kfree_skb()进行封装的,所以重点还是kfree_skb()函数。
kfree_skb()函数首先是获取到skb->users成员字段,这是个引用计数器,当只有skb->users == 1是才能真正释放空间内存(也不是释放,而是放回到缓存池中)。如果不为1的话,那么kfree_skb()函数只是简单的skb->users减去个1而已。skb->users表示有多少个人正在引用这个结构体,如果不为1表示还有其他人在引用他,不能释放掉这
个结构体,否则会让引用者出现野指针、非法操作内存等错误。这种情况下只需要skb->users减去个1即可,表明我不再引用这个结构体了。如果skb->users == 1,则表明是最后一个引用该结构体的,所以可以调用_kfree_skb()函数直接释放掉了。当skb释放掉后,dst_release同样会被调用以减小相关dst_entry数据结构的引用计数。如果destructor(skb的析构函数)被初始化过,相应的函数会在此时被调用。还有分片结构体(skb_shared_info)也会相应的被释放掉,然后把所有内存空间全部返还到skbuff_head_cache缓存池中,这些操作都是由kfree_skbmem()函数来完成的。这里分片的释放涉及到了克隆问题:如果skb没有被克隆,数据区也没有其他skb引用,则直接释放即可;如果是克隆了skb结构,则当克隆数计数为1时,才能释放skb结构体;如果分片结构被克隆了,那么也要等到分片克隆计数为1时,才能释放掉分片数据结构。如果skb是从skbuff_fclone_cache缓存池中申请的内存时,则要仔细销毁过程了,因为从这个缓存池中申请的内存,会返还2个skb结构体和一个引用计数器。所以销毁时不仅要考虑克隆问题还要考虑2个skb的释放顺序。销毁过程见下图(原图来自《深入理解linux网络技术内幕》):