文章目录
linux中的伙伴系统内存分配器是以页为最小粒度来进行内存分配。在实际的应用中,内核存储的大多数obj(如strcut task_strcuct,struct inode等数据结构)往往只需要几字节到几百字节不等。如果这些内核object的内存申请和释放都通过伙伴系统进行管理,会有以下缺点:
- 内存资源浪费,页内部碎片化严重
- 内存分配效率低
比如内核用伙伴系统为一个128字节的obj分配内存,内核不得不分配一个4k的page来存储该object,该页剩余的3972字节将会被闲置,内存资源被浪费。而且伙伴系统在释放一个内存块后,会立即将该内存块放在对应的freelist上,且该内存块还有可能与freelist上的相邻内存块合并成高一阶的内存块,若此时内核马上又要使用该内存块,则还需要再次从伙伴系统的freelist上去申请对应内存块,这种操作效率是很低的(未合理使用cpu高速缓存)。
为解决上面小块内存分配时遇到的问题,linux内核提出了slab分配机制,该内存分配器不是按页进行内存分配,而是按字节来分配的。
1 slab分配器原理
slab分配器中用到了对象这个概念,所谓对象就是内核中的数据结构以及对该数据结构进行创建和撤销的操作。它的基本思想是将内核中经常使用的对象放到slab缓存中,并且由系统保持为初始的可利用状态。比如进程描述符,内核中会频繁对此数据进行申请和释放。当一个新进程创建时,内核会直接从slab分配器的slab缓存中获取一个已经初始化了的对象;当进程结束时,该结构所占的页框并不被释放,而是重新返回slab分配器中。如果没有基于对象的slab分配器,内核将花费更多的时间去分配、初始化以及释放一个对象。
slab分配器把对象分组放进slab缓存。每个slab缓存都是同种类型对象的一种“储备”。一个slab cache管理一组大小固定的内存块(也称为对象实体),每个内存块都可用作一种数据结构。slab cache中的内存块来自一到多个slab。一个slab来自物理内存管理器的一到多个物理页,该slab被分成一组固定大小的块,被称为slab对象(object),一个slab属于一个slab cache,其中的对象就是该slab cache所管理的固定大小的内存块。所以一个slab cache可以有一到多个slab。
在基于slab的内核内存管理器中,基本的概念是保存管理型数据的缓存(即slab cache)和保存被管理对象的各个slab。每个缓存都负责一种对象类型,比如kmalloc-128缓存负责管理65-128字节内存空间的的分配。系统中的所有缓存类型都保存在一个链表slab_caches中。下图给出了slab分配器的各个组成部分间的相互关系:
slab分配器有以下三个基本目标:
- 减少伙伴算法在分配小块连续内存时所产生的内部碎片;
- 将频繁使用的对象缓存起来,减少分配、初始化和释放对象的时间开销。
- 通过着色技术调整对象以更好的使用硬件高速缓存;
2 slab分配器重要数据结构以及组织关系
2.1 slab cache描述符struct kmem_cache
在slab分配器中slab cache的描述符为struct kmem_cache,它是该机制的核心数据结构。
在slab分配器中,最顶层结构为slab cache,其描述符为struct kmem_cache.linux中每个slab obj对应的slab cache都有自己的名字(如:ext4_inode_cache,kmalloc-8和kmalloc-16等).
struct kmem_cache用于描述一种slab cache,并管理着一个或多个该slab中的的所有obj。linux中所有的kmem_cache会保持在一个以slab_caches作为头节点的链表中。kmem_cache代码定义如下所示。
// mm_slab_def.h
/*
* Definitions unique to the original Linux SLAB allocator.
*/
struct kmem_cache {
//本地高速缓存,每CPU结构,对象释放时,优先放入这个本地CPU高速缓存中
struct array_cache __percpu *cpu_cache;
/* 1) Cache tunables. Protected by slab_mutex */
/*batchcount指定了在per-CPU变量中entry列表为空的情况下,从缓存的slab中获取对象的数目。它还表示在
*缓存增长时,若entry数组中obj数量超过limit限制后,需要从entry数组中迁移走的obj数量(到本地节点cpu共享空闲链
*表中)。
*/
unsigned int batchcount;
//本地高速缓存中entry数组中空闲obj的最大数目
unsigned int limit;
//CPU共享高速缓存标志,实际地址保存在kmem_cache_node结构中
unsigned int shared;
//对象长度 + 填充字节
unsigned int size;
struct reciprocal_value reciprocal_buffer_size;
/* 2) touched by every alloc & free from the backend */
//属性的标识,如果SLAB管理结构放在外部,则CFLAGS_OFF_SLAB置1
unsigned int flags; /* constant flags */
//每个slab中obj数量
unsigned int num; /* # of objs per slab */
/* 3) cache_grow/shrink */
/* order of pgs per slab (2^n) */
//每个slab页块的阶(一个slab由2^gfporder个页构成)
unsigned int gfporder;
/* force GFP flags, e.g. GFP_DMA */
//从伙伴系统分配页,补足slab时,页分配的gfp码
gfp_t allocflags;
//一个slab中有多少不同的cache line
size_t colour; /* cache colouring range */
//一个cache colour的长度,和一个cache line的大小相同
unsigned int colour_off; /* colour offset */
/*
*空闲对象链表放在slab外部时使用,管理用于slab对象管理结构中freelist成员的缓存,也就是又一个新缓存
*/
struct kmem_cache *freelist_cache;
//空闲对象链表的大小
unsigned int freelist_size;
/* constructor func */
// 创建高速缓存时的构造函数指针,一半为null
void (*ctor)(void *obj);
/* 4) cache creation/removal */
//slab缓存名字
const char *name;
//slab缓存描述符双向链表指针
struct list_head list;
int refcount;
//slab中每个obj的大小
int object_size;
//obj对齐字节
int align;
...
...
...
//slab节点链表组,对于NUMA系统中每个节点都会有一个struct kmem_cache_node数据结构
struct kmem_cache_node *node[MAX_NUMNODES];
};
通过struct kmem_cache的代码实现可以看出,slab cache中所有slab的大小一致,由一个或多个连续页组成(通常为一个page,伙伴系统提供),每个slab中的obj大小和数量也是相同的。
slab cache描述符struct kmem_cache中除了相关的管理数据外,有两个很重要的成员:
-
struct array_cache __percpu *cpu_cache:
- cpu_cache是一个Per-CPU数据结构,每个CPU独享(相当于函数和它局部变量的关系),用来表示本地CPU的slab cache对象缓冲池(注意是slab cache obj缓冲池不是slab cache slab缓冲池)。
- CPU都有自己的硬件高速缓存,当前CPU上释放对象时,这个对象很可能还在CPU的硬件高速缓存中,这时使用这个对象的代价是非常小的,不需要重新装载到硬件高速缓存中,离CPU又最近。同时还可以减少锁的竞争,尤其是在多个CPU同时申请同样size或者同个缓存对象时,无需加锁即可操作。
- array_cache中的entry空数组,就是用于保存本地cpu刚释放的obj,所以该数组初始化时为空,只有本地cpu释放slab cache的obj后才会将此obj装入到entry数组。array_cache的entry成员数组中保存的obj数量是由成员limit控制的,超过该限制后会将entry数组中的batchcount个obj迁移到对应节点cpu共享的空闲对象链表中。
- entry数组的访问机制是LIFO(last in fist out),此种设计非常巧妙,能保证本地cpu最近释放该slab cache的obj立马被下一个slab内存申请者获取到(有很大概率此obj仍然在本地cpu的硬件高速缓存中).
// mm_slab_def.h /* * struct array_cache * * Purpose: * - LIFO ordering, to hand out cache-warm objects from _alloc * - reduce the number of linked list operations * - reduce spinlock operations * * The limit is stored in the per-cpu structure to reduce the data cache * footprint. * */ struct array_cache { //当前CPU上该slab可用对象数量 unsigned int avail; //最大对象数目,和kmem_cache中一样 unsigned int limit; /*同kmem_cache,batchcount指定了在per-CPU列表为空的情况下,从缓存的slab中获取对象的数目。它还表示在 *缓存增长时,若entry数组中obj数量超过limit限制后,需要迁移entry中obj的数量(到本地节点cpu共享空闲链 *表中)。 */ unsigned int batchcount; /*在从缓存移除一个对象时,将touched设置为1,而缓存收缩时, 则将touched设置为0。这使得内核能够确认在缓 * 存上一次收缩之后是否被访问过,也是缓存重要性的一个标志。 */ unsigned int touched; /*最后一个成员entry是一个伪数组, 其中并没有数组项, 只是为了便于访问内存中array_cache实例之后缓存中的 *各个对象而已 */ void *entry[]; /* * Must have this definition in here for the proper * alignment of array_cache. Also simplifies accessing * the entries. */ };
-
struct kmem_cache_node *node[MAX_NUMNODES]:
-
slab缓存会为不同的节点维护一个自己的slab链表,用来缓存和管理自己节点的slab obj,这通过kmem_cache中node数组成员来实现,node数组中的每个数组项都为其对应的节点分配了一个struct kmem_cache_node结构来。
-
struct kmem_cache_node结构定义的变量是一个每node变量。相比于struct array_cache定义的每cpu变量,kmem_cache_node管理的内存区域粒度更大,因为kmem_cache_node维护的对象是slab,而array_cache维护的对象是slab中的obj(一个kmem_cache可能包含一个或多个slab,而一个slab中可能包含一个或多个slab).
-
通过下面struct kmem_cache_node结构的代码实现我们来分析该结构体如何实现对本地节点指定slab cache中所有的slab进行管理的。
//mm/slab.h struct kmem_cache_node { //该链表中存储的所有slab中只有部分obj是空闲的 struct list_head slabs_partial; /* partial list first, better asm code */ //该链表中存储的所有slab中不存在空闲的obj struct list_head slabs_full; //该链表中存储的所有slab中每个obj都是空闲的 struct list_head slabs_free; //该节点中此kmem_cache的slab总数 unsigned long num_slabs; //该节点中此kmem_cache空闲obj总数 unsigned long free_objects; //该节点中此kmem_cache中空闲obj数量的上限,多了就会回收到伙伴系统的空闲链表中 unsigned int free_limit; unsigned int colour_next; /* Per-node cache coloring */ //该节点上所有cpu共享的本地高速缓存 struct array_cache *shared; /* shared per node */ //其他节点上所有cpu共享的本地高速缓存 struct alien_cache **alien; /* on other nodes */ unsigned long next_reap; /* updated without locking */ int free_touched; /* updated without locking */ };
由上代码可以看出,struct kmem_cache_node对于本节点中slab的管理主要分了3个链表:slabs_partial(部分空闲slab链表),非空闲slab链表(slabs_full)和全空闲slab链表(slabs_free).struct kmem_cache_node还会将本地节点中需要节点共享的slab obj缓存在它的shared成员中。若本地节点向访问其他节点贡献的slab obj,可以利用struct kmem_cache_node中的alien成员去获取。
-
2.2 slab描述符struct page
为了提高linux内存利用率,slab描述符结合在页描述符struct page中,当页描述符成员flage标志设置了PG_SLAB后,struct page中的部分成员可以用来描述slab对象.如下代码所示(由于struct page成员很多,下面只将描述slab对象有关的成员列出):
//为了节省内存,struct page中许多结构体会通过union联合体进行封装,此处做了省略
struct page {
/* First double word block */
//当页描述符成员flage标志设置了PG_SLAB后,struct page中对应的slab成员才有效
unsigned long flags; /* Atomic flags, some possibly updated asynchronously */
void *s_mem; /* slab first object */
//freelist实际上是一个数组,数组中存放slab中未使用的obj的index值
void *freelist; /* sl[aou]b first free object */
/* page_deferred_list().prev -- second tail page */
//当前slab已经使用的obj数量
unsigned int active; /* SLAB */
/* 通过该成员将链到slab cache对应节点kmem_cache_node所管理的3个链表上*/
struct list_head lru; /* Pageout list, eg. active_list
* protected by zone_lru_lock !
* Can be used as a generic list
* by the page owner.
*/
//指向slab的slab cache描述符kmem_cache
struct kmem_cache *slab_cache; /* SL[AU]B: Pointer to slab */
//slab首地址,待验证
void *virtual;
}
由上面代码可以看出在slab描述符中最重要两个数据结构是s_mem和freelist这两个成员。
- s_mem用于指向指定slab区域中第一个obj(slab由一个或多个连续的页构成,由伙伴系统分配的内存块,固一个slab包含页的个数为2^gfp_order)
- freelist指向的是slab空闲obj的索引数组,该数组要和slab描述符中的active成员搭配使用。freelist指向的区域是一个连续地址的数组,它保存的地方有两种情况:一种情况是放在slab自身所在的内存区域,另外一种情况是保存在slab所在内存区域的外部(若保存在slab外部,则保存在slab对应slab cache的kmem_cache结构体中freelist_cache成员指针指向的链表).
下面介绍slab的空闲索引数组保存在slab自身内部的情况slab描述符是如何管理slab和其内部obj,如下图所示:
通过示意图来描述slab如何根据freelist数组和active成员来分配和释放其内部的空闲obj的(假设该slab中有6个obj)。
在slab机制中slab管理的内存区域的连续页块数,和slab cache结构描述符struct kmem_cache中的gfporder有关。该gfp是在slab cache初始化时通过每个slab中obj的数量和大小,着色区大小,freelist大小,slab对象描述符大小等一系列数据计算而来。需要注意的是这些对象的大小并不是创建的时候对象本身的大小,系统为了优化内存访问还会做一些字节对齐的填充,或者是在对象交界处填充一些magicnum进行防越界处理(如red_zone).
3.slab分配器中各个重要结构体间的关系总结
通过上图我们对slab分配器中各个重要结构体间的关系做一个总结:
-
slab机制中每个slab cache对应一个struct kmem_cache结构体,linux内核中会分配一个静态全局的struct kmem_cache变量slab_caches,该指向一个双向链表,内核中所有的slab cache的结构描述符都会挂到该链表中(通过list成员变量接入slab_caches链表)。我们可以在shell中断通过/proc/slabinfo查看内核中所有slab cache的相关数据信息。
/ # cat /proc/slabinfo slabinfo - version: 2.1 # name <active_objs> <num_objs> <objsize> <objperslab> <pagesperslab> : tunables <limit> <batchcount> <sharedfactor> : slabdata <active_slabs> <num_slabs> > zswap_entry 0 0 360 22 2 : tunables 0 0 0 : slabdata 0 0 0 sw_flow_stats 0 0 448 18 2 : tunables 0 0 0 : slabdata 0 0 0 ...... ...... ...... dma-kmalloc-64 0 0 368 22 2 : tunables 0 0 0 : slabdata 0 0 0 dma-kmalloc-32 0 0 336 24 2 : tunables 0 0 0 : slabdata 0 0 0 dma-kmalloc-16 0 0 320 25 2 : tunables 0 0 0 : slabdata 0 0 0 dma-kmalloc-8 0 0 312 26 2 : tunables 0 0 0 : slabdata 0 0 0 dma-kmalloc-192 0 0 496 16 2 : tunables 0 0 0 : slabdata 0 0 0 dma-kmalloc-96 0 0 400 20 2 : tunables 0 0 0 : slabdata 0 0 0 kmem_cache_node 213 216 448 18 2 : tunables 0 0 0 : slabdata 12 12 0 kmem_cache 216 224 576 28 4 : tunables 0 0 0 : slabdata 8 8 0
-
每个slab cache的描述符(struct kmem_cache)中会包含一个Per-CPU结构struct array_cache,该结构体用来作为对应slab cache对象的本地CPU高速缓存,缓存的对象是slab obj.
-
每个slab cache的描述符中还包含了一个struct kmem_cache_node数组成员node:
- node数组长度为系统内存节点个数,也就是每个slab cache会给os的每个内存节点分配一个kmem_cache_node结构体(node[nodeid]),kmem_cache_node结构体用来缓存和管理对应内存节点中的所有slab。
- 每个kmem_cache_node结构中包含3个链表slabs_partial、slabs_full和slabs_free,每个链表存储的都是对应内存节点中的slab描述符。其中slabs_partial链表中存储的slab既包含空闲的 slab obj,也包含已经被使用的slab obj。slabs_full链表中的slab只包含处于使用状态的slab obj。而slab_free链表中的slab则只包含处于空闲状态的slab
- 每个kmem_cache_node结构中还存在一个其他node共享的stuct array_cache成员shared,shared成员中的entry数组存储的是本内存节点中部分的slab obj,这些slab obj能被系统中的其他内存节点共享(本质是其他内存节点对应的cpu也能访问这些slab obj)
-
slab的描述符共用了页的结构描述符struct page,此时struct page中的flage标志成员置位了PG_SLAB位。
- 每个slab cache包含了一个或多个slab,同一个slab cache中的slab指向的内存区域大小一样(2^gfporder),且slab内部包含的slab obj数量也一样。
- slab描述符(struct page)中的s_mem成员指向了slab中第一个obj(存储obj的首地址),而freelist成员指向了slab的空闲obj索引数组,它结合描述符中的active成员就能获取第一个空闲obj在s_mem指向内存区域的索引。
- slab着色区的作用是为了错开不同的slab,让CPU更有效的缓存slab。