slab分配器是sun首创的,sun的技术绝对一流,拥有最一流的unix系统solaris,拥有最成功的语言java,同时首创了很多东西,比如slab分配器,nfs之类,别的还有很多,就不多说了。
先来熟悉一下相关数据结构:
struct kmem_cache {
/* Used for retriving partial slabs etc */
unsigned long flags;
int size; /* The size of an object including meta data */
int objsize; /* The size of an object without meta data */
int offset; /* Free pointer offset. */
int order; /* Current preferred allocation order /
struct kmem_cache_node local_node;
/* Allocation and freeing of slabs */
int objects; /* Number of objects in slab */
gfp_t allocflags; /* gfp flags to use on each alloc */
int refcount; /* Refcount for slab cache destroy */
void (*ctor)(struct kmem_cache *, void *);
int inuse; /* Offset to metadata */
int align; /* Alignment */
const char *name; /* Name (only for display!) */
struct list_head list; /* List of slab caches */
#ifdef CONFIG_NUMA
/*
* Defragmentation by allocating from a remote node.
*/
int remote_node_defrag_ratio;
struct kmem_cache_node *node[MAX_NUMNODES];//每个节点一个
#endif
#ifdef CONFIG_SMP
struct kmem_cache_cpu *cpu_slab[NR_CPUS];//每个处理器一个,内含有本处理器的slab缓存信息。
#else
struct kmem_cache_cpu cpu_slab;
#endif
};
struct kmem_cache_cpu {
void **freelist; /* Pointer to first free per cpu object */
struct page *page; /* The slab from which we are allocating *///这个相当于基页,命名为base会更好。
int node; /* The node of the page (or -1 for debug) */
unsigned int offset; /* Freepointer offset (in word units) */
unsigned int objsize; /* Size of an object (from kmem_cache) */
};
以上是最基本的数据结构,往下我们看一下slab的分配和释放,先看分配:
static __always_inline void *slab_alloc(struct kmem_cache *s,
gfp_t gfpflags, int node, void *addr)
{
void **object;
struct kmem_cache_cpu *c;
unsigned long flags;
local_irq_save(flags);//中断中往往会触及大量内核数据结构,为了不使事情混乱,先禁用中断。
c = get_cpu_slab(s, smp_processor_id());//因为一个kmem_cache会为每一个处理器都保留一个cpu缓存结构,因此我们首先得到这个结构
if (unlikely(!c->freelist || !node_match(c, node)))
//如果本cpu节点没有空余的缓存供我们使用了或者我们需要在不同节点分配,那么进入slow模式,进行分配。
object = __slab_alloc(s, gfpflags, node, addr, c);
else {//执行到else说明我们1.这正是我们分配内存的NUMA节点;2.本cpu缓存还有空闲缓存可用,那么直接分配。
object = c->freelist;//取出空闲的slab中的对象。
c->freelist = object[c->offset];// 因为我们把空闲对象分配走了,故重置空闲对象指针。为什么这样设置,等看完slab_free后自会明白。释放时候将原来的空闲指针指向了要释放的下一个对象,所以既然我们已经把释放的对象重新分配了那么就要取得原来的空闲指针。如果没有对象被释放,那么就是一个接一个往下分配了。
stat(c, ALLOC_FASTPATH);//忽略,我们并不分析记录信息。
}
local_irq_restore(flags);//事情做完了,打开中断。
if (unlikely((gfpflags & __GFP_ZERO) && object))//如果需要一个零页,那么就清零。
memset(object, 0, c->objsize);
return object;
}
本着广度优先的原则,我们先看如何释放,这样问题不会像堆栈一样堆积,所以看看释放函数代码:
static __always_inline void slab_free(struct kmem_cache *s,
struct page *page, void *x, void *addr)
{
void **object = (void *)x;
struct kmem_cache_cpu *c;
unsigned long flags;
local_irq_save(flags);//同样,关中断
c = get_cpu_slab(s, smp_processor_id());//得到本处理器缓存信息结构
debug_check_no_locks_freed(object, c->objsize);//忽略
if (likely(page == c->page && c->node >= 0)) {//如果要释放的对象所在的页正是本处理器的基页(我们就是在基页分配的内存)那么就更新本处理器缓存信息结构的空闲信息
object[c->offset] = c->freelist;//把原来的空闲指针指向要释放的对象的下一个对象,这么做完全是为了让slab_alloc的时候用的。
c->freelist = object;//更新空闲指针,指向要释放的对象,从而使得在slab_alloc的时候首先就能把这个对象分配出去,这样做可以最小化slab的内碎片,可见设计者的用心良苦。
stat(c, FREE_FASTPATH);
} else
__slab_free(s, page, x, addr, c->offset);
local_irq_restore(flags);//开中断
}
释放也分析过了,如果还是不明白那个空闲指针的重置,就在纸上画两笔,真相就大白了,代码和文字在图形面前往往显得苍白无力。slub分配器的分配 就是这么清晰,比原来slab真的简单了很多,如果想了解其精髓,到此收住就可以了,但是有很多细节也同样是有趣的,错过了可惜啊。
现在我们来考虑分配和释放的慢速模式,对于分配,就是说现在的本cpu缓存已经全部被分配了或者要在别的numa节点进行分配;对于释放的慢速模式就是说要释放的对象所在的页不是当前处理器缓存的基页或者在调试。那么怎么可能释放的页不是本处理器基页呢?所有对象不都应该在基页吗?考虑一个个的对象被从 slab中分配走,终于用空了所有空闲对象,这迫使再有对象分配请求的时候会重新分配一个或多个页面用于slab缓存,这就是说基页变了,此时有个原来基 页的对象要释放了,那么它就会发现它所在的页已经不再是基页了,从而进入慢速释放模式。还是先看慢速分配代码:
static void *__slab_alloc(struct kmem_cache *s, gfp_t gfpflags, int node, void *addr, struct kmem_cache_cpu *c)
{
void **object;
struct page *new;
/* We handle __GFP_ZERO in the caller */
gfpflags &= ~__GFP_ZERO;
if (!c->page)
goto new_slab;
slab_lock(c->page);
if (unlikely(!node_match(c, node)))//如果在别的节点分配,那么就直接到对应的标号处理。
goto another_slab;
load_freelist:
object = c->page->freelist;//将新的基页分配对象,这个新基页有两个来源:1.新分配的内存页;2.从partial中得到的半满页。
if (unlikely(!object))
goto another_slab;
c->freelist = object[c->offset];
c->page->inuse = s->objects;
c->page->freelist = NULL;//如果在本基页分配满了之前及时释放了一些对象,在重新设置新的基页之前c->page->freelist字段会一直为空
c->node = page_to_nid(c->page);
unlock_out:
slab_unlock(c->page);
return object;
another_slab:
deactivate_slab(s, c);//在尝试别的slab之前,必须先将现有的加入半满链表,妥善安置。它主要将处理器的空闲对象赋给slab的空闲对象,主要就是从c->freelist到page->freelist的转化,一会还得接触此函数
new_slab:
new = get_partial(s, gfpflags, node);//从半满链表中得到页面。如果得到一个全满的页,那么在load_freelist后面会得不到空闲对象,结果是内核会将这个全满页加入半满链表末尾,然后尝试下一个半满表中的页面。
if (new) {
c->page = new;//设置新的基页为从半满链表中得到的页。
goto load_freelist;//重新加载,注意半满链表中还有空闲对象可以使用,故到load_freelist标号开始则把page的freelist转化为处理器的freelist,为何?因为半满链表是针对numa节点的,一个节点一个,既然从当中取出一个来了,那么它就要和处理器挂钩了。
}
if (gfpflags & __GFP_WAIT)//可能的睡眠,睡眠总是好事,当你累了,心情不好了,睡一觉就好了,内核也是这样,但是能睡的地方确实不多啊。
local_irq_enable();
new = new_slab(s, gfpflags, node);//看来半满链表里面都没有了,那么只好分配内存页面了。
if (gfpflags & __GFP_WAIT)
local_irq_disable();
if (new) {
c = get_cpu_slab(s, smp_processor_id());//到此,得到处理器缓存结构,它实际上的基页里面已经没有空闲对象了。
if (c->page)
flush_slab(s, c);// 因为处理器的基页只有一个,那么在设置新的基页之前要先将老基页安置好,怎么安置呢?就是放入半满链表,在这里这个基页不是半满的,是全满的,否则就可以分配了,就到不了这里了。但是蒋一个全满页加入了,那么别的执行绪到前面get_partial之后一下子就得到这个全满页了,然后还是没有办法分配怎么办?这就是flush_slab调用的deactivate_slab将老的基页面加入尾端,这样的意义在于,有足够的时间等待老基页里面的对象被释放,惯坏了那些生命周期很长的对象,本来它们占那么久内存就不对,现在还惯着它们,专门为它们设置这一套机制,看来linux内核的另一大特征就是:包容!尽量满足你的任何要求,不像windows那样,这也不行那也不可。
slab_lock(new);
SetSlabFrozen(new);//冻结基页。,表示它且仅它是这个处理器的相应slab基页。
c->page = new;//将崭新的页面给处理器当基页。
goto load_freelist;
}
//往下就不分析了,与本文主要内容无关。
if (!(gfpflags & __GFP_NORETRY) && (s->flags & __PAGE_ALLOC_FALLBACK)) {
if (gfpflags & __GFP_WAIT)
local_irq_enable();
object = kmalloc_large(s->objsize, gfpflags);
if (gfpflags & __GFP_WAIT)
local_irq_disable();
return object;
}
return NULL;
}
下面我来说一下上面提到的:deactivate_slab和flush_slab两个函数:
static inline void flush_slab(struct kmem_cache *s, struct kmem_cache_cpu *c)
{
stat(c, CPUSLAB_FLUSH);
slab_lock(c->page);//锁住基页,直接调用deactivate_slab就不用锁,那是因为前面已经上过锁了。这里主要是防止重入,我们已经没有空闲对象了,如果两个执行绪一前一后进入这个函数,结果就有可能混乱。
deactivate_slab(s, c);
}
static void deactivate_slab(struct kmem_cache *s, struct kmem_cache_cpu *c)
{
struct page *page = c->page;
int tail = 1;
if (page->freelist)
stat(c, DEACTIVATE_REMOTE_FREES);
/*
* Merge cpu freelist into slab freelist. Typically we get here
* because both freelists are empty. So this is unlikely
* to occur.
*/ //正如注释所说,我们很少到达循环内部。
while (unlikely(c->freelist)) {
void **object;
//如果真的到了循环内部,那么说明还有空闲对象,于是就将这个老基页放到半满链表的前面,因为我们确定它内部可以分配起码一个对象。
//我认为可以将半满链表也加权排队,像伙伴系统一样,很艺术吧,但是不知道有没有用,有时间实现一个测试一把。
tail = 0; /* Hot objects. Put the slab first */
/* Retrieve object from cpu_freelist */
object = c->freelist;
c->freelist = c->freelist[c->offset];
/* And put onto the regular freelist */
object[c->offset] = page->freelist;
page->freelist = object;
page->inuse--;
}
c->page = NULL;
unfreeze_slab(s, page, tail);
}
static void unfreeze_slab(struct kmem_cache *s,struct page *page, int tail)
{
struct kmem_cache_node *n = get_node(s, page_to_nid(page));
struct kmem_cache_cpu *c = get_cpu_slab(s, smp_processor_id());
ClearSlabFrozen(page);//解除冻结。
if (page->inuse) {
if (page->freelist) //如果有空闲对象,那么加入半满链表。要是没有空闲对象呢?不加入了,一会看看__slab_free就知道了。
add_partial(n, page, tail);
slab_unlock(page);//解锁。我最烦linux的一点就是乱加锁/解锁,有时候在一个文件加的锁,在别的什么地方解锁,比侦探小说还 “引人入胜”
} else {
if (n->nr_partial < MIN_PARTIAL) {
/*这一段注释很好,保留(比我写的好得多!!毕竟人家是作者!)
* Adding an empty slab to the partial slabs in order
* to avoid page allocator overhead. This slab needs
* to come after the other slabs with objects in
* so that the others get filled first. That way the
* size of the partial list stays small.
*
* kmem_cache_shrink can reclaim any empty slabs from the
* partial list.
*/
//为了不让半满链表无条件膨胀,内核会在适当时候(kmem_cache_shrin)调整半满链表的内部顺序,使他们当中inuse最大
的排在前面,依次减小,这样就会使得释放全空slab的时候从后向前扫描更加有效率。最后的位置的slab的inuse最少,而从半满链表分配是从前面开始的,这就给了后面的很大机会被释放,从而限制了半满链表无限膨胀
add_partial(n, page, 1);
slab_unlock(page);
} else {
slab_unlock(page);
discard_slab(s, page);
}
}
}
static void __slab_free(struct kmem_cache *s, struct page *page, void *x, void *addr, unsigned int offset)
{
void *prior;
void **object = (void *)x;
struct kmem_cache_cpu *c;
c = get_cpu_slab(s, raw_smp_processor_id());//得到此处理器的slab缓存结构
slab_lock(page);
checks_ok:
prior = object[offset] = page->freelist;//看前面alloc的代码,如果一个slab直到用完也没有一个释放,那么,它将保持page->freelist
为空的状态。
page->freelist = object;//执行万这里,前面的prior就没有机会为空了。
page->inuse--;
if (unlikely(SlabFrozen(page))) {//前面看到过,只在unfreeze_slab的时候解除冻结状态。也就是在那段代码执行后才会执行下一个if语 句以及往后的语句。
goto out_unlock;
}
if (unlikely(!page->inuse))//在执行了unfreeze_slab后,那个老的基页的inuse字段不一定为0,经过这里page->inuse--后就可能为0了,如果为0,当即释放。
goto slab_empty;
if (unlikely(!prior)) {//在执行了unfreeze_slab时候,page->freelist可能为空,但是inuse不为0,在当时并没有加入那个页到半满链表 ,那时不加入,这时加入。
add_partial(get_node(s, page_to_nid(page)), page, 1);
stat(c, FREE_ADD_PARTIAL);
}
out_unlock:
slab_unlock(page);
return;
slab_empty:
if (prior) {
/*
* Slab still on the partial list.
*/
remove_partial(s, page);
stat(c, FREE_REMOVE_PARTIAL);
}
slab_unlock(page);
stat(c, FREE_SLAB);
discard_slab(s, page);
return;
//注意,如果执行到了这个函数,那么说明这个处理器的基页已经变了,要么已经被加入半满链表了,要么就是处于游离态。
}