SLAB分配器
内存在进行申请和释放内存的情况下,难免会产生碎片。Linux采用伙伴系统解决外部碎片的问题,采用slab解决内部碎片的问题。
什么是碎片
内存碎片是由内存的申请和释放产生的,通常分为内部碎片和外部碎片。
- 内部碎片是由于采用固定大小的内存分区,即以固定的大小块为单位来分配,采用这种方法,进程所分配的内存可能会比所需要的大,这多余的部分便是内部碎片。
- 外部碎片是由于未分配的连续内存区域太小,以至于不能满足任意进程所需要的内存分配请求,这些小片段且不连续的内存空间被称为外部碎片。
SLAB分配器
malloc函数是我们常用的用于分配内存空间的函数,同样,内核也经常需要分配内存,但如果按照伙伴系统分配内存,一次就会分配一个4KB的完整页面,这个单位太大了,所以需要把页拆分成更小的单位,可以容纳大量小对象。为此,这里引入slab分配器。
提供小内存并不是slab分配器的唯一任务。由于结构的特点,它也可以作为一个缓存,主要针对经常分配和释放的对象。通过建立slab缓存,内核能够储备一些对象供之后使用。举个例子,内核每创建一个进程都会给进程分配一个mm_struct,考虑到系统中进程的数量,内核需要维护大量的mm_struct,而且该结构体在内核中初始化、分配和释放的频率相当大,为了应付这种场景,slab为这样的对象创建缓存,slab分配器将释放的内存块保存在内部列表中,并不马上返回给伙伴系统,在下一次请求该类型对象实例时,会使用最近释放的内存块。通过这种方式,内核会减少使用伙伴系统的次数,减少处理时间;也可以使内存块驻留在CPU高速缓存的概率增大。
slab分配器对大多数系统都工作良好,但是在另一些情况中,它无法提供最优性能,为此在内核版本2.6后增加了两个slab分配器的替代品:
- slob分配器为了配合某些微小嵌入式系统,减少了代码量。围绕一个简单的内存块链展开,在分配内存时使用了同样简单的最先适配算法。
- slub分配器为了配合大规模并行系统,通过将页帧打包为组,并用struct page中未使用的字段来管理这些组,试图最小化所需要的内存开销。
SLAB分配器结构
slab分配器由下图所示的两部分组成:保存管理性数据的缓存对象和保存被管理对象的各个slab。
数据结构
每个内存缓存都对应一个kmem_cache实例。
/* include/linux/slab_def.h */
/* slab分配器中的slab高速缓存 */
struct kmem_cache {
/* 指向包含空闲对象的本地高速缓存,每个CPU有一个该结构,当有对象释放时,优先放入本地CPU高速缓存中 */
struct array_cache __percpu *cpu_cache;
/* 1) Cache tunables. Protected by slab_mutex */
/* 要转移进本地高速缓存或从本地高速缓存中转移出去的对象的数量 */
unsigned int batchcount;
/* 本地高速缓存中空闲对象的最大数目 */
unsigned int limit;
/* 是否存在CPU共享高速缓存,CPU共享高速缓存指针保存在kmem_cache_node结构中 */
unsigned int shared;
/* 对象长度 + 填充字节 */
unsigned int size;
/* size的倒数,加快计算 */
struct reciprocal_value reciprocal_buffer_size;
/* 2) touched by every alloc & free from the backend */
/* 高速缓存永久属性的标识,如果SLAB描述符放在外部,则CFLAGS_OFF_SLAB置1 */
slab_flags_t flags; /* constant flags */
/* 每个SLAB中对象的个数(在同一个高速缓存中slab中对象个数相同) */
unsigned int num; /* # of objs per slab */
/* 3) cache_grow/shrink */
/* order of pgs per slab (2^n) */
/* slab的阶数 */
unsigned int gfporder;
/* force GFP flags, e.g. GFP_DMA */
/* 分配页框时传递给伙伴系统的一组标识 */
gfp_t allocflags;
/* SLAB使用的颜色范围 */
size_t colour; /* cache colouring range */
/* SLAB中基本对齐偏移,当新SLAB着色时,偏移量需要乘上这个基本对齐偏移,通俗讲就是1个偏移量等于多少B大小的值 */
unsigned int colour_off; /* colour offset */
/* 空闲对象链表放在外部时使用,其指向的SLAB高速缓存来存储空闲对象链表 */
struct kmem_cache *freelist_cache;
/* 空闲对象链表的大小 */
unsigned int freelist_size;
/* constructor func */
/* 构造函数,一般用于初始化这个SLAB高速缓存中的对象 */
void (*ctor)(void *obj);
/* 4) cache creation/removal */
/* 高速缓存的名字 */
const char *name;
/* 高速缓存描述符双向链表指针 */
struct list_head list;
int refcount;
/* 高速缓存中对象的大小 */
int object_size;
int align;
/* 5) statistics */
#ifdef CONFIG_DEBUG_SLAB
unsigned long num_active;
unsigned long num_allocations;
unsigned long high_mark;
unsigned long grown;
unsigned long reaped;
unsigned long errors;
unsigned long max_freeable;
unsigned long node_allocs;
unsigned long node_frees;
unsigned long node_overflow;
atomic_t allochit;
atomic_t allocmiss;
atomic_t freehit;
atomic_t freemiss;
#ifdef CONFIG_DEBUG_SLAB_LEAK
atomic_t store_user_clean;
#endif
/*
* If debugging is enabled, then the allocator can add additional
* fields and/or padding to every object. 'size' contains the total
* object size including these internal fields, while 'obj_offset'
* and 'object_size' contain the offset to the user object and its
* size.
*/
/* 对象间的偏移 */
int obj_offset;
#endif /* CONFIG_DEBUG_SLAB */
#ifdef CONFIG_MEMCG
/* 用于分组资源限制 */
struct memcg_cache_params memcg_params;
#endif
#ifdef CONFIG_KASAN
struct kasan_cache kasan_info;
#endif
#ifdef CONFIG_SLAB_FREELIST_RANDOM
unsigned int *random_seq;
#endif
unsigned int useroffset; /* Usercopy region offset */
unsigned int usersize; /* Usercopy region size */
/* 结点链表,此高速缓存可能在不同NUMA的结点都有SLAB链表 */
struct kmem_cache_node *node[MAX_NUMNODES];
};
其中,gfporder是slab的阶数,num是每个slab包含的对象数量,object_size是对象的原始长度,size是包括填充的对象长度,node用于存放结点链表。每一个结点链表对应一个kmem_cache_node实例。
/* mm/slab.h */
struct kmem_cache_node {
/* 锁 */
spinlock_t list_lock;
#ifdef CONFIG_SLAB
/* 只使用了部分对象的SLAB描述符的双向循环链表 */
struct list_head slabs_partial; /* partial list first, better asm code */
/* 满SLAB对象的SLAB描述符的双向循环链表 */
struct list_head slabs_full;
/* 只包含空闲对象的SLAB描述符的双向循环链表 */
struct list_head slabs_free;
unsigned long total_slabs; /* length of all slab lists */
unsigned long free_slabs; /* length of free slab list only */
/* 高速缓存中空闲对象个数(包括slabs_partial和slabs_free中的空闲对象) */
unsigned long free_objects;
/* 高速缓存中空闲对象的上限 */
unsigned int free_limit;
/* 下一个被分配的SLAB使用的颜色 */
unsigned int colour_next; /* Per-node cache coloring */
/* 指向这个结点上所有CPU共享的一个本地高速缓存 */
struct array_cache *shared; /* shared per node */
struct alien_cache **alien; /* on other nodes */
/* 两次缓存收缩时的间隔,降低次数,提高性能 */
unsigned long next_reap; /* updated without locking */
/* 0:收缩,1:获取一个对象 */
int free_touched; /* updated without locking */
#endif
};
kmem_cache_node实例包括三条slab链表:slabs_partial链表把部分对象空闲的slab链接起来,slabs_full链表把没有空闲对象的slab链接起来,slabs_free链表把所有空闲对象的slab链接起来,total_slabs统计slab个数。
page结构体中与slab相关的成员:
- flags设置标志位PG_slab,表示页属于SLAB分配器。
- s_mem存放slab第一个对象的地址。
- active表示已分配对象的数量。
- lru作为链表结点加入其中一条slab链表。
- slab_cache指向kmem_cache实例。
- freelist指向空闲对象链表。
kmem_cache实例的成员cpu_cache指向array_cache实例,每一个处理器都对应一个array_cache,称为数组缓存,用来存放刚刚被释放的对象,分配时会首先从当前处理器的数组缓存中分配,如果没有空闲的再去找对象链表,这样可以避免每次都要从slab中分配,减少链表的处理次数或锁操作,提高效率。
每个对象的结构如上图所示,其中:
- 红色区域1长度为8字节,如果值被修改说明对象被改写过。
- 真实对象长度是kmem_cache.obj_size,偏移是kmem_cache.obj_offset。
- 填充区域用于对齐。
- 红色区域2长度为8字节,如果值被修改说明对象被改写过。
- 最后一个使用者在64位系统上长度为8字节,存放最后一个调用者的地址,用来确定对象是被谁改写的。
API实现
- void *kmalloc(size_t size, gfp_t flags);
/* include/linux/slab.h */
static __always_inline void *kmalloc(size_t size, gfp_t flags)
{
if (__builtin_constant_p(size)) {
if (size > KMALLOC_MAX_CACHE_SIZE)
return kmalloc_large(size, flags);
#ifndef CONFIG_SLOB
if (!(flags & GFP_DMA)) {
unsigned int index = kmalloc_index(size);
if (!index)
return ZERO_SIZE_PTR;
return kmem_cache_alloc_trace(kmalloc_caches[index],
flags, size);
}
#endif
}
return __kmalloc(size, flags);
}
其中,传入参数size是申请的空间大小,flags是传给页分配器的分配标识符。
函数入口判断语句if (__builtin_constant_p(size))是GCC内建函数,用于判断一个值是否为编译时常量。当kmalloc()传入常量size大于KMALLOC_MAX_CACHE_SIZE时即申请的空间大于kmalloc所能分配最大cache大小时,将交于kmalloc_large函数进行分配,过程为kmalloc_large->kmalloc_order->__get_free_pages,最终通过伙伴算法申请所需内存。而另一条路径是通过__kmalloc函数进行分配。
/* mm/slab.c */
void *__kmalloc(size_t size, gfp_t flags)
{
return __do_kmalloc(size, flags, _RET_IP_);
}
static __always_inline void *__do_kmalloc(size_t size, gfp_t flags,
unsigned long caller)
{
struct kmem_cache *cachep;
void *ret;
if (unlikely(size > KMALLOC_MAX_CACHE_SIZE))
return NULL;
cachep = kmalloc_slab(size, flags);
if (unlikely(ZERO_OR_NULL_PTR(cachep)))
return cachep;
ret = slab_alloc(cachep, flags, caller);
kasan_kmalloc(cachep, ret, size, flags);
trace_kmalloc(caller, ret,
size, cachep->size, flags);
return ret;
}
/* mm/slab_common.c */
struct kmem_cache *kmalloc_slab(size_t size, gfp_t flags)
{
unsigned int index;
if (size <= 192) {
if (!size)
return ZERO_SIZE_PTR;
index = size_index[size_index_elem(size)];
} else {
if (unlikely(size > KMALLOC_MAX_CACHE_SIZE)) {
WARN_ON(1);
return NULL;
}
index = fls(size - 1);
}
#ifdef CONFIG_ZONE_DMA
if (unlikely((flags & GFP_DMA)))
return kmalloc_dma_caches[index];
#endif
return kmalloc_caches[index];
}
如果申请的内存大小小于192且不为0,将通过size_index_elem宏转换为下标后,由size_index全局数组取得索引值;否则直接通过fls()取得索引值。
- void kfree(const void *objp)
void kfree(const void *objp)
{
struct kmem_cache *c;
unsigned long flags;
/* 记录kfree轨迹 */
trace_kfree(_RET_IP_, objp);
/* 对地址做非零判断 */
if (unlikely(ZERO_OR_NULL_PTR(objp)))
return;
local_irq_save(flags);
kfree_debugcheck(objp);
/* 将虚拟地址转换为缓存 */
c = virt_to_cache(objp);
debug_check_no_locks_freed(objp, c->object_size);
debug_check_no_obj_freed(objp, c->object_size);
__cache_free(c, (void *)objp, _RET_IP_);
local_irq_restore(flags);
}
该函数首先经过trace_kfree函数记录kfree轨迹,然后对objp地址进行非零判断,接着virt_to_cache将对象的虚拟地址转换为缓存,这个函数会返回一个缓存引用,然后再__cache_free调用中使用该引用释放对象。
- struct kmem_cache * kmem_cache_create(const char *name, unsigned int size, unsigned int align, slab_flags_t flags, void (ctor)(void ))
/* mm/slab_common.c */
/* name:名称;size:对象长度;align:对象需要对齐的数值;flags:SLAB标志位;ctor:对象的构造函数 */
struct kmem_cache *
kmem_cache_create(const char *name, unsigned int size, unsigned int align,
slab_flags_t flags, void (*ctor)(void *))
{
return kmem_cache_create_usercopy(name, size, align, flags, 0, 0,
ctor);
}
创建缓存通过接口kmem_cache_create进行,创建新的缓存主要内容是申请管理结构所需要的空间,并初始化。这些管理结构包括kmem_cache、kmem_cache_node、kmem_cache_cpu。最后把kmem_cache加入到全局的缓存链表中。注意此时只是创建了size大小的缓存,并不会调用对象构造函数,只有当调用kmem_cache_alloc函数时才会调用构造函数。
- void kmem_cache_alloc(struct kmem_cache cachep, gfp_t flags)
/* mm/slab.c */
函数调用过程:
kmem_cache_alloc -> slab_alloc -> __do_cache_alloc ->
____cache_alloc -> cache_alloc_refill
/* 分配对象时,先会从当前处理器的数组缓存中分配对象,采用后进先出的原则,这样可以提高性能。*/
static void *cache_alloc_refill(struct kmem_cache *cachep, gfp_t flags)
{
int batchcount;
struct kmem_cache_node *n;
struct array_cache *ac, *shared;
int node;
void *list = NULL;
struct page *page;
check_irq_off();
node = numa_mem_id();
/* 获取当前CPU对应的array cache */
ac = cpu_cache_get(cachep);
batchcount = ac->batchcount;
/* 对批量申请的slab对象数进行重新调整 */
if (!ac->touched && batchcount > BATCHREFILL_LIMIT) {
batchcount = BATCHREFILL_LIMIT;
}
n = get_node(cachep, node);
BUG_ON(ac->avail > 0 || !n);
shared = READ_ONCE(n->shared);
if (!n->free_objects && (!shared || !shared->avail))
goto direct_grow;
spin_lock(&n->list_lock);
shared = READ_ONCE(n->shared);
/* See if we can refill from the shared array */
if (shared && transfer_objects(ac, shared, batchcount)) {
shared->touched = 1;
goto alloc_done;
}
/*
* 内核必须找到array_cache->batchcount个未使用对象重新填充per-CPU缓存,
* get_first_slab函数将首先扫描部分空闲slab链表,然后通过 slab_get_obj依次获取所有对象,直至相应的slab中没有空闲对象为止,
* 然后遍历空闲slab链表,
* slab获取对象时,内核必须将slab放置到正确的slab链表中
* 直到batchcount == 0
*/
while (batchcount > 0) {
/* Get slab alloc is to come from. */
page = get_first_slab(n, false);
if (!page)
goto must_grow;
check_spinlock_acquired(cachep);
/*
* alloc_block中通过while循环检查slab缓存中的对象是否已经全部被使用,
* 并更新batchcount值
* 最后通过slab_get_obj函数将对象填充到array cache中
*/
batchcount = alloc_block(cachep, ac, page, batchcount);
/*
* 将该slab从之前的链表中游离出来
* 如果该slab对象全部被分配出去,则把它放到满slab链表
* 否则放到部分空闲slab链中
*/
fixup_slab_list(cachep, n, page, &list);
}
must_grow:
n->free_objects -= ac->avail;
alloc_done:
spin_unlock(&n->list_lock);
fixup_objfreelist_debug(cachep, &list);
direct_grow:
if (unlikely(!ac->avail)) {
/* Check if we can use obj in pfmemalloc slab */
if (sk_memalloc_socks()) {
void *obj = cache_alloc_pfmemalloc(cachep, n, flags);
if (obj)
return obj;
}
page = cache_grow_begin(cachep, gfp_exact_node(flags), node);
/*
* cache_grow_begin() can reenable interrupts,
* then ac could change.
*/
ac = cpu_cache_get(cachep);
if (!ac->avail && page)
alloc_block(cachep, ac, page, batchcount);
cache_grow_end(cachep, page);
if (!ac->avail)
return NULL;
}
ac->touched = 1;
/* 对象分配 */
return ac->entry[--ac->avail];
}
- void kmem_cache_free(struct kmem_cache cachep, void objp)
/* mm/slab.c */
void kmem_cache_free(struct kmem_cache *cachep, void *objp)
{
unsigned long flags;
/* 获取回收对象的kmem_cache并检查这个slab是否合法 */
cachep = cache_from_obj(cachep, objp);
if (!cachep)
return;
/* 释放mem的时候,会禁止当前cpu的irq */
local_irq_save(flags);
debug_check_no_locks_freed(objp, cachep->object_size);
if (!(cachep->flags & SLAB_DEBUG_OBJECTS))
debug_check_no_obj_freed(objp, cachep->object_size);
/*
* 释放mem
* 会首先检查per-CPU数组中是否有空间可用
* 是,会将对象加入缓存
* 否,将一些对象从缓存移回slab
*/
__cache_free(cachep, objp, _RET_IP_);
local_irq_restore(flags);
trace_kmem_cache_free(_RET_IP_, objp);
}
SLUB分配器结构
slub的数据结构相对于slab来说要简单很多,并且对外接口和slab兼容。
与slab相同,每个内存缓存都对应一个kmem_cache实例。
/* include/linux/slub_def.h */
struct kmem_cache {
/* per-CPU变量,对于每个cpu来说相当于一个本地内存缓存池 */
struct kmem_cache_cpu __percpu *cpu_slab;
/* Used for retriving partial slabs etc */
slab_flags_t flags;
/* 限制kmem_cache_node中partial链表slab的数量 */
unsigned long min_partial;
/* 包括元数据的对象长度 */
unsigned int size; /* The size of an object including meta data */
/* 对象的原始长度 */
unsigned int object_size;/* The size of an object without meta data */
/*
* slub分配在管理object的时候采用的方法是:
* 既然每个object在没有分配之前不在乎每个object中存储的内容,
*那么完全可以在每个object中存储下一个object内存首地址,就形成了一个单链表。
* offset存储下一个object地址数据相对于这个object首地址的偏移量
*/
unsigned int offset; /* Free pointer offset. */
#ifdef CONFIG_SLUB_CPU_PARTIAL
/* Number of per cpu partial objects to keep around */
/*
* per-CPU的partial中所有slab的free object的数量的最大值
* 超过这个值就会把所有slab转移到kmem_cache_node的partial链表中
*/
unsigned int cpu_partial;
#endif
/* 存放最优slab的阶数和对象数,低16位是对象数,高16位是slab阶数,即oo等于((slab的阶数<<16)|对象数)。最优slab是剩余部分最小的slab */
struct kmem_cache_order_objects oo;
/* Allocation and freeing of slabs */
/* 与oo相等 */
struct kmem_cache_order_objects max;
/* 当按照oo大小分配内存的时候出现内存不足就会考虑用min大小分配,最小slab只需要足够存放一个对象 */
struct kmem_cache_order_objects min;
/* 从伙伴系统分配内存掩码 */
gfp_t allocflags; /* gfp flags to use on each alloc */
int refcount; /* Refcount for slab cache destroy */
void (*ctor)(void *);
/* 按照元数据的对象对齐后的大小 */
unsigned int inuse; /* Offset to metadata */
/* 字节对齐大小 */
unsigned int align; /* Alignment */
unsigned int red_left_pad; /* Left redzone padding size */
/* sysfs文件系统显示使用 */
const char *name; /* Name (only for display!) */
/* 系统有一个slab_cache链表,所有的slab都会挂在这个链表上 */
struct list_head list; /* List of slab caches */
#ifdef CONFIG_SYSFS
struct kobject kobj; /* For sysfs */
struct work_struct kobj_remove_work;
#endif
#ifdef CONFIG_MEMCG
struct memcg_cache_params memcg_params;
/* for propagation, maximum size of a stored attr */
unsigned int max_attr_size;
#ifdef CONFIG_SYSFS
struct kset *memcg_kset;
#endif
#endif
#ifdef CONFIG_SLAB_FREELIST_HARDENED
unsigned long random;
#endif
#ifdef CONFIG_NUMA
/*
* Defragmentation by allocating from a remote node.
*/
unsigned int remote_node_defrag_ratio;
#endif
#ifdef CONFIG_SLAB_FREELIST_RANDOM
unsigned int *random_seq;
#endif
#ifdef CONFIG_KASAN
struct kasan_cache kasan_info;
#endif
unsigned int useroffset; /* Usercopy region offset */
unsigned int usersize; /* Usercopy region size */
/* slab节点。在NUMA系统中,每个node都有一个kmem_caceh_node数据结构 */
struct kmem_cache_node *node[MAX_NUMNODES];
};
每个内存结点都对应一个kmem_cache_node实例。
struct kmem_cache_node {
spinlock_t list_lock;
#ifdef CONFIG_SLUB
/* slab结点中slab的数量 */
unsigned long nr_partial;
/* slab结点的slab partial链表,和kmem_cache_cpu的partial链表功能类似 */
struct list_head partial;
#ifdef CONFIG_SLUB_DEBUG
atomic_long_t nr_slabs;
atomic_long_t total_objects;
struct list_head full;
#endif
#endif
};
kmem_cache_cpu是对本地内存缓存池的描述,每个cpu对应一个kmem_cache_cpu结构体。
struct kmem_cache_cpu {
/* 指向下一个可用object */
void **freelist; /* Pointer to next available object */
/* 用于同步 */
unsigned long tid; /* Globally unique transaction id */
/* slab内存的page指针 */
struct page *page; /* The slab from which we are allocating */
#ifdef CONFIG_SLUB_CPU_PARTIAL
/* 本地slab partial链表 */
struct page *partial; /* Partially allocated frozen slabs */
#endif
#ifdef CONFIG_SLUB_STATS
unsigned stat[NR_SLUB_STAT_ITEMS];
#endif
};
page结构体中与slub相关的成员:
- flags设置标志位PG_slab,表示页属于SLUB分配器。
- freelist指向第一个空闲对象。
- inuse表示已分配对象的数量。
- objects表示slab对象的数量。
- frozen表示slab是否被冻结在每处理器slab缓存中。如果slab在每处理器slab缓存中,它处于冻结状态;如果slab在内存结点的部分空闲slab链表中,它处于解冻状态。
- lru作为链表结点加入部分空闲slab链表。
- slab_cache指向kmem_cache实例。
SLAB分配器的每处理器数组entry是以对象为单位的,而SLUB分配器的每处理器slab缓存是以slab位单位的,freelist指向下一个可用对象,page指向当前正在使用的slab对应的page实例,partial指向每处理器部分空闲slab链表。
SLUB中对象有两种内存布局,区别是空闲指针的位置不同。
两种内存布局的区别是空闲指针的位置,第二种布局空闲指针充用真实对象的第一个字。kmem_cache.offset是空闲指针的偏移,空闲指针的地址等于真实对象的地址+空闲指针偏移。
每个对象的结构如上图所示,其中:
- 红色区域1的长度 = kmem_cache.red_left_pad = 字长对齐到指定的对齐值
- 红色区域2的长度 = 字长 - (对象长度 % 字长)
SLOB分配器结构
与slab相比,slob的特点就是小巧,代码只有600行左右,适合某些微小嵌入式系统。
每个内存缓存对应一个kmem_cache实例。所有内存缓存共享slab,所有对象长度小于256字节的内存缓存共享小对象slab链表中的slab,所有对象长度小于1024字节的内存缓存共享中等对象slab链表中的slab,所有对象长度小于一页的内存缓存共享大对象slab链表中的slab。对象长度大于或等于一页的内存缓存,直接从页分配器分配页。每个slab大小是一页,page结构体中与slob相关的成员:
- flags设置标志位PG_slab,表示页属于SLOB分配器;设置标志位PG_slob_free表示slab在slab链表中。
- freelist指向第一个空闲对象。
- units为空闲单元的数量。
- lru作为链表节点加入slab链表。
引用
《Linux内核深度解析》
《深入Linux内核架构》
http://www.wowotech.net/memory_management/426.html