一、概述
linux内存管理核心是伙伴系统,slab,slub,slob是基于伙伴系统之上提供api,用于内核内存分配释放管理,适用于小内存(小于1页)分配与释放,当然大于1页,也是可以的.小于一页的,我们也可以直接用伙伴系统api申请内存和释放,但伙伴系统最小单位是页,如果我们只需要100byte,伙伴系统申请内存最小1页(一般情况1页是4k, 具体看系统PAGE_SHIFT定义),显然就很浪费.
二、slub核心原理
slub是从伙伴系统中分配连续一块内存页作为缓存池,再将其分成大小相等,多块小内存块,当申请内存时,返回其中一小块(返回小块内存起始地址.如下图),假设分成9小块内存(简单的数值表示地址):
每块小内存的首地址,存放下一小块内存起始地址,最后一小块内存首地址存放null.
缓存池(如上图,9个地址连续小内存块,可视为缓存池),从开始分配到小块内存分配完的过程:
假设指针p=0,表示指向第0小块内存.首次分配内存时,首先addr=p, p=*p(从第0小块内存首地址中获取下一小块内存起始地址,并写入p指针中)之后p=1指向第1小块内存起始地址,addr(指向第0小块内存起始地址)返回给申请分配内存的程序;第二次分配内存时,首先addr=p,p=*p,此后p=2,以此类推,直到最后,将第8小块内存地址分配完后,若再次发起内存申请时,发现p=null,表示内存分配完了,此时,会再次向伙伴系统申请分配连续的,一块大内存(缓存池).
一块缓存池,释放过程:
释放内存时,假设有个指针pa, 首次释放的内存,是第5小块内存,pa=5,*pa=null(第5小块内存首地址存放null), 然后第8小块内存被释放,这时候pa=8,*pa=5(第8小块内存首地址存放5),第5小块内存首地址仍然存放null,, 以此类推.
假如释放顺序是5,8,2,0,6,4,3,1,7释放完后,pa=7,内存情况如下:
释放完或者只释放部分,随时都可以加入到分配中去.
slub就是负责:缓存池之间的切换,以及小内存块分配与释放.
那么,小内存块(如上面9小块)大小,块数是怎么计算,缓存池大小又是怎么计算?
小内存块大小计算:
假如,申请x_size大小的内存,那么:
n = fls(x_size - 1);
j = 1 << n;
or = j < x_size ? 1 << n+1 : j;
or就是小块内存大小
fls()返回最高位为1的位置,起始是从1开始,返回0表示没有找到为1的位置
小块内存大小,也可以直接调用include/linux/slab.h里面定义的kmalloc_index()得到:
a = kmalloc_index(x_size);
or = a < 0 ? 0 : 1 << a;
由此,可看出,最终分配的小内存块大小,不一定是申请时指定的大小,而是2的n次方,不足往大的计算,如申请5字节,最终计算下来,会分配8字节返回.在申请内存时,大小按照2的n次方申请最佳,否则会出现浪费.如我们申请5字节,分配8字节,多余的3字节,申请者,不知道,就不会去使用这3字节,出现浪费.
缓存池大小计算:
int rem = 0;
int order = 0;
int max_order;
unsigned long slab_size;
//2的order次幂,就是要向伙伴系统申请的,连续大块内存,单位页
for(order = 0; order < max_order; order++)//max_order最大限制
{
slab_size = PAGE_SIZE << order;//将order,转化为字节(byte)大小
//reserved,一般为0
//x_size小块内存大小
rem = (slab_size - reserved) % x_size;//碎片,残留rem字节大小内存,是无法分配的
if (rem <= slab_size / fract_leftover)//fract_leftover会分别用16,8,4试探,看是否满足条件
break;//表示成功
}
printk("order:%d\n",order)
上面的代码,是个人理解后的简单描述,实现在mm/slub.c中calculate_order()函数.
2^order(2的order次幂)个页,就是缓存池大小.
简而言之,以2^order个页,循环试探,找到满足,对碎片要求的值order.
缓存池中小内存块,块数计算:
slab_size = PAGE_SIZE << order;
num = slab_size / or;
num就是小内存块,块数.
slub最大只能申请分配2页内存空间(小内存块),在include/linux/slab.h中定义:
#define KMALLOC_SHIFT_HIGH (PAGE_SHIFT + 1)
其中的PAGE_SHIFT,需要看具体系统配置,一般会定义在arch/xxx/include/asm/page.h里面:
#define PAGE_SHIFT 12
slub最小能分配内存空间(小内存块),由宏KMALLOC_SHIFT_LOW定义,在include/linux/slab.h,KMALLOC_SHIFT_LOW会受平台定义的ARCH_DMA_MINALIGN影响,如果平台没定义ARCH_DMA_MINALIGN,slub默认:
#define KMALLOC_SHIFT_LOW 3
即,最小可以申请8个字节(1 << 3),假如申请3个字节,也会分配8字节.
三、slub code实现
为了便于描述,如下定义:
同类缓存池:struct kmem_cache *s;
小缓存池:struct page *page;
当前小缓存池:s->cpu_slab->page;
当前小内存块链:s->cpu_slab->freelist
一级(小缓存池)回收链表:s->cpu_slab->partial
二级(小缓存池)回收链表:s->node[x]->partial
小内存块:"slub核心原理"描述的,分成多个内存块,其中的一块,就叫做小内存块,即每次向slub申请分配,获取到的内存,就是一块小内存块,小内存块大小由struct kmem_cache成员size指定.
同类缓存池:一个struct kmem_cache实例化对象,管理着同一类缓存池(一个或多个缓存池),这里说的同一类,指相同大小小内存块,即可以叫做相同size小内存块,struct kmem_cache成员size(object_size与size相等)就是小内存块的大小.例如:size=1024的小内存块,与size=2048的小内存块,就属于不同类.(个人觉得,struct kmem_cache成员size,命名为size_type,或许更合适)
小缓存池:slub每次向伙伴系统申请分配内存页(单个或多个连续页),就可以叫做小缓存池.向伙伴系统分配的页大小由s->oo决定.如上"slub核心原理"提到的0到8的九个小块内存,组成一个小缓存池.
大缓存池:为方便描述和理解,同类缓存池取个别名,叫做大缓存池,里面有多个小缓存池.
1、同类缓存池初始化
同类缓存池结构初始化后,形成的大致结构图:
图一
slab_caches是链表头,为了便于描述,我们取名为同类缓存池链表,或者大缓存池链表,定义在mm/slab_common.c中:
LIST_HEAD(slab_caches);
kmem_cache是指向struct kmem_cache的一个实体对象,定义在mm/slab_common.c中:
struct kmem_cache *kmem_cache;
kmem_cache_node是指向struct kmem_cache_node的一个实体对象,定义在mm/slub.c中;
static struct kmem_cache *kmem_cache_node;
kmem_cache,kmem_cache_node,kmalloc_caches[0], kmalloc_caches[1], …kmalloc_caches[i]都是struct kmem_cache实体,以slab_caches为列表头,通过链表(struct list_head)形式连接起来.
s0,s1,s2…sn分别代表不同size的,同类缓存池管理结构(即struct kmem_cache一个实体对象)
memory0, memory1, memory2…memoryN代表缓存池,即实际内存空间,图一方框大小并不代表缓存池空间大小
kmem_cache, s0代表同一个struc kmem_cache实体,即同类缓存池管理结构,kmem_cache和s0不分彼此,图一只是为了更好的描述.有意思的地方,mermory0用来存放小内存块s0, s1, s2…sn;而s0用来管理memory0缓存池里面其他小内存块分配释放.
依次,有如下关系:
kmem_cache_node对应s1,管理memory1
kmalloc_caches[0]对应s2,管理memory2
kmalloc_caches[1]对应s3,管理memory3
…
如下:
kmalloc_caches[6]管理所有大小为64byte的内存块(本文称为小内存块)分配释放.
kmalloc_caches[7]管理所有大小为128byte的内存块分配释放.
kmalloc_caches[8]管理所有大小为256byte的内存块分配释放.
…
在分析代码是如何描述图一结构前,先了解下同类缓存池struct kmem_cache结构体(定义在include/linux/slub_def.h里面):
struct kmem_cache {
//cpu_slab管理当前小缓存池和一级回收链表
struct kmem_cache_cpu __percpu *cpu_slab;
/* Used for retriving partial slabs etc */
unsigned long flags;
//二级回收链表上小缓存池数量超过min_partial时,
//将会把二级回收链表超过的部分,且inuse为0时,释放回伙伴系统
//unfreeze_partials()函数做的处理
unsigned long min_partial;
//size同类缓存池关键,小内存块空间大于等于size
int size; /* The size of an object including meta data */
//size == object_size
int object_size; /* The size of an object without meta data */
int offset; /* Free pointer offset. */
//一级回收链表上小内存块数量超过cpu_partial时,
//将会把一级回收链表上所有小缓存池移动到二级回收链表上
//unfreeze_partials()函数会做这样的处理
int cpu_partial; /* Number of per cpu partial objects to keep around */
//oo低16位存放小缓存池小内存块数量,
//oo高16位,存放小缓存池空间大小(高16位是2的幂指数,2^(oo>>16)得到的值,就是小缓存池空间大小)
struct kmem_cache_order_objects oo;
/* Allocation and freeing of slabs */
//max是oo>>16的上限值,slub向伙伴系统申请内存页时,不得超过这个值
struct kmem_cache_order_objects max;
// oo高16位的最小值,当缓存池向伙伴系统申请分配页时,按照oo申请不成功时,
//会降低为min的值,再次向伙伴系统申请页
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 *);
//inuse == size
int inuse; /* Offset to metadata */
int align; /* Alignment */
//reserved一般为0
int reserved; /* Reserved bytes at the end of slabs */
//同类缓存池名字,便于管理和查询,debug
const char *name; /* Name (only for display!) */
//所有同类缓存池都是通过list连接到slab_caches(同类缓存池链表头)
struct list_head list; /* List of slab caches */
#ifdef CONFIG_SYSFS
struct kobject kobj; /* For sysfs */
#endif
#ifdef CONFIG_MEMCG_KMEM
struct memcg_cache_params *memcg_params;
int max_attr_size; /* for propagation, maximum size of a stored attr */
#endif
#ifdef CONFIG_NUMA
/*
* Defragmentation by allocating from a remote node.
*/
int remote_node_defrag_ratio;
#endif
//node管理二级回收链表
//多核CPU,MAX_NUMNODES代表CPU核数量,每个CPU核都有自己独立的一个node
struct kmem_cache_node *node[MAX_NUMNODES];
};
struct kmem_cache_cpu {
//freelist指向当前小内存块链头
void **freelist; /* Pointer to next available object */
unsigned long tid; /* Globally unique transaction id */
//page指向当前小缓存池
struct page *page; /* The slab from which we are allocating */
//partial指向一级回收链表
struct page *partial; /* Partially allocated frozen slabs */
#ifdef CONFIG_SLUB_STATS
unsigned stat[NR_SLUB_STAT_ITEMS];
#endif
};
//min的作用(在mm/slub.c中函数allocate_slab())如下:
static struct page *allocate_slab(struct kmem_cache *s, gfp_t flags, int node)
{
...
...
...
page = alloc_slab_page(alloc_gfp, node, oo);
if (unlikely(!page)) {//按照oo申请失败,降低为min再次试图向伙伴系统申请
oo = s->min;
/*
* Allocation may have failed due to fragmentation.
* Try a lower order alloc if possible
*/
page = alloc_slab_page(flags, node, oo);
if (page)
stat(s, ORDER_FALLBACK);
}
...
...
...
}
slub初始化
函数调用情况:
start_kernel() --> mm_init() --> kmem_cache_init()
start_kernel(), mm_init()都定义在init/main.c中
kmem_cache_init()定义在mm/slub.c中,函数如下:
void __init kmem_cache_init(void)
{
//boot_kmem_cache为启动图一里面kmem_cache(s0)铺路或者引路
//boot_kmem_cache_node为启动kmem_cache_node(s1)铺路或引路
static __initdata struct kmem_cache boot_kmem_cache,
boot_kmem_cache_node;
if (debug_guardpage_minorder())
slub_max_order = 0;
kmem_cache_node = &boot_kmem_cache_node;
kmem_cache = &boot_kmem_cache;
//初始化boot_kmem_cache_node
//第二个参数,作为缓存池管理结构名字boot_kmem_cache_node.name
//第三个参数,作为缓存池管理结构boot_kmem_cache_node大小,即小内存块大小
//这个函数,还会计算小内存块数量
create_boot_cache(kmem_cache_node, "kmem_cache_node",
sizeof(struct kmem_cache_node), SLAB_HWCACHE_ALIGN);
register_hotmemory_notifier(&slab_memory_callback_nb);
/* Able to allocate the per node structures */
slab_state = PARTIAL;
//初始化缓存池管理结构boot_kmem_cache
//第二个参数,作为缓存池管理结构名字boot_kmem_cache.name
//第三个参数,作为缓存池管理结构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);
//将从memory0中分配s0,然后将boot_kmem_cache内容拷贝到s0
//当bootstrap()返回后,kmem_cache将不再指向boot_kmem_cache,
//而是指向从伙伴系统分配来的内存空间s0,
//之后kmem_cache(s0)管理的缓存池memory0,将用存放所有同类缓存池管理结构实体的,
//(需要注意,同类缓存池管理结构struct kmem_cache实体,本身就是需要空间存放,
//所有管理结构实体都存放在memory0中,而s0用来管理memory0分配释放)
kmem_cache = bootstrap(&boot_kmem_cache);
/*
* Allocate kmem_cache_node properly from the kmem_cache slab.
* kmem_cache_node is separately allocated so no need to
* update any list pointers.
*/
//从memory0中分配出s1小内存块,然后把boot_kmem_cache_node拷贝到s1里面
//bootstrap()返回后,kmem_cache_node指向s1
kmem_cache_node = bootstrap(&boot_kmem_cache_node);
/* Now we can use the kmem_cache to allocate kmalloc slabs */
//主要初始化kmalloc_caches[n]
//后期分配内存,通过申请size,调用函数kmalloc_index()或者kmalloc_slab()
//计算查找出n,并得出kmalloc_caches[x]同类缓存池管理结构实体
//其中n,表示将分配出大小为1<<n的内存块空间(小内存块),即2的n次方
create_kmalloc_caches(0);
#ifdef CONFIG_SMP
register_cpu_notifier(&slab_notifier);
#endif
printk(KERN_INFO
"SLUB: HWalign=%d, Order=%d-%d, MinObjects=%d,"
" CPUs=%d, Nodes=%d\n",
cache_line_size(),
slub_min_order, slub_max_order, slub_min_objects,
nr_cpu_ids, nr_node_ids);
}
void __init create_boot_cache(struct kmem_cache *s, const char *name, size_t size,
unsigned long flags)
{
int err;
s->name = name;//同类缓存池管理结构名字
s->size = s->object_size = size;//小内存块空间大小,这很关键哦
//内存对齐
s->align = calculate_alignment(flags, ARCH_KMALLOC_MINALIGN, size);
//同类缓存池管理结构主要初始化
err = __kmem_cache_create(s, flags);
if (err)
panic("Creation of kmalloc slab %s size=%zu failed. Reason %d\n",
name, size, err);
s->refcount = -1; /* Exempt from merging for now */
}
int __kmem_cache_create(struct kmem_cache *s, unsigned long flags)
{
int err;
err = kmem_cache_open(s, flags);
if (err)
return err;
...
}
static int kmem_cache_open(struct kmem_cache *s, unsigned long flags)
{
s->flags = kmem_cache_flags(s->size, flags, s->name, s->ctor);
s->reserved = 0;
if (need_reserve_slab_rcu && (s->flags & SLAB_DESTROY_BY_RCU))
s->reserved = sizeof(struct rcu_head);
//小缓存池里面小内存块数量计算,计算方法,可参考以上"slub核心原理"
if (!calculate_sizes(s, -1))
goto error;
...
//二级回收链表上,小缓存池数量min_partial上限初始化
set_min_partial(s, ilog2(s->size) / 2);
...
//一级回收链表上小内存块数量上限cpu_partial初始化
//超过cpu_partial,将会把一级回收链表上所有小缓存池移动到二级回收链表上
if (kmem_cache_debug(s))
s->cpu_partial = 0;
else if (s->size >= PAGE_SIZE)
s->cpu_partial = 2;
else if (s->size >= 1024)
s->cpu_partial = 6;
else if (s->size >= 256)
s->cpu_partial = 13;
else
s->cpu_partial = 30;
...
//二级回链表初始化
if (!init_kmem_cache_nodes(s))
goto error;
//当前缓存池,一级回收链表初始化
if (alloc_kmem_cache_cpus(s))
return 0;
//当前缓存池,一级回收链表,二级回收链表初始化失败时,将会释放掉二级回收链表
free_kmem_cache_nodes(s);
error:
if (flags & SLAB_PANIC)
panic("Cannot create slab %s size=%lu realsize=%u "
"order=%u offset=%u flags=%lx\n",
s->name, (unsigned long)s->size, s->size, oo_order(s->oo),
s->offset, flags);
return -EINVAL;
}
static int calculate_sizes(struct kmem_cache *s, int forced_order)
{
unsigned long flags = s->flags;
unsigned long size = s->object_size;
int orde
...
size = ALIGN(size, sizeof(void *));
...
s->inuse = size;
...
size = ALIGN(size, s->align);
s->size = size;
if (forced_order >= 0)
order = forced_order;
else
//calculate_order()计算方式,见"slub核心原理"描述
order = calculate_order(size, s->reserved);
if (order < 0)
return 0;
...
//oo高16位存放order,其中order是向伙伴系统申请内存空间的大小,2的order次方页
//oo低16位存放,小内存块数量,即小缓存池里面有多少个小内存块(2的order次方除以size)
s->oo = oo_make(order, size, s->reserved);
s->min = oo_make(get_order(size), size, s->reserved);
if (oo_objects(s->oo) > oo_objects(s->max))
s->max = s->oo;
return !!oo_objects(s->oo);
}
static int init_kmem_cache_nodes(struct kmem_cache *s)
{
int node;
//node cpu核编号(多核cpu)
for_each_node_state(node, N_NORMAL_MEMORY) {
struct kmem_cache_node *n;
...
//从memory1中分配一个二级回收链表struct kmem_cache_node实体内存空间
n = kmem_cache_alloc_node(kmem_cache_node,
GFP_KERNEL, node);
if (!n) {//分配失败,释放二级回收链表
free_kmem_cache_nodes(s);
return 0;
}
s->node[node] = n;//二级回收链表指针,指向实体struct kmem_cache_node
init_kmem_cache_node(n);//初始化二级回收链表
}
return 1;
}
static inline int alloc_kmem_cache_cpus(struct kmem_cache *s)
{
...
//为cpu_slab分配内存块空间
s->cpu_slab = __alloc_percpu(sizeof(struct kmem_cache_cpu),
2 * sizeof(void *));
if (!s->cpu_slab)
return 0;
//初始tid
init_kmem_cache_cpus(s);
return 1;
}
static struct kmem_cache * __init bootstrap(struct kmem_cache *static_cache)
{
int node;
//从memory0中分配struct kmem_cache实体(这主要指s0或s1)
struct kmem_cache *s = kmem_cache_zalloc(kmem_cache, GFP_NOWAIT);
memcpy(s, static_cache, kmem_cache->object_size);
...
for_each_node_state(node, N_NORMAL_MEMORY) {
struct kmem_cache_node *n = get_node(s, node);
struct page *p;
if (n) {
list_for_each_entry(p, &n->partial, lru)
p->slab_cache = s;//二级回收链表上小缓存池归属于那类大缓存池
...
}
...
}
list_add(&s->list, &slab_caches);//加入同类缓存池链表
return s;
}
2、slub分配和释放分析
首先看下同类缓存池,小缓存池,小内存块组成的结构,或者说模型,如下图:
图二
代码是如何描述图中的结构,看下面慢慢分析:
每次分配内存时,都是通过申请size查找到对应的同类缓存池,再从同类缓存池中找到小缓存池,最后从小缓存池中找到空闲小内存块,并将小内存块起始地址返回给申请者.那么究竟是从s->cpu_slab->page, s->cpu_slab->partial, s->node[x]->partial三个中那个获取到小内存块,且看下面分析:
s->cpu_slab:
当前小内存块链头指针freelist
当前小缓存池指针page
一级回收链表partial
s->cpu_slab->freelist :
如图二,我们把freelist指向的,多个相互连接的小内存块,叫做小内存块链.
s->cpu_slab->freelist始终指向当前小缓存池里面小内存块链上第一个可用小内存块,即小缓存池首个小内存块.如图,小缓存池里面小内存块是按照"slub核心原理"描述方式串联在一起的,关于分配和释放,在"slub核心原理"节也有描述.
s->cpu_slab->page :
指向当前小缓存池首页.
在当前小缓存池上分配内存,主要函数slab_alloc_node(),定义在mm/slub.c中:
函数调用过程:
kmalloc() --> slab_alloc() --> slab_alloc_node()
static __always_inline void *slab_alloc_node(struct kmem_cache *s,
gfp_t gfpflags, int node, unsigned long addr)
{
void **object;
struct kmem_cache_cpu *c;
struct page *page;
unsigned long tid;
...
c = __this_cpu_ptr(s->cpu_slab);
...
object = c->freelist;//小缓存池上首个可用小内存块
page = c->page;//当前小缓存池首页
if (unlikely(!object || !node_match(page, node)))//object为NULL时表示当前小缓存池没有可用小内存块,需要从其他地方分配内存
object = __slab_alloc(s, gfpflags, node, addr, c);
else {//当前小缓存池有空闲小内存块,从当前小缓存池中分配获取小内存块
void *next_object = get_freepointer_safe(s, object);//获取当前小缓存池上,下一个可用小内存块地址
...
/*
this_cpu_cmpxchg_double()将做如下操作:
s->cpu_slab->freelist = next_object;
s->cpu_slab->tid = tid;
即s->cpu_slab->freelist指向小缓存池中下一个可用小内存块
*/
if (unlikely(!this_cpu_cmpxchg_double(
s->cpu_slab->freelist, s->cpu_slab->tid,
object, tid,
next_object, next_tid(tid)))) {
...
}
...
}
...
return object;//返回可用小内存块首地址
}
小内存块释放,发生在当前小缓存池上,主要函数slab_free(), 定义在mm/slub.c:
函数调用过程:
kfree() --> slab_free()
void kfree(const void *x)
{
struct page *page;
void *object = (void *)x;//释放内存地址
...
page = virt_to_head_page(x);//通过释放内存虚拟地址,查找到小内存所在小缓存池,即小缓存池首页地址
...
slab_free(page->slab_cache, page, object, _RET_IP_);
}
static __always_inline void slab_free(struct kmem_cache *s,
struct page *page, void *x, unsigned long addr)
{
void **object = (void *)x;//释放内存地址首地址
struct kmem_cache_cpu *c;
unsigned long tid;
...
c = __this_cpu_ptr(s->cpu_slab);
...
if (likely(page == c->page)) {//释放内存,属于当前小缓存池
set_freepointer(s, object, c->freelist);//c->freelist被写入到释放小内存块object空间头部,
//这也是小内存块串联在一起的关键,就如"slub核心原理"描述的
//this_cpu_cmpxchg_double执行主要操作:
//s->cpu_slab->freelist = object;刚释放的小内存块,被放在小内存块链上第一个位置
//s->cpu_slab->tid = next_tid(tid);
if (unlikely(!this_cpu_cmpxchg_double(
s->cpu_slab->freelist, s->cpu_slab->tid,
c->freelist, tid,
object, next_tid(tid)))) {
...
}
...
} else
__slab_free(s, page, x, addr);
}
从以上可以看出:
if (likely(page == c->page)) {//释放内存,属于当前小缓存池
当释放的小内存块,属于当前缓存池c->page时,释放才会发生在当前小缓存池上,即把释放的小内存块,回收到当前缓存池.
内存分配和释放,发生在当前缓存池,速度是最快的.
下面,将会分析,不是发生在当前缓存池的情况.
s->cpu_slab->partial :
如图二,多个相互有连接的小缓存池,我们取名为小缓存池链
s->cpu_slab->partial始终指向小缓存池链头
当前缓存池s->cpu_slab->page指向的小缓存池里面小内存块被分配使用完时, 会从一级回收链表s->cpu_slab->partial上取小缓存池, 给到s->cpu_slab->page上,即切换到新的小缓存池,s->cpu_slab->freelist也会指向新小缓存池里面小内存块链上头个小内存块.
函数slab_alloc_node()在当前缓存池耗尽时,会调用__slab_alloc()进一步找寻空闲内存:
static __always_inline void *slab_alloc_node(struct kmem_cache *s,
gfp_t gfpflags, int node, unsigned long addr)
{
void **object;
struct kmem_cache_cpu *c;
struct page *page;
unsigned long tid;
...
c = __this_cpu_ptr(s->cpu_slab);
...
object = c->freelist;
page = c->page;
...
if (unlikely(!object || !node_match(page, node)))//object为NULL时,表示小缓存池里面小内存块耗尽
object = __slab_alloc(s, gfpflags, node, addr, c);
else {//从当前小缓存池分配内存
...
}
...
}
__slab_alloc()首先从一级回收链表s->cpu_slab->partial上取小缓存池,并在小缓存池中找寻空闲小内存块:
static void *__slab_alloc(struct kmem_cache *s, gfp_t gfpflags, int node,
unsigned long addr, struct kmem_cache_cpu *c)
{
void *freelist;
struct page *page;
unsigned long flags;
...
c = this_cpu_ptr(s->cpu_slab);
...
page = c->page;//当前小缓存池
if (!page)
goto new_slab;
redo:
...
freelist = get_freelist(s, page);//第一次执行时,当前小缓存池已经没有了空闲小内存块,返回的freelist为NULL,
//只有通过下面goto redo跳转,再次执行时,
//page指向新的小缓存池,如存在空闲小内存块,返回的freelist才会是非NULL
if (!freelist) {
c->page = NULL;//清空指向当前小缓存池的指针
stat(s, DEACTIVATE_BYPASS);
goto new_slab;//跳转到切换当前小缓存池的地方
}
...
load_freelist://执行到这里,表示已经找到空闲小内存块
...
c->freelist = get_freepointer(s, freelist);//freelist是指向小内存块链上第一块,
//get_freepointer返回后,c->freelist指向小内存块链上第二块
//get_freepointer()函数,就是从小内存块链上取小内存块的过程,并返回下一块
c->tid = next_tid(c->tid);
...
return freelist;//成功获取小内存块,返回
new_slab:
if (c->partial) {//小缓存池链上,如有小缓存池
page = c->page = c->partial;//从c->partial上取新的小缓存池,即切换当前小缓存池
c->partial = page->next;//c->partial指向小缓存池链上,下一个小缓存池
stat(s, CPU_PARTIAL_ALLOC);
c->freelist = NULL;//清空当前小内存块链指针,为切换当前小内存块链做准备
goto redo;//切换当前小缓存池后,跳转准备再次查找空闲小内存块.
}
...
}
函数get_freelist():
static inline void *get_freelist(struct kmem_cache *s, struct page *page)
{
struct page new;
unsigned long counters;
void *freelist;
do {
freelist = page->freelist;//从当前小缓存池上取小内存块链头
//page->freelist始终指向小内存块链头
//小缓存池从伙伴系统分配来,切换为当前小缓存
//池时,page->freelist会被置为NULL
counters = page->counters;
new.counters = counters;
VM_BUG_ON(!new.frozen);
new.inuse = page->objects;
new.frozen = freelist != NULL;//page为当前小缓存池时,freelist为
//NULL,那么new.frozen=0,
//frozen为0表示解冻,可被释放,为1时
//表示冻住,小缓存池不可被释放,
//这里的释放,是指释放回伙伴系统
//page不为当前小缓存池时,如果有空闲
//小内存块,则new.fozen=1,没有空闲
//小内存块时,new.fozen=0
//__cmpxchg_double_slab函数主要作用:
//page->freelist=NULL
//page->counters=new.counters,即修改page->counter值
} while (!__cmpxchg_double_slab(s, page,
freelist, counters,
NULL, new.counters,
"get_freelist"));
return freelist;//返回小内存块链头地址
}
在上面分配过程中,s->cpu_slab->page指向的当前小缓存池,在切换到新小缓存池时,旧的小缓存池,似乎直接被抛弃,那么,slub后面是怎么知道旧小缓存池的存在?请看小内存块释放,是怎么发生在一级回收链表s->cpu_slab->partial上:
函数调用过程:
kfree() --> slab_free() --> __slab_free() --> put_cpu_partial()
void kfree(const void *x)
{
struct page *page;
void *object = (void *)x;//释放小内存块的地址
...
page = virt_to_head_page(x);//通过内存地址,查找到内存所在页的首页地址,
//即,被释放小内存块,所在小缓存池首页地址
...
//同类缓存池page->slab_cache
//被释放内存所在小缓存池首页page
//被释放object小内存块起始地址
slab_free(page->slab_cache, page, object, _RET_IP_);
}
static __always_inline void slab_free(struct kmem_cache *s,
struct page *page, void *x, unsigned long addr)
{
void **object = (void *)x;
struct kmem_cache_cpu *c;
unsigned long tid;
...
c = __this_cpu_ptr(s->cpu_slab)
...
if (likely(page == c->page)) {//当前缓存池
...
}else
__slab_free(s, page, x, addr);//释放内存,不在当前小缓存池中
}
//__slab_free需要关注:
//同一个page小缓存池里面小内存块分配完后,
//是否为首次发生小内存块释放
static void __slab_free(struct kmem_cache *s, struct page *page,
void *x, unsigned long addr)
{
void *prior;
void **object = (void *)x;
int was_frozen;
struct page new;
unsigned long counters;
struct kmem_cache_node *n = NULL;
unsigned long uninitialized_var(flags);
...
do {
...
prior = page->freelist;//page小缓存池,里面小内存块分配完后,
//首次发生小内存块释放时,page->freelist为NULL
//不是首次释放时,表示小缓存池,前面发生过小内存块释放,page->freelist不为NULL
counters = page->counters;
set_freepointer(s, object, prior);//将prior写入到object空间开头地方
new.counters = counters;
was_frozen = new.frozen;//page小缓存池,里面小内存块分配完后,
//首次发生小内存块释放时,new.frozen=0,可看切换当前
//小缓存池时get_freelist()函数的调用
//不是首次释放时,new.frozen=1
new.inuse--;//inuse表示小缓存池里面有多少个小内存块被分配使用中,
//释放时,自然就需要减去1
if ((!new.inuse || !prior) && !was_frozen) {//此条件,在page小缓存池,里面小内存块分配完,
//首次发生小内存块释放时,将会为真
//不是首次释放小内存块时,将为假
//kmem_cache_debug(s)主要用于slub调试时打开CONFIG_SLUB_DEBUG,
//正常情况下CONFIG_SLUB_DEBUG是不被打开的
if (!kmem_cache_debug(s) && !prior)//page上首次发生小内存块释放时,此条件为真,
//不是首次发生小内存块释放,则为假
/*
* Slab was on no list before and will be partially empty
* We can defer the list move and instead freeze it.
*/
new.frozen = 1;
else { /* Needs to be taken off a list */
n = get_node(s, page_to_nid(page));
/*
* Speculatively acquire the list_lock.
* If the cmpxchg does not succeed then we may
* drop the list_lock without any processing.
*
* Otherwise the list_lock will synchronize with
* other processors updating the list of slabs.
*/
spin_lock_irqsave(&n->list_lock, flags);
}
}
} while (!cmpxchg_double_slab(s, page,//cmpxchg_double_slab主要完成:
//page->freelist=object
//page->counters=new.counters
prior, counters,
object, new.counters,
"__slab_free"));
if (likely(!n)) {//此条件,基本为真
/*
* If we just froze the page then put it onto the
* per cpu partial list.
*/
if (new.frozen && !was_frozen) {//小缓存池page在一级回收链表上,
//首次发生小内存块释放时,
//此条件为真,不是首次释放时,为假
put_cpu_partial(s, page, 1);//将小缓存池page放到s->cpu_slab->partial链表上
stat(s, CPU_PARTIAL_FREE);
}
/*
* The list lock was not taken therefore no list
* activity can be necessary.
*/
if (was_frozen)
stat(s, FREE_FROZEN);
return;
}
...
}
//put_cpu_partial将把小缓存池,放到s->cpu_slab->partial
static void put_cpu_partial(struct kmem_cache *s, struct page *page, int drain)
{
struct page *oldpage;
int pages;
int pobjects;
do {
pages = 0;
pobjects = 0;
oldpage = this_cpu_read(s->cpu_slab->partial);//一级回收链表
if (oldpage) {//同类缓存池,首次发生小内存块释放,oldpage为NULL
//之后,发生的小内存块释放,oldpage将为非NULL
pobjects = oldpage->pobjects;//一级回收链表上,小内存块总数
pages = oldpage->pages;//一级回收链表上,小缓存池总数
//一级回收链表上,小内存块总数超过cpu_partial时,
//将会把一级回收链表上的小缓存池,取出放入到二级回收链表上
//注意:__slab_free()调用本函数,drain是为1
if (drain && pobjects > s->cpu_partial) {
unsigned long flags;
/*
* partial array is full. Move the existing
* set to the per node partial list.
*/
local_irq_save(flags);
//将一级回收链表上小缓存池,取出放入到二级
unfreeze_partials(s, this_cpu_ptr(s->cpu_slab));
local_irq_restore(flags);
oldpage = NULL;
pobjects = 0;
pages = 0;
stat(s, CPU_PARTIAL_DRAIN);
}
}
pages++;//统计一级回收链表上,小缓存池数
pobjects += page->objects - page->inuse;//统计一级回收链表上,空闲小内存块数
//page->objects小缓存池上小内存块总数,从伙伴系统分配时确定,之后一直不变
//page->inuse小缓存池已分配使用的小内存块数
page->pages = pages;//将统计的小缓存池数信息,存放到一级回收链表头中
//由此可看出,一级回收链表上小缓存池总数信息,始终存放在链表头中
page->pobjects = pobjects;//将统计的小内存块数,存放到一级回收链表头中
//由此可看出,一级回收链表上小内存块总数信息,始终存放在链表头中
page->next = oldpage;//新加入的小缓存池,放入一级回收链表头
//this_cpu_cmpxchg主要操作:
//s->cpu_slab->partial=page
} while (this_cpu_cmpxchg(s->cpu_slab->partial, oldpage, page) != oldpage);
}
s->node[x]->partial :
二级回收链表
在什么情况下,才会从二级回收链表分配获取小缓存池呢?
在什么情况下,才会把小缓存池放入到二级回收链表呢?
从上面分析中,可以看到分配调用过程,有个函数__slab_alloc(),在这个函数里面,根据一定条件满足,将从二级回收链表分配内存:
static void *__slab_alloc(struct kmem_cache *s, gfp_t gfpflags, int node,
unsigned long addr, struct kmem_cache_cpu *c)
{
void *freelist;
struct page *page;
unsigned long flags;
...
c = this_cpu_ptr(s->cpu_slab);
...
load_freelist://执行到这里,表示已经成功切换到有空闲小内存块的小缓存池
/*
* freelist is pointing to the list of objects to be used.
* page is pointing to the page from which the objects are obtained.
* That page must be frozen for per cpu allocations to work.
*/
VM_BUG_ON(!c->page->frozen);
c->freelist = get_freepointer(s, freelist);
c->tid = next_tid(c->tid);
local_irq_restore(flags);
return freelist;
new_slab:
if (c->partial) {//c->partial为NULL时,表示一级级回收链表无小缓存池,
//将进入下面的code,从二级回收链表上分配获取小缓存池
...
}
//将有可能从二级回收链表上获取小缓存池
freelist = new_slab_objects(s, gfpflags, node, &c);
...
page = c->page;//此时当前小缓存池已经切换到新小缓存池
if (likely(!kmem_cache_debug(s) && pfmemalloc_match(page, gfpflags)))
goto load_freelist;//跳转
...
}
static inline void *new_slab_objects(struct kmem_cache *s, gfp_t flags,
int node, struct kmem_cache_cpu **pc)
{
void *freelist;
struct kmem_cache_cpu *c = *pc;
struct page *page;
freelist = get_partial(s, flags, node, c);//从二级回收链表上分配获取内存
if (freelist)
return freelist;
page = new_slab(s, flags, node);//当前小缓存池,一级,二级回收链表上都没
//有空闲小内存块,将会从伙伴系统分配
//获取新小缓存池
if (page) {//成功从伙伴系统中分配获取到小缓存池
c = __this_cpu_ptr(s->cpu_slab);
if (c->page)
flush_slab(s, c);//清空当前小缓存池和小内存块链
//并且根据条件,是否将小缓存池释放会伙伴系统
/*
* No other reference to the page yet so we can
* muck around with it freely without cmpxchg
*/
freelist = page->freelist;//新小缓存池中小内存块链
page->freelist = NULL;//情况当前小缓存池中小内存块链,表示小内存块都被用
//page->freelist始终指向未被用小内存块链
stat(s, ALLOC_SLAB);
c->page = page;//新小缓存池,作为当前小缓存池
*pc = c;
} else
freelist = NULL;
return freelist;
}
static void *get_partial(struct kmem_cache *s, gfp_t flags, int node,
struct kmem_cache_cpu *c)
{
void *object;
int searchnode = (node == NUMA_NO_NODE) ? numa_node_id() : node;
//从二级回收链表上分配获取小缓存池
object = get_partial_node(s, get_node(s, searchnode), c, flags);
if (object || node != NUMA_NO_NODE)
return object;
...
}
static void *get_partial_node(struct kmem_cache *s, struct kmem_cache_node *n,
struct kmem_cache_cpu *c, gfp_t flags)
{
struct page *page, *page2;
void *object = NULL;
int available = 0;
int objects;
/*
* Racy check. If we mistakenly see no partial slabs then we
* just allocate an empty slab. If we mistakenly try to get a
* partial slab and there is none available then get_partials()
* will return NULL.
*/
if (!n || !n->nr_partial)//二级回收链表是否为空
//n指向二级回收链表
//n->nr_partial二级回收链表上小缓存池数量
return NULL;
spin_lock(&n->list_lock);
//page指向前一个小缓存池,page2指向下一个小缓存池
//n->partial二级回收链表
//这个循环将把二级回收链表上,第一个小缓存池作为当前小缓存池,其他的放到二级回收链表
list_for_each_entry_safe(page, page2, &n->partial, lru) {
void *t;
if (!pfmemalloc_match(page, flags))
continue;
//从二级回收链表上获取空闲小内存块链t
t = acquire_slab(s, n, page, object == NULL, &objects);
if (!t)
break;
available += objects;//统计小内存块数量
if (!object) {
c->page = page;//第一个,从二级回收链表获取的小缓存池作为当前小缓存池
stat(s, ALLOC_FROM_PARTIAL);
object = t;
} else {
put_cpu_partial(s, page, 0);//将后续从二级回收链表上获取的小缓存
//池全部取出来放入到一级回收链表上
stat(s, CPU_PARTIAL_NODE);
}
if (kmem_cache_debug(s) || available > s->cpu_partial / 2)
break;
}
spin_unlock(&n->list_lock);
return object;
}
static inline void *acquire_slab(struct kmem_cache *s,
struct kmem_cache_node *n, struct page *page,
int mode, int *objects)
{
void *freelist;
unsigned long counters;
struct page new;
/*
* Zap the freelist and set the frozen bit.
* The old freelist is the list of objects for the
* per cpu allocation list.
*/
freelist = page->freelist;
counters = page->counters;
new.counters = counters;
*objects = new.objects - new.inuse;//计算空闲小内存块数量
if (mode) {//mode非0,表示二级回收链表上第一个小缓存池
new.inuse = page->objects;
new.freelist = NULL;
} else {//二级回收链表上,其他小缓存池
new.freelist = freelist;
}
VM_BUG_ON(new.frozen);
new.frozen = 1;//表示小缓存池被冻结,不能释放回伙伴系统
//page->freelist = new.freelist
//page->counters=new.counters
if (!__cmpxchg_double_slab(s, page,
freelist, counters,
new.freelist, new.counters,
"acquire_slab"))
return NULL;
remove_partial(n, page);//从二级回收链表上移除小缓存池
WARN_ON(!freelist);
return freelist;
}
slub释放内存时,如何将内存释放到二级回收链表上?看如下:
函数调用过程:
kfree() --> slab_free() --> __slab_free() --> put_cpu_partial()
static void put_cpu_partial(struct kmem_cache *s, struct page *page, int drain)
{
struct page *oldpage;
int pages;
int pobjects;
...
//pobjeccts表示一级回收链表上空闲小内存块数量
//s->cpu_partial表示一级回收链表上空闲小内存块数量上限,大于上限将会移动到二级回收链表
if (drain && pobjects > s->cpu_partial) {
unsigned long flags;
...
//将一级回收链表上小缓存池,移动到二级回收链表
unfreeze_partials(s, this_cpu_ptr(s->cpu_slab));
...
}
...
}
static void unfreeze_partials(struct kmem_cache *s,
struct kmem_cache_cpu *c)
{
struct kmem_cache_node *n = NULL, *n2 = NULL;
struct page *page, *discard_page = NULL;
while ((page = c->partial)) {//循环从一级回收链表上获取小缓存池
struct page new;
struct page old;
c->partial = page->next;//修改一级回收链表头指针
n2 = get_node(s, page_to_nid(page));//n2指向二级回收链表
if (n != n2) {
if (n)
spin_unlock(&n->list_lock);
n = n2;
spin_lock(&n->list_lock);
}
do {
old.freelist = page->freelist;
old.counters = page->counters;
VM_BUG_ON(!old.frozen);
new.counters = old.counters;
new.freelist = old.freelist;
new.frozen = 0;//清楚冻结标志
//page->freelist=new.freelist
//page->counters=new.counters
} while (!__cmpxchg_double_slab(s, page,
old.freelist, old.counters,
new.freelist, new.counters,
"unfreezing slab"));
//n->nr_partial二级回收链表上,小缓存池数量
// s->min_partial二级回收链表上,小缓存池数量上限,超过将被释放回到伙伴系统
//new.inuse为0时,表示小缓存池上,没有小内存块被分配使用
if (unlikely(!new.inuse && n->nr_partial > s->min_partial)) {
page->next = discard_page;
discard_page = page;
} else {
add_partial(n, page, DEACTIVATE_TO_TAIL);//将小缓存池page加入到二级回收链表n上
stat(s, FREE_ADD_PARTIAL);
}
}
if (n)
spin_unlock(&n->list_lock);
while (discard_page) {//将到达n->nr_partial > s->min_partial条件的
//二级缓存回收链表上小缓存池将被释放回伙伴系统
page = discard_page;
discard_page = discard_page->next;
stat(s, DEACTIVATE_EMPTY);
discard_slab(s, page);//释放回伙伴系统
stat(s, FREE_SLAB);
}
}
从当前小缓存池,一级,二级回收链表上,都无法寻找到具有空闲小内存块的缓存池时,将会从伙伴系统分配获取小缓存池:
函数调用:
kmalloc() --> slab_alloc() --> slab_alloc_node() --> __slab_alloc() --> new_slab_objects() --> new_slab()
static struct page *new_slab(struct kmem_cache *s, gfp_t flags, int node)
{
struct page *page;
void *start;
void *last;
void *p;
int order;
...
//从伙伴系统中分配内存页,作为小缓存池
page = allocate_slab(s,
flags & (GFP_RECLAIM_MASK | GFP_CONSTRAINT_MASK), node);
if (!page)
goto out
...
page->slab_cache = s;//小缓存池page,属于同类缓存池s
__SetPageSlab(page);
if (page->pfmemalloc)
SetPageSlabPfmemalloc(page);
start = page_address(page);//通过内存页计算内存页首地址
...
last = start;
//将分配获取到的小缓存池,按照"slub核心原理"方式初始化,也可以视为格式化小缓存池
for_each_object(p, s, start, page->objects) {
setup_object(s, page, last);
set_freepointer(s, last, p);
last = p;
}
setup_object(s, page, last);
set_freepointer(s, last, NULL);//最后一个小内存块,头部空间写入NULL
page->freelist = start;//小缓存池中,第一个小内存块起始地址
page->inuse = page->objects;//小缓存池中小内存块总数量objects
page->frozen = 1;//小缓存池被冻结,不能释放回伙伴系统
out:
return page;
}
static struct page *allocate_slab(struct kmem_cache *s, gfp_t flags, int node)
{
struct page *page;
struct kmem_cache_order_objects oo = s->oo;
gfp_t alloc_gf
...
flags |= s->allocflags;
/*
* Let the initial higher-order allocation fail under memory pressure
* so we fall-back to the minimum order allocation.
*/
alloc_gfp = (flags | __GFP_NOWARN | __GFP_NORETRY) & ~__GFP_NOFAIL;
page = alloc_slab_page(alloc_gfp, node, oo);//小缓存池空间大小,oo是2的幂指数
if (unlikely(!page)) {//按照oo大小分配失败,将降低为s->min继续分配
oo = s->min;
/*
* Allocation may have failed due to fragmentation.
* Try a lower order alloc if possible
*/
page = alloc_slab_page(flags, node, oo);
if (page)
stat(s, ORDER_FALLBACK);
}
...
page->objects = oo_objects(oo);//计算小缓存池里面小内存块总数
...
return page;
}
简单总结:
slub分配时小内存块计算:
也可以说,申请size小内存块,查找属于那类大缓存池(同类缓存池).
第一种办法:
(1)kmalloc_index()得到kmalloc_caches[]角标index.
(2)然后kmalloc_caches[index]就是同类缓存池,之后见上面"slub分配释放"分析.
第二种办法:
通过调用kmalloc_slab()直接获取,同样,之后见上面"slub分配释放"分析.
slub分配内存时:
首先在当前小缓存池上检查有没有空闲小内存块,有则直接分配,如没有,检查一级回收链表上有没有小缓存池,有,则把该小缓存池切换为当前小缓存池,然后分配,如果一级回收链表还是没有,继续检查二级回收链表上小缓存池,同样,有,则切换为当前小缓存池,然后分配,要是二级回收链表上都还是没有,最后,向伙伴系统申请分配新的小缓存池,并将新分配的小缓存池切换为当前小缓存池,再分配.
slub释放内存:
释放小内存块,属于当前小缓存池时,直接释放回当前小缓存池.
释放小内存块,不属于当前小缓存池时,会被释放回所所属小缓存池,如果该小缓存池是分配使用完后,首次发生小内存块释放,那么将会把小缓存池挂接到一级回收链表上.每当有其他小缓存池发生挂接到一级回收链表时,会借此时机,先检查一级回收链表上小内存块总数是否大于s->cpu_partial,如果大于,将会把一级回收链表上所有小缓存池移动到二级回收链表上,移动过程中,还会检查二级回收链表上小缓存池数,如果超过s->min_partial,将会把超过的小缓存池(里面小内存块都未被分配使用情况)释放回伙伴系统,然后再将该小缓存池放入到一级回收链表.