slab分配器,通过kmem_cache_t,kmem_list3,slab结构体来达到构建框架的目的,使得高速缓存(cache),slab,object,三者结成一个架构。其中,kmem_list3是包含于kmem_cache_t中的字段,而在前者中,存在三个关键的循环链表。分别是不包含空闲对象的slab描述符双向循环链表,只包含空闲对象的slab描述符双向循环链表,包含空闲和不空闲的slab描述符的双向循环链表。通过上面结构的架设,也可以看出slab分配器的两个特点。对象分组存放。对象类型同种的放到一起,这样有助于提高调用速度,比如file类的放到一组。每个slab都由连续的页框组成,其中包括空闲的和不空闲的。而实现以上框架的具体三个结构体中的字段分别为:
kmem_list3,slabs_partial,slabs_full,slabs_free,list.
static kmem_cache_t cache_cache = {
.lists = LIST3_INIT(cache_cache.lists),
.batchcount = 1,
.limit = BOOT_CPUCACHE_ENTRIES,
.objsize = sizeof(kmem_cache_t),
.flags = SLAB_NO_REAP,
.spinlock = SPIN_LOCK_UNLOCKED,
.name = "kmem_cache",
#if DEBUG
.reallen = sizeof(kmem_cache_t),
#endif
};
struct kmem_list3 {
struct list_head slabs_partial; /* partial list first, better asm code */
struct list_head slabs_full;
struct list_head slabs_free;
unsigned long free_objects;
int free_touched;
unsigned long next_reap;
struct array_cache *shared;
};
slab分配器也并不xx占有所有高速缓存,高速缓存分为普通和专用,slab使用的是普通高速缓存。而通过函数kmem_getpages,slab分配器在创建新的slab时,可以获得一组连续的空闲页框。函数中最关键的语句就是 page = alloc_pages(flags, cachep->gfporder);可以看出,使用的还是分区页框分配器,还是还原到__alloc_pages函数调用中。
在申请顺序上也是按照框架顺序来,先要有cache,然后是slab,然后是object.在函数cache_grow中,就是这一过程的演示。函数的参数中,关键类型kmem_cache_t * cachep足以说明这一点。而其中几个关键语句的顺序是:objp = kmem_getpages(cachep, flags, nodeid),slabp = alloc_slabmgmt(cachep, objp, offset, local_flags)。后一个就是申请slab描述符,而在函数alloc_slabmgmt中,引用了另一个申请对象的函数slabp = kmem_cache_alloc(cachep->slabp_cache, local_flags);此函数的作用就是分配slab对象。
正如高速缓存和slab都有对应的数据结构一般,object也同样有对应的结构:kmem_bufctl_t.这些对象组成一个数组,存放在相应类型的slab描述符之后。这个数组的作用就是定位!数组的{dy}个对象描述符描述的是slab中的{dy}个对象。每一个项对应的都是下一个空闲对象在slab中的下标,slab后面是一堆同类对象的组。可以说,对象描述符之所以可以用来定位,就是因为他们组成了在slab中一个空闲对象的链表。
而slab之所以可以访问的速度快,一个原因就是对象在内存中是对齐的。存放它们的内存单元的起始物理地址是一个给定常量的倍数,这个常量就是对齐因子。{zh0}的情况就是宽度是与内部总线宽度对齐。为了达到这个效果,slab分配器设计为“整存整取”。始终以某一个单元为固定大小,来读取数据。这样的情况下,有些时候会出现很多浪费空间。也就是以空间的牺牲换取时间的快速,以人为的方式来获得较好的高速缓存性能。但是会出现很多内存碎片。所以,这也是slab用于小规模内存分配的原因之一。
而slab性能提高的努力之一,就是“着色”。问题的产生是由于,相同大小的对象倾向于存放在高速缓存内相同的偏移处。也就是说,不同的slab中的相同偏移量的对象有很大可能同时映射在同一高速缓存行中。当读取的时候,有时就需要在此二者之间频繁读取转换,也就是说,在同一高速缓存行和ram之间反复搬运这两个对象,浪费效率。而“着色”就是用来解决这个问题的方法。由于上面谈到是对齐的,那么就可以通过计算某些量来获得slab的一些数据。对这些数据进行使用,从而来实现“着色”。
num:可在slab中存放的对象个数;
osize:对象的大小,包括对齐的字节。
dsize:slab描述符的大小加上所有对象描述符的大小,就等于硬件高速缓存行大小的最小倍数。
free:在slab内未用字节个数。
aln:对齐因子。
以上几个变量足以组成以下等式:slab的长度=(num*osize)+dsize+free,这其中的free就是用来着色的,可用的颜色个数就是free/aln。用来细分slab,并允许内存分配器把对象展开在不同的线性地址之中。{dy}个颜色为0,{zh1}一个为-1.而col对一个slab着色的时候,要从slab描述符和所有对象描述符的后面开始,也就是从对象开始。于是就要加上dsize这个变量,而因为对齐引子也就是aln,那么公式就是col*aln+dsize.而当slab描述符和object描述符都存在slab外部的时候,就不用有跨过它们的考虑,那么公式就是col*aln.而grow函数的一部分就是用来做这个工作的。
static int cache_grow (kmem_cache_t * cachep, int flags, int nodeid)
{
…………
size_t offset;
…………
offset = cachep->colour_next;
cachep->colour_next++;
if (cachep->colour_next >= cachep->colour)
cachep->colour_next = 0;
offset *= cachep->colour_off;
…………
slabp = alloc_slabmgmt(cachep, objp, offset, local_flags)
…………
}
以上就是函数中关于着色部分的操作。首先在高速缓存描述符中存储下一个slab的使用颜色(col),在cache_grow中获得这个字段的信息。然后自动加一,以便于下次申请slab时准备颜色(col).如果此时颜色大于slab所使用的颜色个数,也就是cachep->colour字段中的内容时,那么就从0再开始循环。然后offset *= cachep->colour_off;的作用就是上面的公式col*aln。此时的colour_off字段就是slab中的基本对齐偏移。而{zh1}一个语句alloc_slabmgmt函数的引用,正是申请一个新的slab描述符.
static struct slab* alloc_slabmgmt (kmem_cache_t *cachep,void *objp, int colour_off, int local_flags)
{
struct slab *slabp;
if (OFF_SLAB(cachep)) {
slabp = kmem_cache_alloc(cachep->slabp_cache, local_flags);
if (!slabp)
return NULL;
} else {
slabp = objp+colour_off;
colour_off += cachep->slab_size;
}
slabp->inuse = 0;
slabp->colouroff = colour_off;
slabp->s_mem = objp+colour_off;
return slabp;
}
函数写的非常简洁,这就是理想的写法,最少的语句表达最多的内容,简单的让人不用注释也能看懂,而所完成的任务却又是关键的。首先通过判断标示来决定是否需要申请的slab描述符,或者直接通过objp+colour_off来定位新的slabp地址。而slab_size字段的意思是单个slab的大小,基本对齐+单个大小,为下一个slab的对象偏移做好准备。对应的就是slabp->colouroff = colour_off;以下就是函数所使用的slab描述中几个主要字段:
colouroff:slab中{dy}个对象的偏移。
s_mem:slab中{dy}个对象的地址。
inuse:当前正在使用的slab中的对象个数。
而这个函数的作用就是新申请一个slab描述符。而在函数中,关键的一个引用函数kmem_cache_alloc的作用就是获得一个新的对象。这个函数封装的是return __cache_alloc(cachep, flags);而其中有一个关键结构:struct array_cache *ac;
slab分配器的每个高速缓存包含一个被称作slab本地高速缓存的每cpu数据结构,该结构由一个指向被释放对象的小指针数组组成:array_cache,系统中的每一个cpu对应于一个元素,每个array_cache数据结构是空闲对象的本地高速缓存的一个描述符。在函数中,参数cachep指向高速缓存描述符。函数中关键的是以下几句:
static inline void * __cache_alloc (kmem_cache_t *cachep, int flags)
{
unsigned long save_flags;
void* objp;
struct array_cache *ac;
…………
ac = ac_data(cachep);
if (likely(ac->avail)) {
STATS_INC_ALLOCHIT(cachep);
ac->touched = 1;
objp = ac_entry(ac)[--ac->avail];
} else {
STATS_INC_ALLOCMISS(cachep);
objp = cache_alloc_refill(cachep, flags);
}
…………
return objp;
}
ac->avail字段的作用是指向本地高速缓存中可使用对象的指针个数。判断语句{dy}个的判断的意思就是,当还有的时候,就使用,并且avail减一。否则将当没有空闲对象的时候,调用cache_alloc_refill,获得一个新的空闲对象。而cache_alloc_refill函数很关键,具体可以分成几个部分来分析。
{dy}个部分,就是判断slab是否包含本地高速缓存共享,且有一定的空闲对象。在函数中,l3是keme_list3类型的,而其中的字段shared就是指向所有cpu共享的一个本地高速缓存的指针。而shared_array就是一个本地高速缓存。它所获得的就是l3的同类型指针。然后再判断是否存在本地高速缓存中可使用对象的指针的个数,此字段也作为高速缓存中{dy}个空槽的下标。如果大于0,那么继续判断,batchcount的作用是表明本地告诉缓存重新填充或腾空时使用的块大小,在前面,batchcount中所存储的是cachep中的batchcount。如果所需要的块大小大于avail,那么就batchcount = shared_array->avail。而这个batchcount就是从共享本地高速缓存中上移的指针数量。memcpy(ac_entry(ac), &ac_entry(shared_array)[shared_array->avail],sizeof(void*)*batchcount);然后return ac_entry(ac)[--ac->avail];
static void* cache_alloc_refill(kmem_cache_t* cachep, int flags)
{
…………
ac = ac_data(cachep);
retry:
batchcount = ac->batchcount;
…………
l3 = list3_data(cachep);
…………
if (l3->shared) {
struct array_cache *shared_array = l3->shared;
if (shared_array->avail) {
if (batchcount > shared_array->avail)
batchcount = shared_array->avail;
shared_array->avail -= batchcount;
ac->avail = batchcount;
memcpy(ac_entry(ac), &ac_entry(shared_array)[shared_array->avail],sizeof(void*)*batchcount);
shared_array->touched = 1;
goto alloc_done;
}
}
…………
alloc_done:
…………
ac->touched = 1;
return ac_entry(ac)[--ac->avail];
}
这就是当有本地共享高速缓存时,且有一定的空闲空间时所进行的操作。而第二部分则是没有以上条件时,来填充slab中的包含的batchcount个空闲对象的指针。而在这个部分中,也分为两种情况。以下是其中的一个。kmem_list3结构中,包含三个循环双向链表分别是:空闲、填满、空闲+填满。partial字段就是{zh1}一个。而l3->slabs_partial.next如果等于&l3->slabs_partial,以及l3->slabs_free.next是否等于&l3->slabs_free,这两个的意思就是,slab被填充了,但是部分还有空闲。那么就进入must_grow,在空闲对象中减去相应的数量,然后再根据情况判断是否返回ac_entry(ac)[--ac->avail]。
while (batchcount > 0) {
struct list_head *entry;
struct slab *slabp;
entry = l3->slabs_partial.next;
if (entry == &l3->slabs_partial) {
l3->free_touched = 1;
entry = l3->slabs_free.next;
if (entry == &l3->slabs_free)
goto must_grow;
}
…………
}
…………
must_grow:
l3->free_objects -= ac->avail;
…………
return ac_entry(ac)[--ac->avail];
}
而在第二部分的第二种情况中则是将对象的地址插入本地高速缓存,并增加slab描述符的inuse字段,同时更新free字段使得它存放slab中下一个空闲对象下标。当然,这段也是在{dy}个while循环中的。循环判断条件中的,inuse是当前正在使用的非空闲的slab对象个数,而num是一个slab中封装的对象个数,batchcount则是需要的块大小,那么这循环体的作用就很明显了,填充,直到用完对象个数。重点在于这一句:ac_entry(ac)[ac->avail++] = slabp->s_mem + slabp->free*cachep->objsize;同样还有这句:slabp->free=slab_bufctl(slabp)[slabp->free];
…………
while (slabp->inuse < cachep->num && batchcount--) {
kmem_bufctl_t next;
…………
ac_entry(ac)[ac->avail++] = slabp->s_mem + slabp->free*cachep->objsize;
slabp->inuse++;
next = slab_bufctl(slabp)[slabp->free];
#if DEBUG
slab_bufctl(slabp)[slabp->free] = BUFCTL_FREE;
#endif
slabp->free = next;
}
check_slabp(cachep, slabp);
/* move slabp to correct slabp list: */
list_del(&slabp->list);
if (slabp->free == BUFCTL_END)
list_add(&slabp->list, &l3->slabs_full);
else
list_add(&slabp->list, &l3->slabs_partial);
…………