内核版本:linux-2.6.11
伙伴系统是linux用于满足对不同大小块内存分配和释放请求的解决方案,它为slab分配器提供页框分配请求的实现。
如果我们需要请求具有连续物理地址和任意长度的内存单元序列时,即不定大小的 内存区 时,则需要在伙伴系统之上提供一层更细粒度的管理方案。
Linux在分配内存的时候,会将这部分内存初始化成一定的类型,即 内存对象 ,例如信号、进程描述符、文件描述符等等,在释放的时候,会进行析构。
然而进行初始化和析构占用的时间已然超出了分配这部分内存的时间,于是,需要一个机制能够省略内存初始化和内存析构的时间,这对提升内存从分配到使用再到释放这一系列处理过程的性能有着莫大帮助。
出于这两种需求,slab出现了。
slab分配器最早是由Jeff Bonwick引进Solaris 5.4内核中的,如今各主流Unix-like系统都在使用。
它充当了伙伴系统和内存区分配接口之间的中间件。
举一个完整的例子,假设之前没有任何可用高速缓存的情况下,slab分配器接收到第一个内存分配请求时,
首先会调用伙伴系统来分配足够大小的页框,然后按照一定规则初始化这部分内存,返回。内存释放时,slab分配器不会马上析构这部分内存,
而是标记为可用,下一次的内存分配请求如果类型相同,那么直接使用可用的未析构的那块内存区,
这样一来,高速缓存中会留有相同类型内存区的可用链表,只要链表不为空,那么该类型的内存区分配请求将会跳过初始化这一步直接得到满足。
所以说整个slab分配器的核心就是缓存(硬件高速缓存),是一种用空间换时间的聪明机制。
讲到这里,不得不补充一下高速缓存这个东西。
由于多级高速缓存之间的一致性是由硬件实现的,因此对于Linux而言,CPU片内只有一个逻辑的高速缓存,单位为行,一般由几十个连续的字节组成,同时,片内存在一个高速缓存控制器,里面保存了从高速缓存行到主存单元的映射表,由此判断访问内存时是否命中高速缓存。
Linux在系统初始化的时候调用函数 kmem_cache_init()
和函数 kmem_cache_sizes_init()
初始化了一系列高速缓存,并创建描述符以供slab分配器使用。
slab本身是存在于高速缓存以及对应的页框里的,由高速缓存控制器控制这里的高速缓存跟主存保持一致。
kmem_getpages()
调用伙伴系统的 alloc_pages()
函数接口,返回首个页描述符指针。 给高速缓存分配slab
cache_grow
给高速缓存分配一个新的slab,同时调用 cache_init_objs()
,该函数主要代码如下
static void cache_init_objs(struct kmem_cache *cachep, struct page *page) { for (i = 0; i < cachep->num; i++) { void *objp = index_to_obj(cachep, page, i); if (cachep->ctor) cachep->ctor(objp); set_obj_status(page, i, OBJECT_FREE); set_free_obj(page, i, i); } }
第一个参数为指定高速缓存的描述符指针,第二个参数为上一步调用伙伴系统分配的首个页框的描述符指针。
用途是将这个新的slab中的所有对象的构造函数设为该高速缓存中注册的构造函数,且将他们全部设为空闲。
由这里也可以看出,对象类型是跟高速缓存绑定的,即一个高速缓存描述符对应的这一块硬件高速缓存中的所有slab中的所有对象都是同一种类型的。
分配slab对象
假设上一步初始化了一个服务于A类型对象的slab,此时,我们就拥有了一组A类型的空闲对象虚位以待。
当接到内存区分配请求时,通过指定拥有不同构造函数的缓冲区来调用 kmem_cache_alloc(cachep, page)
就可以获得相应的空闲内存对象指针。
首先,系统初始化了两组高速缓存,第一组包括各个指定类型(pcb、singal等等)的高速缓存,第二组则是一系列2的幂字节大小的通用对象(范围从32-131072)的高速缓存,内核其他模块进行内存区分配请求时,指定好自己需要的内存类型所对应的高速缓存,例如,新建进程时就需要指定第一组中的pcb对应高速缓存,kmalloc函数则是指定第二组中的高速缓存,slab分配器会取得指定高速缓存中的一个空闲内存对象返回给请求模块。slab分配器在做分配和释放内存对象的工作时,会尽量减少构造新对象和析构旧对象以实现这个空间换时间的算法。
PS: 有什么问题,请各位一定指出,万分感谢。