数据结构
主要包含两个缓存对象(CPU的kmem_cache_cpu
和内存节点numa的kmem_cache_node
),还有一个描述本身slab数据的kmem_cache
。
CPU缓存
struct kmem_cache_cpu {
void **freelist; // 指向第一个空闲对象。
struct page *page; // 从哪个页面分配
int node; /* numa id */
unsigned int offset; /* 对象中指向下一个对象的偏移量 */
unsigned int objsize; /* 对象大小 */
...
};
- freelist 是一个空闲链表,freelist指向第一个对象,对象中包含下一个节点;
- offset 对象内存中指向下一个对象的偏移量,object = this->freelist; next = object[offset];
numa节点缓存
struct kmem_cache_node {
spinlock_t list_lock; /* Protect partial list and nr_partial */
unsigned long nr_partial; // 有多少个部分空闲链表
struct list_head partial; // partial是struct page链表
...
};
kmem_cache
kmem_cache保存着一个slub分配器相关的数据,包括一个对象的大小,名称,构造函数,每个CPU上的缓存等。
/*
* Slab cache management.
*/
struct kmem_cache {
/* Used for retriving partial slabs etc */
unsigned long flags;
int size; /* 包含原数据的对象大小 */
int objsize; /* 不包含原数据的对象大小,就是最原始的申请的对象的大小 */
int offset; /* 一个对象中下一个空闲对象的存放的位置 */
struct kmem_cache_order_objects oo; // oo中包含一个数字:unsigned long x,这个数字包含两个内容,一个是order,一个是一个slab中能容纳多少个对象。order数据表示一次分配页内存时,分配页的个数(1<<order)。
/*
* Avoid an extra cache line for UP, SMP and for the node local to
* struct kmem_cache.
* 为了避免再重新刷新到cache中保存的缓存
*/
struct kmem_cache_node local_node;
/* Allocation and freeing of slabs */
struct kmem_cache_order_objects max;
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 *);
int inuse; /* Offset to metadata */
int align; /* 对齐 */
unsigned long min_partial;
const char *name; /* 展示用的名称 */
struct list_head list; /* slab cache列表 */
#ifdef CONFIG_NUMA
/*
* 从远端内存节点上分配的时候,碎片整理
* 在创建cache的时候,函数kmem_cache_open会设置为1000
#ifdef CONFIG_NUMA
s->remote_node_defrag_ratio = 1000;
#endif
*/
int remote_node_defrag_ratio;
struct kmem_cache_node *node[MAX_NUMNODES];
#endif
#ifdef CONFIG_SMP
struct kmem_cache_cpu *cpu_slab[NR_CPUS]; // 每个CPU对应一个cpu slab缓存
#else
struct kmem_cache_cpu cpu_slab;
#endif
};
实现
分配内存
kmalloc/__kmalloc
void *kmalloc(size_t size, gfp_t flags)
如果size > SLUB_MAX_SIZE(2 * PAGE_SIZE),就使用kmalloc_large
分配内存,否则使用slab分配slab_alloc
。kmalloc_large
使用页分配,与slub无关,不记录。
slab_alloc
如果kmem_cache_cpu.freelist不为空切numa node id与要分配的节点匹配,那么直接成功返回,否则从__slab_alloc
分配。
NOTE: kmem_cache_cpu上有一个freelist,kmem_cache_cpu.page上也有一个freelist。从
__slab_alloc
中可以看到,会把page.freelist迁移到kmem_cache_cpu上。
__slab_alloc
加slab锁
struct kmem_cache_cpu *c; slab_lock(c->page);
判断node id是否匹配,不匹配会deactive当前
kmem_cache_cpu
,然后重新分配新的slab
。load_freelist
。从kmem_cache_cpu.page.freelist
中获取对象。c->freelist = object[c->offset]; c->page->inuse = c->page->objects; c->page->freelist = NULL; c->node = page_to_nid(c->page);
another_slab
。freelist中没有,就deactive当前kmem_cache_cpu
,然后分配新的slab。new_slab
。先尝试从kmem_cache_node
中获取partial slab的page,重新设置kmem_cache_cpu.page=new page
。获取不到尝试分配新的slab,然后设置kmem_cache_cpu = get_cpu_slab(s, smp_processor_id())
,然后重新加锁新的页面。如果还不成功,就会报错,slab_out_of_memory
。上面两次尝试如果任意一次成功,就会回到第二步load_freelist
。- 如果
new_slab
也分配失败,就会slab_out_of_memory
,内存分配失败。 - 上面的步骤任一步导致结束的时候,都会解slab锁。
上面步骤简单汇总一下大致就是:
slab_lock page
kmem_cache_cpu.page.freelist
new_page from kmem_cache_node(get_partial)
new_slab from kmem_cache(new_slab@mm/slub.c)
slab_out_of_memory
slab_unlock page
步骤中提到的deactive当前kmem_cache_cpu:
1. 将freelist从kmem_cache_cpu.freelist释放到kmem_cache_cpu.page.freelist,然后将kmem_cache_cpu.page设置为NULL,等于取消关联。代码中注释说这种情况很少会出现,因为走到这里时,kmem_cache_cpu.freelist和kmem_cache_cpu.page.freelist通常都是空的。
2. unfreeze_slab。将一页还到list中去,这个函数要求调用的时候对应的页加上了slab锁,返回之前会解锁。 // TODO
get_partial
获取一个partial页,然后解锁返回。
static struct page *get_partial(struct kmem_cache *s, gfp_t flags, int node)
{
struct page *page;
int searchnode = (node == -1) ? numa_node_id() : node; // 按照numa id来查找
/*
get_partial_node会遍历kmem_cache_node上的partial list,然后
尝试lock_and_freeze_slab,成功则返回这一页
lock_and_freeze_slab会slab_trylock(page),成功的话再freeze page:
__SetPageSlubFrozen(page)
*/
page = get_partial_node(get_node(s, searchnode));
if (page || (flags & __GFP_THISNODE))
return page;
/*
遍历node_zonelist, 找到一个有partial的page
*/
return get_any_partial(s, flags);
}
什么时候是无法分配对象
__kmalloc
slab_alloc // 忽略大内存情况(大块内存>2*PAGE_SIZE)
__slab_alloc
new_slab // 当前page缓存上没有,node上没有partial page
allocate_slab
先尝试分配2^s->oo个页面,分配失败的话再尝试2^s->min个页面
alloc_slab_page
alloc_pages_node // 分配物理页
// 这个函数在内存紧张的时候会唤醒kswapd回收内存
slab_out_of_memory // 最后可能会用这个函数打印日志
释放内存
释放内存从最直观的理解上,应该与申请内存类似的顺序。比如,先尝试释放到kmem_cache_cpu,不行就释放到page上。如果page上都空了,是否考虑释放到node上,没有空,就放到partial page列表中。
kfree
根据释放的对象所在的页,判断当前页是否有slab标识,如果没有直接使用put_page
释放(根据一个地址可以直接找到对应的page)。否则使用slab_free释放。
slab_free
如果要释放的内存所在的page与当前kmem_cache_cpu中的page是同一个,那么可以直接释放到kmem_cache_cpu中(还有个前提:kmem_cache_cpu.node >= 0)。否则调用__slab_free
释放,里面会调用__free_page释放页到伙伴系统。
__slab_free
- 把对象放到page.freelist上。
- 如果page还有人使用(PageSlubFrozen标识为true),那么直接解锁返回;
- 如果page上还有对象没有释放(page.inuse != 0),说明是partial page,使用
add_partial
加到kmem_cache_node
的partial list中,然后解锁返回; - 走到这里,说明page.inuse==0,page上的节点全都释放了,也没有kmem_cache_cpu使用。那么根据一开始page.freelist信息,判断page是不是partial,如果是,将
kmem_cache_node.nr_partial
减1。最后就是discard_slab
,尝试释放掉这一页。如果slab的释放策略是SLAB_DESTROY_BY_RCU,那么就把page放到rcu队列中,否则就调用__free_slab
释放page。
初始化
kmem_cache_init
kmem_cache有一个全局数组:
struct kmem_cache kmalloc_caches[SLUB_PAGE_SHIFT];
SLUB_PAGE_SHIFT
在4K页面的机器上是14。
初始化的时候还没有任何kmem_cache创建出来,第一个就是为kmem_cache_node创建的,大小就是sizeof(kmem_cache_node)。
然后按照系统的一些信息,在64位系统上大致创建出来的有大小分别是8,16,32,64,…,16384(2^14)的cache。另外还会额外创建大小为96和192的cache,估计是这两个大小的对象会经常使用,如果没有这两个,会使用稍大一点的128和256,会浪费不少内存。
创建kmem_cache的函数是create_kmalloc_cache
。
create_kmalloc_cache
这个函数会调用kmem_cache_open初始化一些kmem_cache的基本参数,包括对象大小,min_partial参数,然后初始化kmem_cache_node和kmem_cache_cpu。在CONFIG_NUMA
生效的时候设置s->remote_node_defrag_ratio = 1000;
最后将初始化完成的kmem_cache
加入到slab_caches
链表中。
kmem_cache_open
该函数负责初始化kmem_cache的一些基本参数。
static int kmem_cache_open(struct kmem_cache *s, gfp_t gfpflags,
const char *name, size_t size,
size_t align, unsigned long flags,
void (*ctor)(void *))
- name是cache的名称;
- size就是原始对象大小;
- align是对齐方式;
- ctor是对象的构造函数;
- flag是分配标识, 在初始化的时候是
GFP_NOWAIT
。
其中对齐方式是固定的:
#define ARCH_KMALLOC_MINALIGN __alignof__(unsigned long long)
kmem_cache中对给定的对象大小做一个运算,选择一个合适的大小作为分配时的对象大小。计算的函数是calculate_sizes
。这里面会对分配对象大小、align和inuse、oo做计算,理解这里的计算对里面kmem_cache中的成员变量很有帮助。
kmem_cache.min_partial是log2(size),并且在[5,10]范围内。
接下来分别初始化kmem_cache_node(init_kmem_cache_nodes)和kmem_cache_cpu(alloc_kmem_cache_cpus)。
init_kmem_cache_nodes
为每个内存节点初始化一个kmem_cache_node,然后创建一个slab给它。因为刚开始初始化时slab还不能用,所以slub使用变量slab_state
表示初始化时slub的状态。而kmem_cache_node
也是一个slub缓存,所以在创建第一个slab缓存kmem_cache_node
时,slab_state
是DOWN,之后就改成PARTIAL,使用early_kmem_cache_node_alloc
分配内存。之后的node就使用kmem_cache_alloc_node
创建,即正常的使用slub创建对象。
alloc_kmem_cache_cpus
为kmem_cache的每个CPU分配一个对应的kmem_cache_cpu对象。
/*
* calculate_sizes计算order和slab对象数据的分布
*/
static int calculate_sizes(struct kmem_cache *s, int forced_order)
{
unsigned long flags = s->flags;
unsigned long size = s->objsize;
unsigned long align = s->align;
int order;
/*
* 用指针大小对齐。空闲指针直接放在了对象内存中,对象内存只能放
* 在指针对齐的内存上。
*/
size = ALIGN(size, sizeof(void *));
/*
* 现在就已经知道了一个对象使用的真正大小就是inuse。这也可能是空闲
* 指针的偏移量,就是kmem_cache.offset
*/
s->inuse = size;
// 在初始化的时候,flags给的是GFP_NOWAIT,s->ctor构造函数也是NULL
// SLAB_DESTROY_BY_RCU: 释放slab到RCU队列上
// SLAB_POISON:比较神奇的名字,毒害的slab对象,这是调试用的
if (((flags & (SLAB_DESTROY_BY_RCU | SLAB_POISON)) ||
s->ctor)) {
/*
* 重新定位空闲指针,放到对象的后面去,以免覆盖对象内存。
*/
s->offset = size;
size += sizeof(void *);
}
/*
* 重新计算对齐,可以选择与cache line对齐,最少按照sizeof(long long)对齐
*/
align = calculate_alignment(flags, align, s->objsize);
s->align = align;
size = ALIGN(size, align);
s->size = size;
if (forced_order >= 0) // 初始化时传的是-1
order = forced_order;
else
order = calculate_order(size); // 没有指定就计算一个
if (order < 0)
return 0;
s->allocflags = 0;
if (order)
s->allocflags |= __GFP_COMP;
if (s->flags & SLAB_CACHE_DMA)
s->allocflags |= SLUB_DMA;
if (s->flags & SLAB_RECLAIM_ACCOUNT)
s->allocflags |= __GFP_RECLAIMABLE;
/*
* 计算一个slab中可以容纳多少对象
* oo中会保存这个slab的order和一个slab中容纳的对象个数
*/
s->oo = oo_make(order, size);
s->min = oo_make(get_order(size), size);
if (oo_objects(s->oo) > oo_objects(s->max))
s->max = s->oo;
return !!oo_objects(s->oo);
}
其实里面有一个计算order的函数还是比较牛的,它的主要目的是为了避免分配内存过程中造成的一些碎片浪费。代码中的注释说order 0是首选的,因为在页分配器中不会造成碎片。但是大于一页的对象就不能用0,因为会有很多空间空下来。如果有1/16的slab会浪费的话,就会选择一个大点的order。详细可以看slab_order
的代码注释。
收缩
收缩是为了防止slub占用过多的内存,或者有很多空的slub node,应该释放掉。
slub提供了接口kmem_cache_shrink
用来收缩某个缓存。这个接口的功能很简单,先把所有CPU缓存上的slab放到node上(flush_slab),然后遍历缓存的所有kmem_cache_node,每个node上释放空的slab,然后把partial slab从大到小排序重新放到node的slab链表上。
NOTE: 什么时候收缩内存?
内存不足,比如分配物理页失败的时候,会调用kswapd。