linux2.6内核分析——slab分配器

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);
        …………   

郑重声明:资讯 【linux2.6内核分析——slab分配器】由 发布,版权归原作者及其所在单位,其原创性以及文中陈述文字和内容未经(企业库qiyeku.com)证实,请读者仅作参考,并请自行核实相关内容。若本文有侵犯到您的版权, 请你提供相关证明及申请并与我们联系(qiyeku # qq.com)或【在线投诉】,我们审核后将会尽快处理。
—— 相关资讯 ——