slab分配器基本思想:将内核中经常使用的对象放到高速缓存中,并且由系统保持为初始的可利用状态。
slab分配器目的:缓存空闲的对象,保留基本结构,从而能够重复使用它们。
每一个高速缓存包括若干slab,slab由连续的页面帧组成,他们被划分成许多小的块以存放由高速缓存所管理的数据结构和对象。不同数据结构之间的关系如下图所示(slab分配器布局)。
slab分配器三个基本目标:
为了解决伙伴分配器产生的内部碎片,系统维护两个高速缓存集,这些高速缓存集由小内存缓存组成(大小从25B到217B)。还有一个高速缓存集供DMA设备使用。这些高速缓存集叫做size-N and size-N(DMA),N是分配器的尺寸。函数 kmalloc() 负责分配这些高速缓存。
下图所示,由伙伴分配器分配的页面如何存储用颜色填充的对象,这些用颜色填充的对象对齐L1 CPU cache。
分配器的API:
对于要缓存的每种类型的对象都存在一个缓存。执行 cat /proc/slabinfo 可以得到运行系统中完整的高速缓存列表。这个文件包含高速缓存的基本信息。
其中每一列对应struct kmem_cache_s结构中的一个字段,含义如下:
书中给出的图
为加快分配、释放对象和slab自身的速度,高速缓存中的slab被组织成3个链表:slabs_full,slabs_partial,slabs_free。
struct kmem_cache_s {
/* 1) each alloc & free */
/* full, partial first, then free */ //对分配和释放对象很重要的成员
struct list_head slabs_full; //其中所有对象都在使用中。
struct list_head slabs_partial; //其中有空闲对象,所以分配对象时它是主要的候选。
struct list_head slabs_free; //其中没有已分配的对象,因此它主要用于slab销毁。
unsigned int objsize; //slab中每一个对象的大小
unsigned int flags; /* constant flags */ //标志位,决定分配器如何处理高速缓存
unsigned int num; /* # of objs per slab */ //每一个slab中包含的对象个数
spinlock_t spinlock; //并发控制锁,避免并发访问高速缓存描述符
#ifdef CONFIG_SMP
unsigned int batchcount;
#endif
/* 2) slab additions /removals */
/* order of pgs per slab (2^n) */ //从高速缓存中分配和释放slab时将用到的字段
unsigned int gfporder;
/* force GFP flags, e.g. GFP_DMA */
unsigned int gfpflags;
size_t colour; /* cache colouring range */
unsigned int colour_off; /* colour offset */
unsigned int colour_next; /* cache colouring */
kmem_cache_t *slabp_cache;
unsigned int growing;
unsigned int dflags; /* dynamic flags */
/* constructor func */
void (*ctor)(void *, kmem_cache_t *, unsigned long);
/* de-constructor func */
void (*dtor)(void *, kmem_cache_t *, unsigned long);
unsigned long failures;
/* 3) cache creation/removal */ //在创建高速缓存时设置
char name[CACHE_NAMELEN];
struct list_head next;
#ifdef CONFIG_SMP
/* 4) per-cpu data */
cpucache_t *cpudata[NR_CPUS];
#endif
#if STATS //在编译时设置CONFIG_SLAB_DEBUG
unsigned long num_active;
unsigned long num_allocations;
unsigned long high_mark;
unsigned long grown;
unsigned long reaped;
unsigned long errors;
#ifdef CONFIG_SMP
atomic_t allochit;
atomic_t allocmiss;
atomic_t freehit;
atomic_t freemiss;
#endif
#endif
};
第一组是内部标志位,仅由slab分配器使用。
第二组在创建高速缓存时设置,这些标志位决定分配器如何处理slab,如何存储对象。
第三组在编译选项CONFIG_SLAB_DEBUG设置后才有效,它们决定对slab和对象做哪些附加的检查,主要与新建的高速缓存相关。
dflag中只有一个标志位DFLAG_GROWN,在kmem_cache_grow()中设置,kmem_cache_reap()就不会选择该高速缓存进行回收。kmeme_cache_reap()将跳过设置了该标志位的高速缓存,并清除该标志位。
下表列出的这些标志位,在为slab分配页面时对应的GFP page flag。
有少量的flags可能传递给构造函数和析构函数。如下所示:
在cache创建期间,可以计算出一个slab可以容纳多少对象,还可以计算出会waste多少字节。根据wastage,可以为cache descriptor计算出两个数值:
kmem_cache_create()用于创建高速缓存并把他们添加到高速缓存链表中。
一个slab空闲时,系统将其放到slab_free链表中,以备将来使用。当kswapd发现内存紧张时,调用kmem_cache_reap()释放内存。对每一个已经检查过的高速缓存中,回收器做如下事:
kmem cache shrink() removes all slabs from slabs_free and returns the number of pages freed as a result. This is the principal function exported for use by the slab allocator users.
kmem cache shrink() frees all slabs from slabs_free and then verifies that slabss_partial and slabs_full are empty. This is for internal use only and is important during cache destruction when it doesn’t matter how many pages are freed, just that the cache is empty.
一个模块卸载时,Linux销毁与之相关的所有高速缓存。使用kmem_cache_destroy()函数。销毁一个高速缓存的步骤如下:
// /mm/slab.c
/*
* slab_t
*
* Manages the objs in a slab. Placed either at the beginning of mem allocated
* for a slab, or allocated from an general cache.
* Slabs are chained into three list: fully used, partial, fully free slabs.
*/
typedef struct slab_s {
struct list_head list;
unsigned long colouroff; //slab中相对于第一个对象基地址的着色偏移。第一个对象的地址是s_mem + colouroff
void *s_mem; /* including colour offset */ //slab中第一个对象的起始地址
unsigned int inuse; /* num of objs active in slab */ //slab中活动的对象数目
kmem_bufctl_t free; //用于存储空闲对象的位置
} slab_t;
typedef unsigned int kmem_bufctl_t;
slab中给定的slab管理器或者对象,没有明显的方法判断它属于哪一个slab或者高速缓存。这里通过struct page里的list字段中的next和prev字段来跟踪对象属于哪一个高速缓存或slab。
SET_PAGE_CACHE()和SET_PAGE_SLAB()函数使用page->list中的next和prev字段来跟踪对象属于哪一个高速缓存或者slab。也可以通过GET_PAGE_CACHE()和GET_PAGE_SLAB()从页面中得到高速缓存或者slab描述符。这些关系如下图所示。
slab描述符的位置由创建高速缓存时对象的大小决定,slab描述符可以存储在slab内,也可以存储在slab外。struct slab_t可以存储在页面帧的首部。
对象大于阀值时,就需要设置CFGS_OFF_SLAB,slab描述符保存在slab之外的某个指定大小的高速缓存中,在需要的时候,从kmem_cache_slabmgmt()函数中进行分配。
还可以将slab manager存储在slab的起始处。起始处要留有足够的空间存储slat_t和无符号整型数组kmem_bufctl_t(这个数组用于跟踪下一个可用对象的索引)。实际的对象保存在kmem_bufctl_t数组后。on-slab类型和off-slab类型的图示:
高速缓存在创建时,是一个空的高速缓存,slabs_full,slabs_partial,slabs_free也都是空的。通过调用kmem_cache_grow()给高速缓存分配新的slab。在slabs_partial没有对象或者slabs_free中没有slab时也会调用该函数。
调用函数kmem_cache_grow()给高速缓存分配新的slab,调用图如下所示:
该函数完成的任务如下:
kmem_bufctl_t是一个简单的对象索引整形数组。得到kmem_bufctl_t
数组:
#define slab_bufctl(slabp) \
((kmem_bufctl_t *)(((slab_t*)slabp)+1))
slab中下一个空闲对象的索引存储在slab->free
中,分配或者释放对象时,slab->free
字段基于kmem_bufctl_t
中的信息更新。
当高速缓存增长时初始化slab中的对象和kmem_bufctl_t
数组。数组被填充为每一个对象的索引,从1开始以标记BUFCTL_END结束。数值0存储在slab_t->free
中。对于第n个对象,其下一个空闲对象索引将被存储在em_bufctl_t[n]
中,这样排列的数组是一个先进后出
队列(LIFO)。
下一个空闲对象索引在kmem_bufctl_t[slab->free]
。
kmem_cache_free_one()
中释放一个对象时才需要更新数组kmem_bufctl_t
。更新数组的代码的例子如下:
// /mm/slab.c
unsigned int objnr = (objp - slabp->s_mem)/cachep->objsize;
slab_bufctl(slabp)[objnr] = slabp->free;
slabp->free = objnr;
kmem_cache_estimate()
函数计算在单个slab上可以存放多少对象。该函数返回可能存储的对象个数
以及浪费的字节数
。
基本的计算步骤:
kmem_cache_init_objs()
负责初始化对象。遍历slab中所有对象,调用构造函数,并为它初始化kmem_bufctl。
kmem_cache_alloc()
负责分配一个对象给调用者。
kmem_cache_free()
用于释放对象。在UP中,对象直接返回slab;而在SMP中,对象返回给per-CPU高速缓存。若对象由可用的析构函数的话,函数会调用。析构函数负责把对象状态返回到初始化状态。
参考文献:
[1] 白洛. 深入理解Linux虚拟内存管理. 2006-1
[2] Mel Gorman. Understanding the Linux Virtual Memory Manager. 2004-5-9