Linux Slub分配器(四)–分配对象
对象的分配过程体现了内存管理器对内存对象的组织方式,相较Slab分配器,Slub在组织对象的方式上给人的感觉就是简洁精悍。Slub没有用任何的管理区数组来组织这些对象,而是巧妙的将对象之间联系的桥梁嵌入在对象自身之中,因为请求分配对象的程序并不关心在对象分配之前,内存中的内容是什么,而这座桥梁就是空闲对象指针void*,它用以指明下一个空闲对象的地址。void*在对象内存中的偏移为offset,需要注意的一点是,struct kmem_cache和struct
kmem_cache_cpu这两个结构中都存在offset变量,两个offset都是表示空闲对象指针的偏移,只不过前者是以字节为单位而后者是以字长为单位!
另外,Slab和Slub都引入了本地CPU缓存的概念,但是Slab分配器中的本地CPU缓存中的对象都是以batchcount为大小,从slab中转移填充(或释放)的,这增加了操作上的复杂性,而Slub则是直接引入一个slab作为本地CPU缓存的管理单位,这样更为简洁。在Slub分配器中,如果一个slab位于本地CPU缓存上的话则称其处于冻结状态(frozen),如果处于slab 链表中中,则称其处于解冻状态(unfrozen)。
下面就来看具体的代码,函数void *kmem_cache_alloc(struct kmem_cache *s, gfp_t gfpflags)用于在指定的缓存中分配一个对象,它实质上是对函数slab_alloc()的一个封装,我们从slab_alloc()开始进行分析。
gfpflags &= gfp_allowed_mask;
lockdep_trace_alloc(gfpflags);
might_sleep_if(gfpflags & __GFP_WAIT);
if (should_failslab(s->objsize, gfpflags))
return NULL;
local_irq_save(flags);
/*获取本地CPU的slab信息结构*/
c = get_cpu_slab(s, smp_processor_id());
objsize = c->objsize;
/*如果本地CPU的freelist为空,也就是说没有空闲对象了,或者节点不匹配
则通过慢速途径进行分配*/
if (unlikely(!c-> freelist || !node_match(c, node)))
object = __slab_alloc(s, gfpflags, node, addr, c);
else {/*常规分配,在freelist不为空的情况下*/
object = c->freelist;//将freelist指向的对象赋给object
c->freelist = object[c->offset];//通过对象的空闲指针找到下一个空闲对象并保存在freelist中
stat(c, ALLOC_FASTPATH);
}
local_irq_restore(flags);
/*如果设置了清零标识并且分配成功,则将object的内存区域置零*/
if (unlikely((gfpflags & __GFP_ZERO) && object))
memset(object, 0, objsize);
kmemcheck_slab_alloc(s, gfpflags, object, c->objsize);
kmemleak_alloc_recursive(object, objsize, 1, s->flags, gfpflags);
return object;
}
我们捡主要的操作进行分析。
首先要从本地CPU高速缓存中分配,那么当然得获取这个结构保存在变量c中。然后出现了两种情况:
1.c->freelist指向NULL,表示本地CPU高速缓存中已经没有可用的空闲对象了,另外一个不想见到的情况就是节点不匹配,出现这两种情况中的任意一种都要通过__slab_alloc()走一条慢速分配的路径;
2.没有异常情况,可以直接从本地CPU高速缓存中进行分配,我们先拿这个来分析。
我们可以看到分配的操作非常的简洁,
object = c->freelist;
c->freelist = object[c->offset];
第一句话很容易理解,freelist指向了一个最热的对象,这一句话就是将这个对象的地址保存在object中。
第二句话的意义也很明显,就是将下一个空闲对象的地址保存在freelist中,以便下次分配使用。但这是如何实现的呢?注意object的定义是void **,这里似乎有点复杂,为什么要定义成一个二级指针?这是为了能够以字长为单位来访问到偏移为offset处的内存值,因为void *指针占用的存储空间是一个字长!可以将object理解成一个void*指针数组的起始地址(数组名),这时在数组内做偏移的运算时就是以void *为单位的,取出来的数值也是以void*为单位的,也就是说都是基于字长的。假如object定义成void*指针,那么虽然仍然可以找到下一个对象的地址,但是却不方便将这个字长的地址值给取出来(必须得将结果强制转换成void**再取内容)!总而言之,这种实现方法是最简洁雅观的。
接下来我们再看慢速分配路径究竟做了什么。
/* We handle __GFP_ZERO in the caller */
gfpflags &= ~__GFP_ZERO;
if (!c->page)/*本地CPU的slab结构中没有存储可分配的内存,则要为其寻找新的slab*/
goto new_slab;
slab_lock(c->page);
if (unlikely(!node_match(c, node)))//节点不匹配
goto another_slab;
stat(c, ALLOC_REFILL);
load_freelist:
object = c->page->freelist;//获取一个对象
if (unlikely(!object))
goto ano