本文主要以内核Linux 4.19来简单说明slab分配器的大概流程。slab是操作系统的一种机制,slab分配算法采用cache 存储。slab 缓存、从缓存中分配和释放对象然后销毁缓存的过程必须要定义一个 kmem_cache 对象,然后对其进行初始化这个特定的缓存包含 32 字节的对象(来自百度百科)。为了防止内存碎片, Linux在slab机制中明确的将伙伴子系统中的page 分成了多组内存大小不同的片段,如96, 192, 2^3,2^4...等,不同大小的片段属于不同的kmem_cache, 因此,当用户调用kmalloc申请内存的时候,只会从对应内存大小片段的kmem_cache中去分配。
首先如下是一个kmem_cache结构(Linux中存在多个这样的结构,每个这样的结构可以管理分配对应大小的内存片段),这里只列出了关键的几个成员。
点击(此处)折叠或打开
struct kmem_cache {
struct array_cache __percpu *cpu_cache; // 用于找到空闲内存的结点信息
unsigned int batchcount; // 一次分配或者释放的大小为size的数量
unsigned int limit; //该kmem最大支持的size数量,也就是几个node的总和
unsigned int shared; // 共享数量
unsigned int size; // 每个内存分配块的大小(这个size包含给用户内存、pad,kasan大小的总和)
unsigned int num; // 每个order的page能分配的最大size个数
unsigned int gfporder; // 需要从order为rfporder的伙伴子系统申请page
gfp_t allocflags; // 申请page的flag
size_t colour;
unsigned int colour_off;
struct kmem_cache *freelist_cache; // 如果管理结构和slag不在一起,就会使用该结构
unsigned int freelist_size; // 管理结构大小
const char *name; // 名字
int object_size; // 用户能在当前kmem_cache申请的最大内存
int align; //对齐
#ifdef CONFIG_KASAN
struct kasan_cache kasan_info; // 指向kasan结构,主要用于检查内存越界使用的
#endif
unsigned int useroffset; /* Usercopy region offset */
unsigned int usersize; /* Usercopy region size */
struct kmem_cache_node *node[MAX_NUMNODES]; // 记录mem node(numa)的具体信息
}
kmem_cache_node结构如下(该结构表示在numa中的一个内存bank),
点击(此处)折叠或打开
struct kmem_cache_node {
spinlock_t list_lock;
struct list_head slabs_partial; // 用于存放没有完全使用的page
struct list_head slabs_full; // 用于存放完全使用了的page
struct list_head slabs_free; // 用于存放空闲的page,这里的page可能会被释放到伙伴子系统当中
unsigned long total_slabs; /* length of all slab lists */
unsigned long free_slabs; /* length of free slab list only */
unsigned long free_objects; // 空闲的大小为size的个数
unsigned int free_limit; // 空闲个数的最大值
unsigned int colour_next; /* Per-node cache coloring */
struct array_cache *shared; // 共享内存结点
struct alien_cache **alien; /* on other nodes */
unsigned long next_reap; /* updated without locking */
int free_touched; /* updated without locking */
}
从上可以了解到,从伙伴子系统中分配出来的page会被挂到对应内存node的partial/full/free等三个链表。当page刚被申请出来时在free链表,如果该page被部分使用了,则在partial链表,如果全部被使用了,则在full链表。对于array_cache结构如下,该结构是用于分配内存的时候,能快速拿到希望的内存结点而使用的,
点击(此处)折叠或打开
struct array_cache {
unsigned int avail; // cache中可用的内存片段数量
unsigned int limit; // ac中最大可用缓存的片段数量
unsigned int batchcount; // ac一次性申请或者释放的片段数量
unsigned int touched;
void *entry[];// 指向不同内存片段的内存地址
}
上述为kmem_cache的关键结构体,那么用图形表示为,
因此,之间的关系如图,page的lru会链接到mem_node的slabs_xxx链表上,而page的freelist用来记录该order的page的不同片段的起始地址,page->s_mem为page所代码的地址的起始地址。另外分配有些从kmem_cache的ac中分配,然后再是mem_node(ac只是mem_node的一个缓存而已)。
先看看kmem_cache的初始化,初始化栈如下,本章只会对几个函数做出说明,
点击(此处)折叠或打开
start_kernel
mm_init
kmem_cache_init // 初始化kmem_cache
create_boot_cache // 创建启动kmem,用于为创建其他kmem_cache的时候分配结点内存
create_kmalloc_cache // 创建用于分配kmem_cache的结点kmem
create_kmalloc_caches // 创建其他大小的kmem_cache
new_kmalloc_cache // 创建新的一个kmem_cache
create_kmalloc_cache
create_boot_cache
__kmem_cache_create // 初始化kmem_cache结构成员的默认值
kasan_cache_create // 预留Kasan的空间,即增加size大小
set_objfreelist_slab_cache // 设置管理page片段的管理结构位置
set_off_slab_cache // 设置 page片段管理结构不在slab上
set_on_slab_cache // 设置Page片段管理结构在slab上
setup_cpu_cache // 初始化ac,即array_cache结构
enable_cpucache
从上发现,在kmem_cache_init中,需要先调用create_boot_cache,这是为什么呢?这主要是因为后期在创建kmem_cache的时候也会申请内存,但是由于kmem_cache都还没有,又怎么能申请到内存呢,于是增加了一个boot_kmem_cache,这个结构是一个静态结构,不需要分配空间。这样,当初始化了这个结构后,后面去创建其他kmem_cache的时候,就能正常分配结点内存了。
点击(此处)折叠或打开
void __init kmem_cache_init(void)
{
int i;
kmem_cache = &kmem_cache_boot; // 准备初始化boot kmem cache
if (!IS_ENABLED(CONFIG_NUMA) || num_possible_nodes() == 1)
use_alien_caches = 0;
for (i = 0; i < NUM_INIT_LISTS; i++)
kmem_cache_node_init(&init_kmem_cache_node[i]); // 初始化kmem node信息
if (!slab_max_order_set && totalram_pages > (32 << 20) >> PAGE_SHIFT)
slab_max_order = SLAB_MAX_ORDER_HI;
// 初始化 boot kmem_cache
create_boot_cache(kmem_cache, "kmem_cache",
offsetof(struct kmem_cache, node) +
nr_node_ids * sizeof(struct kmem_cache_node *),
SLAB_HWCACHE_ALIGN, 0, 0);
list_add(&kmem_cache->list, &slab_caches); // 添加到slag_caches全局链表
memcg_link_cache(kmem_cache);
slab_state = PARTIAL; // slab_stae设置为PARTIAL,这影响array_cache和node初始化
//初始化用于申请node的结点
kmalloc_caches[INDEX_NODE] = create_kmalloc_cache(
kmalloc_info[INDEX_NODE].name,
kmalloc_size(INDEX_NODE), ARCH_KMALLOC_FLAGS,
0, kmalloc_size(INDEX_NODE));
slab_state = PARTIAL_NODE; // 设置slab_state为PARTAL_NODE,setup_cpu_cache会用到
setup_kmalloc_cache_index_table();
slab_early_init = 0;
{ // 将boot kmem cache的node数据内容和INDEX_NODE的node数据置换
int nid;
for_each_online_node(nid) {
init_list(kmem_cache, &init_kmem_cache_node[CACHE_CACHE + nid], nid);
init_list(kmalloc_caches[INDEX_NODE],
&init_kmem_cache_node[SIZE_NODE + nid], nid);
}
}
// 创建其他大小的kmem_cache,实现就是一个for循环,调用__kmem_cache_create的过程
create_kmalloc_caches(ARCH_KMALLOC_FLAGS);
}
其他的函数没什么说的, 下面说说__kmem_cache_create函数,该函数用来初始化kmem_cache,实现如下
点击(此处)折叠或打开
int __kmem_cache_create(struct kmem_cache *cachep, slab_flags_t flags)
{
size_t ralign = BYTES_PER_WORD;
gfp_t gfp;
int err;
unsigned int size = cachep->size;
size = ALIGN(size, BYTES_PER_WORD); //对齐
if (flags & SLAB_RED_ZONE) { // kasan对齐相关
ralign = REDZONE_ALIGN;
size = ALIGN(size, REDZONE_ALIGN);
}
if (ralign < cachep->align) {
ralign = cachep->align;
}
if (ralign > __alignof__(unsigned long long))
flags &= ~(SLAB_RED_ZONE | SLAB_STORE_USER);
cachep->align = ralign; //赋值对齐值
cachep->colour_off = cache_line_size();
if (cachep->colour_off < cachep->align)
cachep->colour_off = cachep->align;
if (slab_is_available()) // 申请标记
gfp = GFP_KERNEL;
else
gfp = GFP_NOWAIT;
kasan_cache_create(cachep, &size, &flags); // 如果支持kasan,则为kasan在用户申请内存中,预留空间
size = ALIGN(size, cachep->align);
if (FREELIST_BYTE_INDEX && size < SLAB_OBJ_MIN_SIZE)
size = ALIGN(SLAB_OBJ_MIN_SIZE, cachep->align);
// 这里需要注意,freelist用于管理对应order的page中分出来的大小为size的每个片段索引号,objfreelist, off slab, on slab三者只用其中一个,优先考虑objfreelist,然后是off slab,最后是on slab
if (set_objfreelist_slab_cache(cachep, size, flags)) {
flags |= CFLGS_OBJFREELIST_SLAB;
goto done;
}
if (set_off_slab_cache(cachep, size, flags)) {
flags |= CFLGS_OFF_SLAB;
goto done;
}
if (set_on_slab_cache(cachep, size, flags))
goto done;
return -E2BIG;
done:
cachep->freelist_size = cachep->num * sizeof(freelist_idx_t); // freelist大小
cachep->flags = flags;
cachep->allocflags = __GFP_COMP;
if (flags & SLAB_CACHE_DMA)
cachep->allocflags |= GFP_DMA;
if (flags & SLAB_CACHE_DMA32)
cachep->allocflags |= GFP_DMA32;
if (flags & SLAB_RECLAIM_ACCOUNT)
cachep->allocflags |= __GFP_RECLAIMABLE;
cachep->size = size;
cachep->reciprocal_buffer_size = reciprocal_value(size);
if (OFF_SLAB(cachep)) {
cachep->freelist_cache =
kmalloc_slab(cachep->freelist_size, 0u);
}
err = setup_cpu_cache(cachep, gfp); // 初始化 cpu cache和mem node
if (err) {
__kmem_cache_release(cachep);
return err;
}
return 0;
}
通过上面函数,我们大概知道用户调用kmalloc申请一个内存的时候,内存的分布
如上图,user_size才是用户申请的时候填写的数据大小,后面都是为了检测内存越界而预留的空间。
点击(此处)折叠或打开
static int __ref setup_cpu_cache(struct kmem_cache *cachep, gfp_t gfp)
{
if (slab_state >= FULL) // 刚开始 slab_state状态是DOWN,只有最后都成功了会是FULL
return enable_cpucache(cachep, gfp);
cachep->cpu_cache = alloc_kmem_cache_cpus(cachep, 1, 1);
if (!cachep->cpu_cache)
return 1;
if (slab_state == DOWN) { // 安装boot kmem的node信息
/* Creation of first cache (kmem_cache). */
set_up_node(kmem_cache, CACHE_CACHE);
} else if (slab_state == PARTIAL) {
/* For kmem_cache_node */
set_up_node(cachep, SIZE_NODE); // 安装size node的node信息
} else {
int node;
for_each_online_node(node) { // 为每个mem node申请内存结点
cachep->node[node] = kmalloc_node(
sizeof(struct kmem_cache_node), gfp, node);
BUG_ON(!cachep->node[node]);
kmem_cache_node_init(cachep->node[node]);
}
}
// 初始化ac结点
cachep->node[numa_mem_id()]->next_reap =
jiffies + REAPTIMEOUT_NODE +
((unsigned long)cachep) % REAPTIMEOUT_NODE;
cpu_cache_get(cachep)->avail = 0;
cpu_cache_get(cachep)->limit = BOOT_CPUCACHE_ENTRIES;
cpu_cache_get(cachep)->batchcount = 1;
cpu_cache_get(cachep)->touched = 0;
cachep->batchcount = 1;
cachep->limit = BOOT_CPUCACHE_ENTRIES;
return 0;
}
从上面知道,初始化的时候,ac的limit都是1,那么什么时候会改变limit的值呢?栈如下,
点击(此处)折叠或打开
start_kernel
kmem_cache_init_late
enable_cpucache // 这里会调整ac等的limit值
内存申请函数kmalloc,申请规则:kmalloc会先从对应kmem_cache的ac缓存中去获取内存片段,如果无法从ac中获取到内存片段,则尝试从node的shared中分配,如果依然无法分配则考虑从mem node的链表上去获取page,当然如果链表上也没有了,就只有从最慢的的伙伴子系统中去分配了,函数调用栈如下,
点击(此处)折叠或打开
kmalloc
kmalloc_large // 用户传入的size大于KMALLOC_MAX_CACHE_SIZE的时候,用kmalloc_order,实际就是从伙伴子系统中分配内存
kmalloc_order
alloc_pages
__kmalloc
__do_kmalloc
kmalloc_slab // 通过用户传入的size,查找需要从哪个kmem_cache中分配
slab_alloc
__do_cache_alloc
____cache_alloc // 从kmem_cache中分配内存
kasan_kmalloc // 将内存object_size没有用完的部分,填充kasan red pading
下面重点讲解 ___cache_alloc函数,该函数完成内存从kmem_cache中的分配,实现如下
点击(此处)折叠或打开
static inline void *____cache_alloc(struct kmem_cache *cachep, gfp_t flags)
{
void *objp;
struct array_cache *ac;
check_irq_off();
ac = cpu_cache_get(cachep); // 获取到当前kmem_cache的arrary_cache成员
if (likely(ac->avail)) { // 判断ac中是否还有未分配的内存片段,有就直接赋值分配。goto out
ac->touched = 1;
objp = ac->entry[--ac->avail];
STATS_INC_ALLOCHIT(cachep);
goto out;
}
// 因为ac中没有可用于分配的内存片段,则需要从slabs_partial或者slabs_free链表中去要
STATS_INC_ALLOCMISS(cachep);
objp = cache_alloc_refill(cachep, flags);
ac = cpu_cache_get(cachep);
out:
if (objp) // 如果分配内存成功,将ac中对该内存信息,进行清除,以免再次被分配
kmemleak_erase(&ac->entry[ac->avail]);
return objp;
}
从node链表slabs_partial、slabs_free中去获取内存片段的函数实现如下,
点击(此处)折叠或打开
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();
ac = cpu_cache_get(cachep); // 获取ac
batchcount = ac->batchcount; // 得到一次性要从node中申请的片段个数
if (!ac->touched && batchcount > BATCHREFILL_LIMIT) {
batchcount = BATCHREFILL_LIMIT;
}
n = get_node(cachep, node); // 得到cache对应的Node结构
BUG_ON(ac->avail > 0 || !n);
shared = READ_ONCE(n->shared); // 如果存在没有共享内存片段,并且结点从也没有内存片段,则需要跳转到direct_grow去向伙伴子系统进货
if (!n->free_objects && (!shared || !shared->avail))
goto direct_grow;
spin_lock(&n->list_lock);
shared = READ_ONCE(n->shared);
// 发现有共享内存片段,则优先从共享中申请batchcount个片段
if (shared && transfer_objects(ac, shared, batchcount)) {
shared->touched = 1;
goto alloc_done;
}
while (batchcount > 0) { // 还有片段未申请完,则调用get_first_slab从node的链表上获取
/* Get slab alloc is to come from. */
page = get_first_slab(n, false);
if (!page)
goto must_grow;
check_spinlock_acquired(cachep);
batchcount = alloc_block(cachep, ac, page, batchcount);
fixup_slab_list(cachep, n, page, &list); // 因为page中的片段被使用,则需要调整page所在的链表
}
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); //从伙伴子系统申请一个order的page
/*
* 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); // 释放到ac中缓存起来
cache_grow_end(cachep, page);
if (!ac->avail)
return NULL;
}
ac->touched = 1;
return ac->entry[--ac->avail]; // 返回第一个片段
}
get_first_slab实现如下,
点击(此处)折叠或打开
static struct page *get_first_slab(struct kmem_cache_node *n, bool pfmemalloc)
{
struct page *page;
assert_spin_locked(&n->list_lock);// 优先从slabs_partial中去获取page
page = list_first_entry_or_null(&n->slabs_partial, struct page, lru);
if (!page) {
n->free_touched = 1;
page = list_first_entry_or_null(&n->slabs_free, struct page,
lru);
if (page)
n->free_slabs--;
}
if (sk_memalloc_socks())
page = get_valid_first_slab(n, page, pfmemalloc);
return page;
}
fixup_slab_list实现如下,
点击(此处)折叠或打开
static inline void fixup_slab_list(struct kmem_cache *cachep,
struct kmem_cache_node *n, struct page *page,
void **list)
{
list_del(&page->lru);
if (page->active == cachep->num) {
list_add(&page->lru, &n->slabs_full);
if (OBJFREELIST_SLAB(cachep)) {
page->freelist = NULL;
}
} else
list_add(&page->lru, &n->slabs_partial);
}
内存释放函数kfree, 释放规则:kfree会优先将内存片段释放到kmem_cache的ac缓存中,如果ac缓存满了,则需要将一定数量的内存片段释放到mem node的链表上,如果mem node的链表也超过了限制,就将page释放到伙伴子系统当中。为什么要设置限制?就是防止slab占用过多的内存,而导致内核系统中其他区间可用内存不足。函数调用栈如下,
点击(此处)折叠或打开
kfree
virt_to_cache // 通过虚拟地址获取对应的kmem_cache结构
__cache_free
kasan_slab_free // 释放kasan
___cache_free // 释放内存片段到kmem_cache
___cache_free实现如下,
点击(此处)折叠或打开
void ___cache_free(struct kmem_cache *cachep, void *objp,
unsigned long caller)
{
struct array_cache *ac = cpu_cache_get(cachep); // 获取到ac
check_irq_off();
kmemleak_free_recursive(objp, cachep->flags);
objp = cache_free_debugcheck(cachep, objp, caller);
if (nr_online_nodes > 1 && cache_free_alien(cachep, objp))
return;
if (ac->avail < ac->limit) { //如果ac中缓存的数量没有超过限制,就释放到ac中缓存起来
STATS_INC_FREEHIT(cachep);
} else { // ac中的缓存超过限制,则释放到node对应的链表上
STATS_INC_FREEMISS(cachep);
cache_flusharray(cachep, ac);
}
if (sk_memalloc_socks()) {
struct page *page = virt_to_head_page(objp);
if (unlikely(PageSlabPfmemalloc(page))) {
cache_free_pfmemalloc(cachep, page, objp);
return;
}
}
ac->entry[ac->avail++] = objp;
}
cache_flusharray释放一个batchcount的内存片段到node的链表上,实现如下
点击(此处)折叠或打开
static void cache_flusharray(struct kmem_cache *cachep, struct array_cache *ac)
{
int batchcount;
struct kmem_cache_node *n;
int node = numa_mem_id();
LIST_HEAD(list);
batchcount = ac->batchcount;
check_irq_off();
n = get_node(cachep, node);
spin_lock(&n->list_lock);
if (n->shared) { // 存在共享,则优先释放到node的共享结点上
struct array_cache *shared_array = n->shared;
int max = shared_array->limit - shared_array->avail;
if (max) {
if (batchcount > max)
batchcount = max;
memcpy(&(shared_array->entry[shared_array->avail]),
ac->entry, sizeof(void *) * batchcount);
shared_array->avail += batchcount;
goto free_done;
}
}
free_block(cachep, ac->entry, batchcount, node, &list); // 释放ac到node的链表上,如果存在node中也超出了限制,则把free链表上的page挂到list指向的链表上
free_done:
spin_unlock(&n->list_lock);
slabs_destroy(cachep, &list); //将list上面挂的page释放到伙伴子系统
ac->avail -= batchcount; // 调整ac缓存
memmove(ac->entry, &(ac->entry[batchcount]), sizeof(void *)*ac->avail);
}
free_block实现,
点击(此处)折叠或打开
static void free_block(struct kmem_cache *cachep, void **objpp,
int nr_objects, int node, struct list_head *list)
{
int i;
struct kmem_cache_node *n = get_node(cachep, node);
struct page *page;
n->free_objects += nr_objects;
// 依次释放ac中的每个内存片段
for (i = 0; i < nr_objects; i++) {
void *objp;
struct page *page;
objp = objpp[i];
page = virt_to_head_page(objp);
list_del(&page->lru);
check_spinlock_acquired_node(cachep, node);
slab_put_obj(cachep, page, objp);
STATS_DEC_ACTIVE(cachep);
if (page->active == 0) { // 如果active为0表示,该page的所有片段都已经被释放,空闲
list_add(&page->lru, &n->slabs_free);
n->free_slabs++;
} else {
list_add_tail(&page->lru, &n->slabs_partial);
}
}
// 这里如果free_objects数量超出了free_limit限制,则释放slabs_free上面的page
while (n->free_objects > n->free_limit && !list_empty(&n->slabs_free)) {
n->free_objects -= cachep->num;
page = list_last_entry(&n->slabs_free, struct page, lru);
list_move(&page->lru, list);
n->free_slabs--;
n->total_slabs--;
}
}
什么是kasan? kasan是Linux内核引入的一种专门检测slab内存越界问题的,通过给内存打上特定的pad来实现,这个操作比较消耗内存。