《Linux6.5源码分析:内存管理系列文章》
本系列文章将对内存管理相关知识进行梳理与源码分析,重点放在linux源码分析上,并结合eBPF程序对内核中内存管理机制进行数据实时拿取与分析。
在进行正式介绍之前,有必要对文章引用进行提前说明。本系列文章参考了大量的博客、文章以及书籍:
-
《深入理解Linux内核》
-
《Linux操作系统原理与应用》
-
《奔跑吧Linux内核》
-
《深入理解Linux进程与内存》
-
《基于龙芯的Linux内核探索解析》
-
《图解Linux内核》
Linux内存管理:(四)物理页面分配之slab机制分配小内存 及 Linux6.5源码分析
前面的文章中,我们介绍了Linux中分配物理页面的流程,梳理了如何从伙伴系统中申请物理页面,如何释放物理页面到伙伴系统中,具体内容见如下文章:
Linux内存管理:(一)物理页面分配流程 及 Linux6.5源码分析(上)-优快云博客
Linux内存管理:(二)物理页面分配流程 及 Linux6.5源码分析(中)-优快云博客
Linux内存管理:(三)物理页面分配流程 及 Linux6.5源码分析(下)-优快云博客
通过伙伴系统分配的物理内存是以物理页为单位的,最小的一个物理页面也有4KB大小,很多时候所需要的内存比较小,达不到1页,这种情况下便可以通过slab机制来满足内存需求。
slab机制可以为系统提供特定大小的内存块,也可以提供按2的整数次幂字节的内存块,以满足不同进程对小内存的需求;slab实际上是内核在伙伴系统的基础上构建的资源池,他批量申请内存,完成映射,用户需要使用内存时直接由他返回,不必经过伙伴系统;
借鉴《图解Linux内核》一书中的一句话:slab机制就像是豆腐零售商,从豆腐工厂(伙伴系统)批量进货(物理页面),并进行二次加工将豆腐切成固定大小的豆腐块供用户购买(将物理页面化分为不同大小的slab分配器),普通用户没必要直接到工厂买一整板豆腐,直接按需求从不同零售商手中购买一小块豆腐即可;
接下来将深入slab机制,对slab机制进行探索学习;
1. slab机制介绍
前面已经对slab机制有了简单的介绍,用一句话概括一下: slab机制就是用于满足系统中对小内存的需求,在伙伴系统上设计出的一套机制, 为系统提供连续的小块固定大小的物理内存。slab机制将内存划分为多个独立缓存(每个缓存专用于特定类型对象,如进程描述符),每个缓存(kmem_cache)由多个slab组成,而每个slab进一步分割为相同大小的对象单元。
前面提到的缓存/独立缓存指的是slab机制中描述不同类型大小的slab描述符,内核中用struct kmem_cache
描述,下文中会详细介绍该结构体;每个缓存代表一种固定的对象缓存仓库(比如专门为task_struct ,dentry, inode等内核数据结构提供的内存slab缓存); 下图展示了系统中slab机制的分布 ,系统中所有类型的slab缓存均在slab缓存链表上,每个slab缓存由一个struct kmem_cache
描述, 一个slab缓存会为每个cpu申请一个专属该slab缓存的本地对象缓存池(存放着若干个空闲对象)、为每个NUMA节点申请一个slab节点,该slab节点中有一个slab共享对象缓存池(存放着若干个空闲对象)、三个链表(slabs_free、slabs_partial、slabs_full),这三个链表上链接着若干个slab ,每个slab均是一段连续的物理内存区域(由若干个对象obj组成),通过slab分配物理内存块是以对象为单位,这就意味着有的slab中所有的obj对象可能是空闲的,有的slab中所有obj对象可能均被使用,而有的slab中部分obj空闲部分被使用,这三种slab分别会被维护在三种slab链表中;具体布局见下图;
slab机制有如下特性:
- 把分配的内存块当作对象(object)来看待。对象可以自定义构造函数(constructor) 和析构函数(destructor)来初始化对象的内容并释放对象的内容。
- slab对象被释放之后不会马上丢弃而是继续保留在内存中,可能稍后会被用到,这样不需要重新向伙伴系统申请内存。
- slab机制可以根据特定大小的内存块来创建slab描述符,如内存中常见的数据结构、打开文件对象等,这样可以有效地避免内存碎片的产生,也可以快速获得频繁访问的数据结构。另外,slab机制也支持按2的n次方字节大小分配内存块。
- slab机制创建了多层的缓冲池,充分利用了空间换时间的思想,未雨绸谬,有效地解决了效率问题。
- 每个CPU有本地对象缓冲池,避免了多核之间的锁争用问题。
- 每个内存节点有共享对象缓冲池。
1.1. slab系统框架描述
前文中对slab机制有了一定的介绍,我们也可以通过上图知道slab机制由slab缓存来描述不同类型大小的slab,为每个cpu及NUMA节点创建了对象缓存池以及slab链表。为了让读者对NUMA框架下 slab机制的分布情况以及slab和伙伴系统的关系有一个直观的认识,本文章将结合下图对slab机制进行逐层介绍:
- 首先slab分配器是基于伙伴系统提供的物理内存块分配功能,他从伙伴系统上申请连续的物理页面,并最终将物理页面释放到伙伴系统中;
- 其次每个类型的slab缓存,在NUMA的各个节点上均有一个slab节点,该slab节点包括三个链表及一个array_cache共享对象缓存池;在每个CPU上包含一个本地对象缓存池,其中包含若干个空闲对象;
- slab节点中的三个slab链表上,维护着若干个slab区域(连续的物理页面),slab区域是有多干个空闲/非空闲对象obj组成的;
以上是一个slab缓存在单个NUMA节点中的分布情况,对于一个slab缓存在不同NUMA节点,以及在不同cpu上的布局情况可参考下图:
不同的slab缓存均在每个NUMA节点有着自己的一套slab节点及本地对象缓存池;
1.2. 结构体介绍
对slab分配器在NUMA系统中的布局有了一定的了解后,需要深入内核源码,对前文中涉及到的相关结构体进行了解,主要包括:slab描述符、slab节点、本地对象缓冲池、共享对象缓冲池、3个slab链表、n个slab分配器,以及众多slab缓存对象;
1.2.1. slab描述符(kmem_cache)
该结构体描述一个slab缓存,用于对每个slab缓存进行管理;
/*每个slab缓存都由 kmem_cache 数据结构表示*/
struct kmem_cache {
/*1.每个CPU都有一个,表示本地对象缓冲池*/
struct array_cache __percpu *cpu_cache;
/* 1) Cache tunables. Protected by slab_mutex */
/*表示在当前 CPU 的本地对象缓冲池 array_cache 为空时,
*从共享对象缓冲池或 slabs_partial/slabs_free 列表中迁移空闲对象的数目
*/
unsigned int batchcount;
/*当本地对象缓冲池中的空闲对象的数目大于 limit 时,
*会主动释放 batchcount 个对象,便于内核回收和销毁 slab*/
unsigned int limit;
/**/
unsigned int shared;
/*对象的长度,这个长度要加上 align 对齐字节*/
unsigned int size;
struct reciprocal_value reciprocal_buffer_size;
/* 2) touched by every alloc & free from the backend */
/*对象的分配掩码*/
slab_flags_t flags; /* constant flags */
/*一个 slab 中最多有多少个对象*/
unsigned int num; /* # of objs per slab */
/* 3) cache_grow/shrink */
/* order of pgs per slab (2^n) */
/*一个slab分配器从伙伴系统申请多少个物理页面2^gfporder个*/
unsigned int gfporder;
/* force GFP flags, e.g. GFP_DMA */
gfp_t allocflags;
/*一个 slab 中可以有多少个不同的缓存行*/
size_t colour; /* cache colouring range */
/*着色区的长度,和 L1 缓存行大小相同*/
unsigned int colour_off; /* colour offset */
/*每个对象要占用 1 字节来存放 freelist*/
unsigned int freelist_size;
/* constructor func */
/*构造函数*/
void (*ctor)(void *obj);
/* 4) cache creation/removal */
/*slab 描述符的名称*/
const char *name;
/*在slab_caches 链表中,该slab缓存用于找到前一个和后一个slab缓存*/
struct list_head list;
/*当前slab缓存被引用的次数,
*当创建其他slab描述符并引用该slab描述符时,
*会增加引用次数
*/
int refcount;
/*对象的实际大小*/
int object_size;
/*对齐的长度*/
int align;
/* 5) statistics */
#ifdef CONFIG_DEBUG_SLAB
......
#endif
/*NUMA系统中,每个节点下对应一个slab节点*/
struct kmem_cache_node *node[MAX_NUMNODES];
};
1.2.2. slab节点 (kmem_cache_node)
每个NUMA节点下都有一个slab节点, slab节点中维护三个slab链表和一个共享对象缓存池,当前NUMA节点下的所有cpu均可访问共享对象缓存池;
/*
* NUMA当前节点下的slab节点
*/
struct kmem_cache_node {
#ifdef CONFIG_SLAB
/*1. 用于保护 slab 节点中的 slab 链表*/
raw_spinlock_t list_lock;
/*2. slab节点的三个链表*/
/*2.1 slab 链表,表示 slab 节点中有部分空闲对象*/
struct list_head slabs_partial; /* partial list first, better asm code */
/*2.2 slab 链表,表示 slab 节点中没有空闲对象*/
struct list_head slabs_full;
/*2.3 slab 链表,表示 slab 节点中全部都是空闲对象*/
struct list_head slabs_free;
/*3. 表示 slab 节点中有多少个 slab */
unsigned long total_slabs; /* length of all slab lists */
/*4. 表示 slab 节点中有多少全是空闲对象的空闲 slab*/
unsigned long free_slabs; /* length of free slab list only */
/*5. 表示 slab 节点中有多少个空闲对象*/
unsigned long free_objects;
/*6. 表示 slab 节点中所有空闲对象的最大阈值,
* 即 slab 节点中可容许的空闲对象数目最大阈值
*/
unsigned int free_limit;
/*7. 记录当前着色区的编号。
* 所有 slab 节点都按照着色编号来计算着色区的大小,
* 达到最大值后又从 0 开始计算
*/
unsigned int colour_next; /* Per-node cache coloring */
/*8. 当前节点中的 共享对象缓存池
* 在多核 CPU 中,除了本地 CPU 外,
* slab 节点中还有一个当前节点下所有 CPU 都共享的对象缓冲区;
*/
struct array_cache *shared; /* shared per node */
/*用于NUMA系统*/
struct alien_cache **alien; /* on other nodes */
/*下一次收割 slab 节点的时间*/
unsigned long next_reap; /* updated without locking */
/*表示访问了 slabs_free 的 slab 节点*/
int free_touched; /* updated without locking */
#endif
...
};
1.2.3. 对象缓冲池结构体 (array_cache):
对象缓存池通过struct array_cache
结构体描述,该结构体可以描述本地对象缓冲池,也可以描述共享对象缓存池;
// slab 描述符会给每个 CPU 提供一个对象缓冲池(array_cache)
// array_cache 可以描述本地对象缓冲池,也可以描述共享对象缓冲池
struct array_cache {
// 对象缓冲池中可用对象的数目
unsigned int avail;
// 对象缓冲池中可用对象数目的最大阈值
unsigned int limit;
// 迁移对象的数目,如从共享对象缓冲池或者其他 slab 中迁移空闲对象到该对象缓冲池的数量
unsigned int batchcount;
// 从缓冲池中移除一个对象时,将 touched 置为 1 ;
// 当收缩缓冲池时,将 touched 置为 0;
unsigned int touched;
// 保存对象的实体
// 指向存储对象的变长数组,每一个成员存放一个对象的指针。这个数组最初最多有 limit 个成员
void *entry[];
};
1.2.4. slab分配器
slab分配器由struct slab
结构体描述, 会根据slab机制是slab, slub, slob进行字段选择;
/* Reuses the bits in struct page */
struct slab {
unsigned long __page_flags;
#if defined(CONFIG_SLAB)
/*1. 指向该slab分配器所属的slab缓存类型*/
struct kmem_cache *slab_cache;
union {
struct {
/*2. 维护在slab节点下的三个slab链表*/
struct list_head slab_list;
/*3. 指向当前slab分配器的管理区*/
void *freelist; /* array of free object indexes */
/*4. 当前slab分配器下的第一个对象*/
void *s_mem; /* first object */
};
struct rcu_head rcu_head;
};
/*5. 当前slab分配器中 活跃的对象*/
unsigned int active;
#elif defined(CONFIG_SLUB)
...
#endif
...
}
1.3. slab分配器的布局
slab节点中的三个slab链表(slabs_full, slabs_free,slabs_partial)上链接着若干个slab分配器,slab分配器的内存布局由下面三个部分组成:
- 着色区;
- 若干个slab对象 obj;
- freelist管理区。管理区可以看作一个freelist数组,数组的每个成员占用1字节,每个成员代表一个slab对象
slab分配器的布局模式不是唯一的,内核提供了三种布局模式:
-
OBJFREELIST_SLAB模式。其目的是高效利用slab分配器中的内存。使用slab分配器中最后一个slab对象的空间作为管理区,如下图所示:
-
OFF_SLAB模式。slab分配器的管理数据不在sIab分配器中,额外分配的内存用于管理,如下图所示:
-
ON_SLAB 正常模式。传统的布局模式,如下图所示:
1.4. 着色区
着色区(colouring)是 slab 分配器用于优化高速缓存访问效率的一种策略。它通过在内存分配时引入偏移量,使对象在缓存行中的分布更加均匀,减少多个对象共享同一缓存行带来的缓存冲突(cache contention)。如果两个对象的起始地址总是对齐到相同的缓存行(如起始地址都为 0x100
),同时访问这些对象会反复覆盖缓存行的内容,导致性能下降。
具体来说,着色区的大小由 colour_next × colour_off 计算得出,其中 colour_next
表示可用的偏移量数量,colour_off
代表 L1 缓存行大小。这样,每个 slab 中的对象在分配时会被适当错开,从而避免 CPU 访问时频繁发生缓存行覆盖,提高数据局部性和访问效率。
通过着色区的优化,同一 slab 分配器管理的对象不再集中在相同的缓存行,而是分散在不同的缓存行中,这样可以有效减少缓存争用,提高 CPU 访问内存的性能。这一机制对于多核系统尤为重要,因为它有助于提高数据在不同核心间的独立性,降低不必要的缓存同步开销。
2. 创建一个slab缓存(kmem_cache)
内核中创建一个slab缓存(slab描述符)的大致步骤如下:
- 在创建slab缓存之前, 会首先查看是否存在一个满足所要创建slab描述符要求的 已存在的slab描述符,如果存在这样的slab描述符, 那么将其复用 (
__kmem_cache_alias()
->find_mergeable()
);不满足复用条件, 则尝试创建一个新的slab描述符;- 可复用条件: 已存在slab描述符大小 大于 所要申请的大小;
- 可复用条件: 二者对齐方式相同;
- 可复用条件: 标志位slag匹配;
- 为slab描述符 重新申请一个缓存用于存放slab描述符名字;
- 开始创建一个新的slab描述符:
- a. 创建slab描述符所用到的结构体
struct kmem_cache
- b. 创建slab描述符实体: 着色区的计算, 分配掩码确认, slab分配器的布局设置, 配置slab描述符
- c. 将新创建好的slab描述符加入系统slab_caches链表中
- a. 创建slab描述符所用到的结构体
具体的函数调用图如下:
借鉴《奔跑吧Linux内核》一书中的插图,slab描述符的创建过程如下:
kmem_cache_create()
是slab分配器提供的接口函数,用于创建一个slab描述符,该函数会调用kmem_cache_create_usercopy
函数:
struct kmem_cache *
kmem_cache_create(const char *name, unsigned int size, unsigned int align,
slab_flags_t flags, void (*ctor)(void *))
{
/*1.调用kmem_cache_create_usercopy实现*/
return kmem_cache_create_usercopy(name, size, align, flags, 0, 0,
ctor);
}
kmem_cache_create()
-> kmem_cache_create_usercopy()
函数会检查是否存在可复用的slab描述符, 如果不存在,才会进一步为slab描述符进行名称和实体的创建; 其中通过__kmem_cache_alias
检查系统中是否存在可复用的slab描述符, 通过kstrdup_const()
为新slab描述符所用名字申请缓存, 通过create_cache()
申请新的slab描述符实体;
struct kmem_cache *
kmem_cache_create_usercopy(const char *name,
unsigned int size, unsigned int align,
slab_flags_t flags,
unsigned int useroffset, unsigned int usersize,
void (*ctor)(void *))
{
struct kmem_cache *s = NULL;
const char *cache_name;
int err;
#ifdef CONFIG_SLUB_DEBUG
/*0. 如果启用了SLUB调试选项,则初始化相关调试功能*/
if (flags & SLAB_DEBUG_FLAGS)
static_branch_enable(&slub_debug_enabled);
if (flags & SLAB_STORE_USER)
stack_depot_init();
#endif
/*1. 申请slab_mutex互斥锁进行上锁保护
*/
mutex_lock(&slab_mutex);
/*2.做必要的检查
* 检查名称、大小是否合法,
* 以及是否在中断上下文中
* 验证传入的flag标志是否在允许范围内
*/
err = kmem_cache_sanity_check(name, size);
if (err) {
goto out_unlock;
}
/* Refuse requests with allocator specific flags */
if (flags & ~SLAB_FLAGS_PERMITTED) {
err = -EINVAL;
goto out_unlock;
}
flags &= CACHE_CREATE_MASK;
/* Fail closed on bad usersize of useroffset values. */
if (!IS_ENABLED(CONFIG_HARDENED_USERCOPY) ||
WARN_ON(!usersize && useroffset) ||
WARN_ON(size < usersize || size - usersize < useroffset))
usersize = useroffset = 0;
/*3. 检查是否有现成的slab描述符可复用
* 调用__kmem_cache_alias 判断是否可复用
*/
if (!usersize)
s = __kmem_cache_alias(name, size, align, flags, ctor);
if (s)
goto out_unlock;
/*4. 没有可复用的slab描述符,重新分配一个缓冲区存放slab描述符的名称*/
cache_name = kstrdup_const(name, GFP_KERNEL);
if (!cache_name) {
err = -ENOMEM;
goto out_unlock;
}
/*5. 创建一个新的slab描述符*/
s = create_cache(cache_name, size,
calculate_alignment(flags, align, size),
flags, useroffset, usersize, ctor, NULL);
if (IS_ERR(s)) {
err = PTR_ERR(s);
kfree_const(cache_name);
}
out_unlock:
/*6. 释放互斥锁*/
mutex_unlock(&slab_mutex);
...
return s;
}
EXPORT_SYMBOL(kmem_cache_create_usercopy);
2.1. create_cache 创建slab描述符实体
该函数会为slab缓存申请slab描述符的结构体kmeme_cache
并填充, 调用 __kmem_cache_create()
函数创建slab描述符, 并最终将新创建好的slab描述符加入系统slab_caches链表;
static struct kmem_cache *create_cache(const char *name,
unsigned int object_size, unsigned int align,
slab_flags_t flags, unsigned int useroffset,
unsigned int usersize, void (*ctor)(void *),
struct kmem_cache *root_cache)
{
struct kmem_cache *s;
int err;
if (WARN_ON(useroffset + usersize > object_size))
useroffset = usersize = 0;
err = -ENOMEM;
/*1. 创建一个新的kmem_cache数据结构,并填充*/
s = kmem_cache_zalloc(kmem_cache, GFP_KERNEL);
if (!s)
goto out;
s->name = name;
s->size = s->object_size = object_size;
s->align = align;
s->ctor = ctor;
#ifdef CONFIG_HARDENED_USERCOPY
s->useroffset = useroffset;
s->usersize = usersize;
#endif
/*2. 创建该slab描述符*/
err = __kmem_cache_create(s, flags);
if (err)
goto out_free_cache;
s->refcount = 1;
/*3. 将新建的slab 描述加入slab_cache 链表中*/
list_add(&s->list, &slab_caches);
return s;
out_free_cache:
kmem_cache_free(kmem_cache, s);
out:
return ERR_PTR(err);
}
2.2. __kmem_cache_create
该函数的重点步骤如下: 首先会计算着色区大小和便偏移, 避免同一 slab
内的对象占用相同的缓存行,减少 CPU 缓存冲突; 其次会确定分配掩码, 并根据slab分配器的内存布局模式进行内存布局的设置, 其实无论是什么布局模式,最终都会调用calculate_slab_order()
;最后会调用set_cpu_cache()
函数配置slab缓存, 其实就是在每个cpu建立该slab缓存的本地对象缓存池,在每个NUMA节点建立该slab缓存的slab节点;
/**
* @brief 根据传入的缓存描述符 (struct kmem_cache *cachep) 和标志位 (slab_flags_t flags),
* 为特定的内存分配需求创建一个 slab 缓存
*/
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;
...
/*1. 对齐方式的确认与调整;
*/
/*1.1 让slab 描述符的大小size 与 系统的word长度对齐
* 确保对象大小是字对齐的,
* 避免在某些架构上因未对齐访问引发错误;
*/
size = ALIGN(size, BYTES_PER_WORD);
if (flags & SLAB_RED_ZONE) {/*SLAB_RED_ZONE,检查是否溢出,实现调试功能*/
ralign = REDZONE_ALIGN;
/* If redzoning, ensure that the second redzone is suitably
* aligned, by adjusting the object size accordingly. */
size = ALIGN(size, REDZONE_ALIGN);
}
/*1.2 强制对齐;
* 如果指定的对齐方式 比当前对齐方式ralign更严格,
* 则调整为更严格的对齐方式;*/
if (ralign < cachep->align) {
ralign = cachep->align;
}
/* disable debug if necessary */
if (ralign > __alignof__(unsigned long long))
flags &= ~(SLAB_RED_ZONE | SLAB_STORE_USER);
/*
* 4) Store it.
*/
cachep->align = ralign;
/*2. 着色区的处理:
* colour_off 表示一个着色区的长度;
* 它和 L1 高速缓存行大小相同;
*/
cachep->colour_off = cache_line_size();
/* Offset must be a multiple of the alignment. */
if (cachep->colour_off < cachep->align)
cachep->colour_off = cachep->align;
/*3. 根据slab初试化程度,确认分配掩码;
* 枚举类型 slab_state 表示 slab 系统初始化的状态;
* 如 DOWN、PARTIAL、PARTIAL_NODE、UP 和 FULL 等;
* FULL 和 UP 表示slab子系统已经初始化完成;
* 当 slab 机制完全初始化完成后状态变成 FULL;
*
* slab_is_available()表示当 slab 分配器处于 UP 或者 FULL 状态时,
* 分配掩码可以使用 GFP_KERNEL;否则,只能使用 GFP_NOWAIT;
*/
if (slab_is_available())
gfp = GFP_KERNEL;
else
gfp = GFP_NOWAIT;
kasan_cache_create(cachep, &size, &flags);
/*slab 对象的大小按照 cachep->align 大小来对齐*/
size = ALIGN(size, cachep->align);
...
/*4. slab分配器的内存布局设置;
*
* 会根据三种不同的slab分配器布局模式,进行设置,主要解决以下问题:
* a. 一个slab分配器 需要多少个连续物理页面?
* b. 一个slab分配器 可包含多少个slab对象?
* c. 管理slab对象的大小是多少?
* d. 一个slab分配器包含多少个着色区?
*/
/*4.1 OBJFREELIST_SLAB 模式
* 若 freelist 小于一个 slab 对象的大小 并且 没有指定构造函数,
* 那么 slab 分配器就可以采用 OBJFREELIST_SLAB 模式
*/
if (set_objfreelist_slab_cache(cachep, size, flags)) {
flags |= CFLGS_OBJFREELIST_SLAB;
goto done;
}
/*4.2 OFF_SLAB 模式
* 若一个 slab 分配器的剩余空间小于 freelist 数组的大小,
* 那么使用 OFF_SLAB 模式,额外分配内存用于管理freelist 数组
*/
if (set_off_slab_cache(cachep, size, flags)) {
flags |= CFLGS_OFF_SLAB;
goto done;
}
/*4.3 正常模式
* 若一个 slab 分配器的剩余空间大于 slab 管理数组freelist的大小,
* 那么使用正常模式
*/
if (set_on_slab_cache(cachep, size, flags))
goto done;
return -E2BIG;
done:
/*5. 配置slab描述符*/
/*freelist_size 表示一个 slab 分配器中管理区————freelist 大小*/
cachep->freelist_size = cachep->num * sizeof(freelist_idx_t);
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;
/*size 表示一个 slab 对象的大小*/
cachep->size = size;
cachep->reciprocal_buffer_size = reciprocal_value(size);
...
/*5.1 继续配置 slab 描述符*/
err = setup_cpu_cache(cachep, gfp);
if (err) {
__kmem_cache_release(cachep);
return err;
}
return 0;
}
3. 分配slab对象
kmem_cache_alloc()
是slab机制中用于分配slab缓存对象的核心函数, 该函数会从指定的slab缓存分配一个空闲的slab对象, 在分配过程中处于关中断状态; __do_cache_alloc()
->____cache_alloc()
实现分配一个空闲对象, ____cache_alloc()
函数会通过快速分配路径和慢速分配路径两种方法 从本地对象缓存池中分配一个空闲slab对对象;
快速分配路径: 在分配对象过程中, 会优先从本地对象缓存池中分配, 如果本地对象缓存池有空闲对象, 则直接弹出缓存池中最后一个空闲对象, 并返回该对象即可;
慢速分配路径: 当本地对象缓存池中 没有一个空闲对象, 那么就需要依次通过以下方法,迁移batchcount个空闲对象到本地对象缓存池中:
- 1.当前NUMA节点中的共享对象缓存池,如果共享对象缓存池中存在空闲对象,则迁移到跟本地对象缓存池ac中;
- 2.slab节点下的slab分配器中迁移,如果当前slab节点中的
slabs_free
、slabs_partial
链表中存在足够的空闲对象,则迁移batchcount个空闲对象到本地对象缓存池; - 3.扩展slab分配器,如果当前slab节点下的共享对象缓存池 以及 三大链表中的slab分配器均没有一个空闲的slab对象,则需要从伙伴系统中申请一段连续的物理页面,即重新分配一个全新的slab分配器,并从中迁移batchcount个空闲对象到本地对象缓存池;
在迁移结束,依然要从本地对象缓存池中申请一个空闲对象,并将该对象返回;
以上便是申请一个slab对象的全流程,接下来上一张图:
kmem_cache_alloc()
函数最终调用slab_alloc_node()
函数 从指定NUMA节点分配slab空闲对象;其实现源码如下:
/*从指定NUMA节点 分配物理对象*/
static __always_inline void *
slab_alloc_node(struct kmem_cache *cachep, struct list_lru *lru, gfp_t flags,
int nodeid, size_t orig_size, unsigned long caller)
{
unsigned long save_flags;
void *objp;
struct obj_cgroup *objcg = NULL;
bool init = false;
/*1. 预分配检查,涉及对象 cgroup 资源限制*/
flags &= gfp_allowed_mask;
cachep = slab_pre_alloc_hook(cachep, lru, &objcg, 1, flags);
if (unlikely(!cachep))
return NULL;
/*2. 使用 KFENCE 机制分配对象;
* 如果 kfence_alloc() 能够满足分配需求(可能是为了检测越界访问),
* 直接返回 objp,无需再进入普通分配流程
*/
objp = kfence_alloc(cachep, orig_size, flags);
if (unlikely(objp))
goto out;
/*3. 正常slab对象分配流程
*3.1 首先关中断,因为要从本地CPU对象缓存池中分配空闲对象,
* 防止cpu资源竞争,保证slab分配的原子性;
*3.2 其次调用__do_cache_alloc -> ____cache_alloc 进行快速或慢速路径
* 从本地对象缓存池分配slab对象;
*3.3 最后 开中断;
*/
/*3.1 关中断*/
local_irq_save(save_flags);
/*3.2 分配slab对象*/
objp = __do_cache_alloc(cachep, flags, nodeid);
/*3.3 开中断*/
local_irq_restore(save_flags);
/*4. 分配后调试检查*/
objp = cache_alloc_debugcheck_after(cachep, flags, objp, caller);
prefetchw(objp);
/*5. 初始化分配的对象*/
init = slab_want_init_on_alloc(flags, cachep);
out:
/*6. 后处理钩子*/
slab_post_alloc_hook(cachep, objcg, flags, 1, &objp, init,
cachep->object_size);
return objp;
}
执行真正分配操作的是__do_cache_alloc()
函数,该函数会调用____cache_alloc()
函数通过快速及慢速方式分配空闲slab对象;
/*分配slab缓存对象*/
static __always_inline void *
__do_cache_alloc(struct kmem_cache *cachep, gfp_t flags, int nodeid __maybe_unused)
{
return ____cache_alloc(cachep, flags);
}
我们看一下____cache_alloc()
函数通过快速或慢速路径 从本地对象缓存池分配slab对象的逻辑:
- 快速路径: 如果本地对象缓存池中有空闲的对象,则直接分配;
- **慢速路径: **从当前节点对应的共享对象缓存池、slab分配器、或扩展slab分配器 迁移空闲对象 至 本地对象缓存池, 并从本地对象缓存池中分配一个空闲对象;
/**
* @brief 通过快速或慢速路径 从本地对象缓存池分配slab对象;
* a.如果本地对象缓存池中有空闲的对象,则直接分配;
* b.如果没有,则尝试慢速路径分配,
* 即:从当前节点对应的共享对象缓存池、slab分配器、或扩展slab
* 来将对应的空闲对象迁移至 本地对象缓存池,并分配;
*/
static inline void *____cache_alloc(struct kmem_cache *cachep, gfp_t flags)
{
void *objp;
struct array_cache *ac;
/*0. 检查是否处于关中断状态*/
check_irq_off();
/*1. 获取slab描述符中cache的本地CPU对象缓冲池ac*/
ac = cpu_cache_get(cachep);
/*2. 快速路径: 从当前CPU 本地缓存池中快速获取对象;
*
* 判断本地对象缓冲池中有没有空闲的对象;
* ac->avail 表示本地对象缓冲池中有空闲对象;
* 这里直接通过 ac->entry[--ac->avai] 弹出最后一个 slab对象。
* 获取到slab对象后, 去out处返回该objp slab对象
*/
if (likely(ac->avail)) {
ac->touched = 1;
objp = ac->entry[--ac->avail];
STATS_INC_ALLOCHIT(cachep);
goto out;
}
/*3. 慢速路径: 从共享缓存池、slab分配器、或扩展slab分配器 来分配;
* 本地CPU缓存池中没有足够的对象,尝试从共享slab中 重新填充本地CPU缓存池;
*/
STATS_INC_ALLOCMISS(cachep);
objp = cache_alloc_refill(cachep, flags);
/*4. 再次获取本地CPU 缓存池*/
ac = cpu_cache_get(cachep);
out:
if (objp)
kmemleak_erase(&ac->entry[ac->avail]);
return objp;
}
3.1. 慢速路径分配空闲对象
慢速路径主要是先将空闲对象迁移至本地对象缓存池, 再从本地对象缓存池中弹出最后一个空闲对象;从哪里迁移空闲对象? 从当前slab节点中迁移, 优先从当前slab节点中的shared共享对象缓存池迁移, 不够则从slab链表中的slab分配器中迁移空闲对象, 如果slab链表中没有一个slab分配器含有空闲对象, 说明需要从伙伴系统中再申请一段连续的物理页面用于slab分配器,然后迁移空闲对象到本地对象缓存池中; 详细分配流程可见下图:
/* 慢速路径分配对象
* 本地CPU对象缓存池为空,即没有空闲slab对象,
* 则从共享对象缓存池、共享数组、通过扩展slab 向本地CPU对象缓存池ac 迁移
* 并弹出本地对象缓存池中最后一个 slab对象;
*/
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 slab *slab;
/*0. 确保本地关中断
* 获取当前 CPU 的 NUMA 节点 ID
* 获取CPU本地对象缓冲池ac;
* 获取slab节点;
* 获取共享对象缓存池 shared;
*/
check_irq_off();
node = numa_mem_id();
ac = cpu_cache_get(cachep);//获取CPU本地对象缓冲池ac
batchcount = ac->batchcount;//单批次最大可迁移数量
if (!ac->touched && batchcount > BATCHREFILL_LIMIT) {
/*
* If there was little recent activity on this cache, then
* perform only a partial refill. Otherwise we could generate
* refill bouncing.
*/
batchcount = BATCHREFILL_LIMIT;
}
n = get_node(cachep, node);//获取当前NUMA节点的 slab节点
BUG_ON(ac->avail > 0 || !n);
shared = READ_ONCE(n->shared);//获取当前节点的共享对象缓存池 shared
/*1. 若共享缓存池为空、当前slab节点没有可用对象,则直接去扩展slab*/
if (!n->free_objects && (!shared || !shared->avail))
goto direct_grow;
/*2. 加锁保护 NUMA 节点的共享和 slab 数据结构*/
raw_spin_lock(&n->list_lock);
shared = READ_ONCE(n->shared);
/* See if we can refill from the shared array */
/*3. 从共享对象缓冲池shared 迁移 到本地缓存池
* 如果共享对象缓冲池shared 中有空闲对象,
* 则从共享对象缓冲池shared 向 本地对象缓冲池ac 迁移 batchcount个 空闲对象;
* 执行迁移操作(transfer_objects);
*/
if (shared && transfer_objects(ac, shared, batchcount)) {
shared->touched = 1;// 标记共享缓存最近被访问
goto alloc_done; // 如果成功,从共享缓存补充完成,跳转至完成流程
}
/*4. 从 slab节点 中两个链表中 迁移空闲对象 到 本地对象缓存池ac
* 如果共享对象缓存池中没有空闲对象;
* 尝试从当前NUMA节点的 slab节点中 (slabs_partial , slabs_free 链表)
* 迁移 batchcount 个空闲对象到本地缓冲池;
*/
while (batchcount > 0) {
/* Get slab alloc is to come from. */
/*4.1 获取slab节点中第一个slab成员;
* 查看 slabs_partial , slabs_free 链表
* 返回该链表中第一个slab成员;
* 若无slab可用,则跳转到 扩展slab 流程中;
*/
slab = get_first_slab(n, false);
if (!slab)
goto must_grow;
check_spinlock_acquired(cachep);
/*4.2 从slab中迁移batchcount个空闲对象 到 本地缓存池中*/
batchcount = alloc_block(cachep, ac, slab, batchcount);
/*4.3 根据 slab 使用情况修正其链表位置*/
fixup_slab_list(cachep, n, slab, &list);
}
must_grow:
/*4.4. 更新 NUMA 节点的 可用对象free_objects 计数*/
n->free_objects -= ac->avail;
alloc_done:
/*5. 分配成功,释放锁 并 修正调试数据(用于对象泄漏检测)*/
raw_spin_unlock(&n->list_lock);
fixup_objfreelist_debug(cachep, &list);
direct_grow:
/*6. 本地缓存仍为空,扩展slab,即重新从伙伴系统申请物理内存分配到slab分配器
* a.共享对象缓存池没有空闲对象 及 b.slab节点没有空闲对象;
* 说明当前NUMA节点没有slab空闲对象;
* 只能重新分配该类型的slab,这就是一开始初始化和配置slab描述符的情景;
*/
if (unlikely(!ac->avail)) {
/* Check if we can use obj in pfmemalloc slab */
/*6.1 检查是否可以使用 pfmemalloc 的特殊 slab(用于内存压力情况下)*/
if (sk_memalloc_socks()) {
void *obj = cache_alloc_pfmemalloc(cachep, n, flags);
if (obj)
return obj;
}
/*6.2 扩展slab(分配一个cachep类型的slab),
* 然后返回该slab中第一个物理页面的page结构体指针
*/
slab = cache_grow_begin(cachep, gfp_exact_node(flags), node);
/*
* cache_grow_begin() can reenable interrupts,
* then ac could change.
*/
ac = cpu_cache_get(cachep);
/*6.3 从新扩展的slab分配器中 迁移 batchcount 个空闲对象 到 本地对象缓存池ac 中*/
if (!ac->avail && slab)
alloc_block(cachep, ac, slab, batchcount);
/*6.4 将刚分配的slab分配器添加到合适的队列中,
* 这个场景下应该添加到slabs partial 链表中
*/
cache_grow_end(cachep, slab);
/*6.5 扩展slab失败,
* 本地缓存池中依然没有空闲对象,返回失败
*/
if (!ac->avail)
return NULL;
}
/*7. 设置本地对象缓冲池的touched 为1,表示刚刚使用过本地对象缓冲池。*/
ac->touched = 1;
/*8. 从本地对象缓存区 弹出一个空闲对象*/
return ac->entry[--ac->avail];
}
3.2. 扩展slab分配器
扩展slab分配器其实就是根据前面创建slab描述符时设定的slab布局, 为slab分配器申请连续物理页面,并为slab分配器中的着色区、freelist管理区进行初始化;在看源码时,我们可以知道,slab机制是通过kmem_getpages()
函数从伙伴系统中申请固定大小的连续物理页面;
/*
* Grow (by 1) the number of slabs within a cache. This is called by
* kmem_cache_alloc() when there are no active objs left in a cache.
* 扩展slab,当共享对象缓存池,slab节点没有空闲对象时,需要扩展slab;
*/
static struct slab *cache_grow_begin(struct kmem_cache *cachep,
gfp_t flags, int nodeid)
{
void *freelist;
size_t offset;
gfp_t local_flags;
int slab_node;
struct kmem_cache_node *n;
struct slab *slab;
/*0. 标志检查,条件检查,中断环境检查*/
if (unlikely(flags & GFP_SLAB_BUG_MASK))
flags = kmalloc_fix_flags(flags);
WARN_ON_ONCE(cachep->ctor && (flags & __GFP_ZERO));
local_flags = flags & (GFP_CONSTRAINT_MASK|GFP_RECLAIM_MASK);
check_irq_off();//确认中断已经关闭;
if (gfpflags_allow_blocking(local_flags))
local_irq_enable();
/*1. 给slab 分配物理页面,从伙伴系统中分配;
* 分配一个 slab分配器所需要的物理页面,
* 这里会从指定的节点(nodeid)分配 2^cachep->gfporder个页面;
* 调用的是__alloc_pages_node来从伙伴系统分配物理页面;
*/
slab = kmem_getpages(cachep, local_flags, nodeid);
if (!slab)
goto failed;
slab_node = slab_nid(slab);
n = get_node(cachep, slab_node);
/* Get colour for the slab, and cal the next value. */
/*2. 计算slab分配器的着色区大小和实际偏移,便于后面为slab管理区分配freelist
* offset在分配对象时用于调整起始地址,
* 使得不同slab的对象在缓存行中的位置不同,提高缓存利用率;
*/
n->colour_next++;
if (n->colour_next >= cachep->colour)
n->colour_next = 0;
offset = n->colour_next;
if (offset >= cachep->colour)
offset = 0;
offset *= cachep->colour_off;
/* Get slab management. */
/*3. 计算slab管理区freelist的起始地址
* 并为slab分配管理结构 freelist
* 这里会根据slab分配器的不同模式 对管理区进行分配;
*/
/*3.1 找到管理区首地址,并分配freelist*/
freelist = alloc_slabmgmt(cachep, slab, offset,
local_flags & ~GFP_CONSTRAINT_MASK, slab_node);
if (OFF_SLAB(cachep) && !freelist)//如果是OFF_SLAB模式,并且freelist没申请成功,跳转到opps1去清理
goto opps1;
/*3.2 建立页面与缓存的映射关系,关联freelist*/
slab->slab_cache = cachep;
slab->freelist = freelist;
/*4. 初始化slab中的对象(调用构造函数,设置freelist等)
* OBJFREELIST_SLAB 模式:使用最后一个slab对象作为管理区;
*/
cache_init_objs(cachep, slab);
if (gfpflags_allow_blocking(local_flags))
local_irq_disable();
return slab;
opps1:
kmem_freepages(cachep, slab);
failed:
if (gfpflags_allow_blocking(local_flags))
local_irq_disable();
return NULL;
}