内存管理-slub的分配和释放(三)

内核版本:3.10.0-693.21.1.el7.x86_64
1.slub cache内存的分配:kmem_cache_alloc(详见kmem_cache_alloc核心函数slab_alloc_node的实现详解
    对象的分配与释放不是直接在kmem_cache_node上面操作的,而是在kmem_cache_cpu上。一个kmem_cache维护了一组kmem_cache_cpu,分别对应系统中的每一个CPU。kmem_cache_cpu相当于为每一个CPU提供了一个分配缓存,以避免CPU总是去kmem_cache_node上面做操作,而产生竞争。并且kmem_cache_cpu能让被它缓存的对象固定在一个CPU上,从而提高CPU的cache命中率。kmem_cache_cpu只提供了一个page的缓存。
      进行对象分配的时候,系统会首先尝试在上去分配。如果分配不成功,再去kmem_cache_node上move一个page到kmem_cache_cpu上面来。分配不成功的原因有两个:kmem_cache_cpu上的page已经full了、或者现在需要分配的node跟kmem_cache_cpu上缓存page对应的node不相同。对于page已full的情况,page被从kmem_cache_cpu上移除掉(或者DEBUG模式下,被移动到对应kmem_cache_node的full链表上);而如果是node不匹配的情况,则kmem_cache_cpu上缓存page会先被move回到其对应kmem_cache_node的partial链表上(再进一步,如果page是free的,且partial链表的长度已经不小于min_partial了,则page被释放),代码如下:

preempt_disable();//关抢占
	c = __this_cpu_ptr(s->cpu_slab);//获取当前每cpu内存变量kmem_cache_cpu
	tid = c->tid;
	preempt_enable();//开抢占
	object = c->freelist;//从kmem_cache_cpu上获取内存对象
	page = c->page;
	if (unlikely(!object || !node_match(page, node)))//(1)kmem_cache_cpu上的page full了,也就是没有剩余空闲对象 (2)node不匹配
		object = __slab_alloc(s, gfpflags, node, addr, c);//再去kmem_cache_node上move一个page到kmem_cache_cpu上面来
	else {//从当前节点的cpu缓存直接分配对象
    ...

       来我们具体描述一下SLUB分配器是如何运作的,kmem_cache初始化后其是没有slab缓冲区的,当其他模块需要从此kmem_cache中申请一个对象时,kmem_cache会从伙伴系统获取连续的页框作为一个slab缓冲区,然后通过kmem_cache中的cotr函数指针指向的构造函数构造初始化这个slab缓冲区后,将其设置为该cpu的当前使用slab缓冲区,当此slab缓冲区使用完后,外部模块在申请对象时,会把这个满的slab缓冲区移除,再从伙伴系统获取一段连续页框作为一个新的空闲slab缓冲区,也是设置为该CPU当前使用的slab缓冲区。而那些满slab缓冲区中有对象释放时,SLUB分配器优先把这些缓冲区放入该CPU对应的部分空slab链表。而当一个部分空slab通过释放对象成为了一个空闲slab缓冲区时,SLUB分配器会视情况而定将此空闲slab释放还是加入到node结点的部分空slab链表中。
      先看看一个slub初始化结束的情况:

内存管理-slub的分配和释放(三)

       初始化完成后,slub中并没有一个slab缓冲区,只有在第一次申请时,才会从伙伴系统中获取一段连续页框作为一个slab缓冲区,如下:

内存管理-slub的分配和释放(三)

      这时候当前CPU获得了一个空闲slab缓冲区,并将其中的一个空闲对象分配出去,而下次申请对象时也会从该slab缓冲区中获取对象,直到此缓冲区中对象用完为止。
      上面描述的是初始化完成后第一次申请对象的情况,现在我们描述一下运行时申请对象的情况:
       1)一种情况是当前CPU使用的slab缓冲区有多余的空闲对象,这样直接从这些多余的空闲对象中分配一个出去即可,这种情况很简单。
      2)着重说明CPU使用的slab缓冲区没有多余的空闲对象的情况,这种情况又分为CPU的部分空slab链表是否为空的情况,
      a.如果CPU部分空slab链表不为空,则CPU会将当前使用的满slab移除,并从CPU的部分空slab链表中获取一个部分空的slab缓冲区,并设置为CPU当前使用的slab缓冲区,如下图:

内存管理-slub的分配和释放(三)

        b.  CPU的部分空slab链表为空,则CPU会将当前使用的满slab移除的情况,这种情况下,会从node结点的部分空slab链表获取若干个部分空slab缓冲区,将它们放入CPU的部分空slab链表中,获取的slab缓冲区个数根据一个规则就是:cpu空闲的对象数量必须要大于kmem_cache中的cpu_partial的值的一半。具体如下:

内存管理-slub的分配和释放(三)

        c. 如果node的部分空链表和CPU的部分空链表都为空的情况,那就与我们第一次申请对象的情况一样,直接从伙伴系统中获取连续页框用于一个slab缓冲区。

2.slub cache内存的释放:kmem_cache_free
         释放对象的时候,通过对象的地址能找到它所对应的page的地址,将对象放归该page即可。但是里面也有一些特殊逻辑,如果page正被kmem_cache_cpu缓存,就没有什么需要额外处理的了;否则,在将对象放归page时,需要对page加锁(因为其他CPU也可能正在该page上分配或释放对象)。另外,如果对象在回收之前该page是full的,则对象释放后该page就成partial的了,它还应该被添加到对应的kmem_cache_node的partial链表中。而如果对象回收之后该page成了free的,则它应该被释放掉。

void kmem_cache_free(struct kmem_cache *s, void *x)
{
	s = cache_from_obj(s, x);//获取释放对象x所对应的slub cache
	if (!s)
		return;
	slab_free(s, virt_to_head_page(x), x, NULL, 1, _RET_IP_);//首先调用virt_to_head_page(x)通过对象x获取其对应的page地址
	trace_kmem_cache_free(_RET_IP_, x);
}
slab_free片段代码:
	preempt_disable();
	c = __this_cpu_ptr(s->cpu_slab);//获取当前每cpu内存变量kmem_cache_cpu
	tid = c->tid;
	preempt_enable();
	if (likely(page == c->page)) {//page正被kmem_cache_cpu缓存
		set_freepointer(s, tail_obj, c->freelist);
		if (unlikely(!this_cpu_cmpxchg_double(
				s->cpu_slab->freelist, s->cpu_slab->tid,
				c->freelist, tid,
				head, next_tid(tid)))) {
			note_cmpxchg_failure("slab_free", s, tid);
			goto redo;
		}
		stat(s, FREE_FASTPATH);
	} else
		__slab_free(s, page, head, tail_obj, cnt, addr);//将对象归还给pgae

         对象的释放还有一个细节,既然对象会放回到对应的page上去,那如果这个page正在被其他的CPU cache呢(其他CPU的kmem_cache_cpu正指使用这个page)?其实没关系,kmem_cache_cpu和page各自有一个freelist指针,当page被一个CPU cache时,page的freelist上的所有对象全部移动到kmem_cache_cpu的freelist上面去(其实就是一个指针赋值),page的freelist变成NULL。而释放的时候是释放到page的freelist上去。两个freelist互不影响。但是这个地方貌似有个问题,如果一个被cache的page的freelist由于对象的释放而变成非NULL,那么这个page就可能再被cache到其他CPU的kmem_cache_cpu上面去,于是多个kmem_cache_cpu可能cache同一个page。这将导致一个CPU内部的缓存可能cache到其他CPU上的对象(因为CPU缓存跟对象并不是对齐的),从而一个CPU上的对象写操作可能引起另一个CPU的缓存失效。
         接下来我们说说释放对象的具体过程,释放对象也分很多种,我们先说说最简单的一种释放情况,
         a. 部分空的slab释放其中一个使用着的对象,释放后这个部分空slab还是部分空slab(有些部分空slab只使用了一个对象,释放这个对象后就变为空闲slab),这些部分空slab可能处于CPU当前使用slab,CPU部分空链表,node部分空链表中,但是它们的处理都是一样的,直接释放掉该对象即可,如下:

内存管理-slub的分配和释放(三)

       b. 另一种情况是满slab缓冲区释放对象后变为了部分空slab缓冲区,这种情况下系统会将此部分空slab缓冲区放入CPU的部分空链表中,如下:

内存管理-slub的分配和释放(三)

        c.最后一种释放情况就是部分空slab释放一个对象后转变成了空闲slab缓冲区,而对于这个空闲slab缓冲区的处理,系统首先会检查node部分空链表中slab缓冲区的个数,如果node部分空链表中slab缓冲区数量小于kmem_cache中的min_partial,则将这个空闲slab缓冲区放入node部分空链表中。否则释放此空闲slab,将其占用页框返回伙伴系统中。我们知道部分空slab有可能存在于3个地方,CPU当前使用的slab缓冲区,CPU部分空链表,node部分空链表,这三个地方对于这种情况下的处理都是一样的,如下:

内存管理-slub的分配和释放(三)

       这样看来只有空闲的slab缓冲区会被放入node结点的部分空链表中,这只是从释放对象的角度看是这样的,当刷新kmem_cache时,会将kmem_cache中所有的slab缓冲区放回到node结点的部分空链表(也包括当前CPU使用的slab缓冲区),这种情况node结点的部分空链表就会有部分空slab缓冲区了。而还有一种情况就是编译时禁用了CPU的部分空链表,即CPU只有一个当前使用的slab缓冲区,这样其他的部分空缓冲区都会保存在node结点的部分空链表上,更多详细细节请看内核源码中的mm/slub.c文件。
参考:https://www.cnblogs.com/tolimit/p/4654109.html

上一篇:史上最详细!嵌入式系统知识和接口技术总结


下一篇:flash_acr_latency