
从内核源码看 slab 内存池的创建初始化流程
在上篇文章 《细节拉满,80 张图带你一步一步推演 slab 内存池的设计与实现 》中,笔者从 slab cache 的总体架构演进角度以及 slab cache 的运行原理角度为大家勾勒出了 slab cache 的总体架构视图,基于这个视图详细阐述了 slab cache 的内存分配以及释放原理。
slab cache 机制确实比较复杂,涉及到的场景又很多,大家读到这里,我想肯定会好奇或者怀疑笔者在上篇文章中所论述的那些原理的正确性,毕竟 talk is cheap ,所以为了让大家看着安心,理解起来放心,从本文开始,我们将正式进入 show you the code 的阶段。笔者会基于内核 5.4 版本,详细为大家剖析 slab cache 在内核中的源码实现。
在上篇文章 《5. 从一个简单的内存页开始聊 slab》和 《6. slab 的总体架构设计》小节中,笔者带大家从一个最简单的物理内存页开始,一步一步演进 slab cache 的架构,最终得到了一副 slab cache 完整的架构图:
在本文的内容中,笔者会带大家到内核源码实现中,来看一下 slab cache 在内核中是如何被一步一步创建出来的,以及内核是如何安排 slab 对象在内存中的布局的。
我们先以内核创建 slab cache 的接口函数 kmem_cache_create 为起点,来一步一步揭秘 slab cache 的创建过程。
kmem_cache_create 接口中的参数,是由用户指定的关于 slab cache 的一些核心属性,这些属性值与我们在前文《细节拉满,80 张图带你一步一步推演 slab 内存池的设计与实现》 的《6.1 slab 的基础信息管理》小节中介绍 struct kmem_cache 结构的相应属性一一对应,在创建 slab cache 的过程中,内核会将 kmem_cache_create 接口中参数指定的值一一赋值到 struct kmem_cache 结构中。
slab cache 的整个创建过程其实是封装在 kmem_cache_create_usercopy 函数中,kmem_cache_create 直接调用了该函数,并将创建参数透传过去。
内核提供 kmem_cache_create_usercopy 函数的目的其实是为了防止 slab cache 中管理的内核核心对象被泄露,通过 useroffset 和 usersize 两个变量来指定内核对象内存布局区域中 useroffset 到 usersize 的这段内存区域可以被复制到用户空间中,其他区域则不可以。
在 Linux 内核初始化的过程中会提前为内核核心对象创建好对应的 slab cache,比如:在内核初始化函数 start_kernel 中调用 fork_init 函数为 struct task_struct 创建其所属的 slab cache —— task_struct_cachep。
在 fork_init 中就调用了 kmem_cache_create_usercopy 函数来创建 task_struct_cachep,同时指定 task_struct 对象中 useroffset 到 usersize 这段内存区域可以被复制到用户空间。例如:通过 ptrace 系统调用访问进程的 task_struct 结构时,只能访问 task_struct 对象 useroffset 到 usersize 的这段区域。
在创建 slab cache 的开始,内核为了保证整个创建过程是并发安全的,所以需要先获取一系列的锁,比如:
- 获取 cpu_hotplug_lock,mem_hotplug_lock 来防止在创建 slab cache 的过程中 cpu 或者内存进行热插拔。
- 防止 memory group 相关的 caches array 被修改,cgroup 相关的不是本文重点,这里简单了解一下即可。
- 内核中使用一个全局的双向链表来串联起系统中所有的 slab cache,这里需要获取全局链表 list 的锁,防止并发对 list 进行修改。
在确保 slab cache 的整个创建过程并发安全之后,内核会首先校验 kmem_cache_create 接口函数传递进来的那些创建参数的合法有效性。
比如,kmem_cache_sanity_check 函数中会确保 slab cache 的创建过程不能在中断上下文中进行,如果进程所处的上下文为中断上下文,那么内核就会返回 -EINVAL
错误停止 slab cache 的创建。因为中断处理程序是不会被内核重新调度的,这就导致处于中断上下文的操作必须是原子的,不能睡眠,不能阻塞,更不能持有锁等同步资源。而 slab cache 的创建并不是原子的,内核需要确保整个创建过程不能在中断上下文中进行。
除此之外 kmem_cache_sanity_check 函数还需要校验用户传入的 name 和 对象大小 object size 的有效性,确保 object size 在有效范围: 8 字节到 4M 之间。
最后内核会校验传入的 slab cache 管理标志位 slab_flags_t 的合法性,确保 slab_flags_t 在内核规定的有效标志集合中:
随后 flags &= CACHE_CREATE_MASK
初始化 slab_flags_t 标志位:
在校验完各项创建参数的有效性之后,按照常理来说就应该进入 slab cache 的创建流程了,但是现在还没到创建的时候,内核的理念是尽最大可能复用系统中已有的 slab cache。
在 __kmem_cache_alias 函数中,内核会遍历系统中 slab cache 的全局链表 list,试图在系统现有 slab cache 中查找到一个各项核心参数与我们指定的创建参数贴近的 slab cache。比如,系统中存在一个 slab cache 它的各项核心参数,object size,align,slab_flags_t 和我们指定的创建参数非常贴近。
这样一来内核就不需要重复创建新的 slab cache 了,直接复用原有的 slab cache 即可,将我们指定的 name 作为原有 slab cache 的别名。
如果找不到这样一个可以被复用的 slab cache,那么内核就会调用 create_cache 开始创建 slab cache 流程。
以上是 slab cache 创建的总体框架流程,接下来,我们来详细看下创建流程中涉及到的几个核心函数。
1. __kmem_cache_alias
__kmem_cache_alias 函数的核心是在 find_mergeable 方法中,内核在 find_mergeable 方法里边会遍历 slab cache 的全局链表 list,查找与当前创建参数贴近可以被复用的 slab cache。
一个可以被复用的 slab cache 需要满足以下四个条件:
- 指定的 slab_flags_t 相同。
- 指定对象的 object size 要小于等于已有 slab cache 中的对象 size (kmem_cache->size)。
- 如果指定对象的 object size 与已有 kmem_cache->size 不相同,那么它们之间的差值需要再一个 word size 之内。
- 已有 slab cache 中的 slab 对象对齐 align (kmem_cache->align)要大于等于指定的 align 并且可以整除 align 。
如果通过 find_mergeable 在现有系统中所有 slab cache 中找到了一个可以复用的 slab cache,那么就不需要在创建新的了,直接返回已有的 slab cache 就可以了。
但是在返回之前,需要更新一下已有 slab cache 结构 kmem_cache 中的相关信息:
- 增加原有 slab cache 的引用计数 refcount++。
- slab cache 中的 object size 更新为我们在创建参数中指定的 object size 与原有 object size 之间的最大值。
- slab cache 中的 inuse 也是更新为原有 kmem_cache->inuse 与我们指定的对象 object size 与 word size 对齐之后的最大值。
最后调用 sysfs_slab_alias 在 sys 文件系统中创建一个这样的目录 /sys/kernel/slab/name
,name 就是 kmem_cache_create 接口函数传递过来的参数,表示要创建的 slab cache 名称。
系统中的所有 slab cache 都会在 sys 文件系统中有一个专门的目录:/sys/kernel/slab/<cachename>
,该目录下的所有文件都是 read only 的,每一个文件代表 slab cache 的一项运行时信息,比如:
-
/sys/kernel/slab/<cachename>/align
文件标识该 slab cache 中的 slab 对象的对齐 align -
/sys/kernel/slab/<cachename>/alloc_fastpath
文件记录该 slab cache 在快速路径下分配的对象个数 -
/sys/kernel/slab/<cachename>/alloc_from_partial
文件记录该 slab cache 从本地 cpu 缓存 partial 链表中分配的对象次数 -
/sys/kernel/slab/<cachename>/alloc_slab
文件记录该 slab cache 从伙伴系统中申请新 slab 的次数 -
/sys/kernel/slab/<cachename>/cpu_slabs
文件记录该 slab cache 的本地 cpu 缓存中缓存的 slab 个数 -
/sys/kernel/slab/<cachename>/partial
文件记录该 slab cache 在每个 NUMA 节点缓存 partial 链表中的 slab 个数 -
/sys/kernel/slab/<cachename>/objs_per_slab
文件记录该 slab cache 中管理的 slab 可以容纳多少个对象。
该目录下还有很多文件笔者就不一一列举了,但是我们可以看到 /sys/kernel/slab/<cachename>
目录下的文件描述了对应 slab cache 非常详细的运行信息。前边我们介绍的 cat /proc/slabinfo
命名输出的信息就来源于 /sys/kernel/slab/<cachename>
目录下的各个文件。
由于我们当前并没有真正创建一个新的 slab cache,而是复用系统中已有的 slab cache,但是内核需要让用户感觉上已经按照我们指定的创建参数创建了一个新的 slab cache,所以需要为我们要创建的 slab cache 也单独在 sys 文件系统中创建一个 /sys/kernel/slab/name
目录,但是该目录下的文件需要软链接到原有 slab cache 在 sys 文件系统对应目录下的文件。
这就相当于给原有 slab cache 起了一个别名,这个别名就是我们指定的 name,但是 /sys/kernel/slab/name
目录下的文件还是用的原有 slab cache 的。
我们可以通过 /sys/kernel/slab/<cachename>/aliases
文件查看该 slab cache 的所有别名个数,也就是说有多少个 slab cache 复用了该 slab cache 。
1.1 find_mergeable 查找可被复用的 slab cache
一个可以被复用的 slab cache 需要满足以下四个条件:
- 指定的 slab_flags_t 相同。
- 指定对象的 object size 要小于等于已有 slab cache 中的对象 size (kmem_cache->size)。
- 如果指定对象的 object size 与已有 kmem_cache->size 不相同,那么它们之间的差值需要再一个 word size 之内。
- 已有 slab cache 中的 slab 对象对齐 align (kmem_cache->align)要大于等于指定的 align 并且可以整除 align 。
1.2 calculate_alignment 综合计算出一个合理的对齐 align
事实上,内核并不会完全按照我们指定的 align 进行内存对齐,而是会综合考虑 cpu 硬件 cache line 的大小,以及 word size 计算出一个合理的 align 值。
内核在对 slab 对象进行内存布局的时候,会按照这个最终的 align 进行内存对齐。
2. create_cache 开始正式创建 slab cache
在前文《细节拉满,80 张图带你一步一步推演 slab 内存池的设计与实现》 中的 《6.2 slab 的组织架构》小节中,为大家介绍的 slab cache 的整体架构就是在 create_cache 函数中搭建完成的。
create_cache 函数的主要任务就是为 slab cache 创建它的内核数据结构 struct kmem_cache,并为其填充我们在前文 《6.1 slab 的基础信息管理》小节中介绍的关于 struct kmem_cache 相关的属性。
随后内核会为其创建 slab cache 的本地 cpu 结构 kmem_cache_cpu,每个 cpu 对应一个这样的缓存结构。
最后为 slab cache 创建 NUMA 节点缓存结构 kmem_cache_node,每个 NUMA 节点对应一个。
当 slab cache 的整个骨架被创建出来之后,内核会为其在 sys 文件系统中创建 /sys/kernel/slab/name
目录节点,用于详细记录该 slab cache 的运行状态以及行为信息。
最后将新创建出来的 slab cache 添加到全局双向链表 list 的末尾。下面我们来一起看下这个创建过程的详细实现。
内核中的每个核心数据结构都会有其专属的 slab cache 来管理,比如,笔者在本文 《3. slab 对象池在内核中的应用场景》小节介绍的 task_struct,mm_struct,page,file,socket 等等一系列的内核核心数据结构。
而这里的 slab cache 的数据结构 struct kmem_cache 同样也属于内核的核心数据结构,它也有其专属的 slab cache 来专门管理 kmem_cache 对象的分配与释放。
内核在启动阶段,会专门为 struct kmem_cache 创建其专属的 slab cache,保存在全局变量 kmem_cache 中。
同理,slab cache 的 NUMA 节点缓存 kmem_cache_node 结构也是如此,内核也会为其创建一个专属的 slab cache,保存在全局变量 kmem_cache_node 中。
在 create_cache 函数的开始,内核会从 kmem_cache 专属的 slab cache 中申请一个 kmem_cache 对象。
然后用我们在 kmem_cache_create 接口函数中指定的参数初始化 kmem_cache 对象。
随后会在 __kmem_cache_create 函数中近一步初始化 kmem_cache 对象的其他重要属性。比如,初始化 slab 对象的内存布局相关信息,计算 slab 所需要的物理内存页个数以及所能容纳的对象个数,创建初始化 cpu 本地缓存结构以及 NUMA 节点的缓存结构。
最后将刚刚创建出来的 slab cache 加入到 slab cache 在内核中的全局链表 list 中管理
3. __kmem_cache_create 初始化 kmem_cache 对象
__kmem_cache_create 函数的主要工作就是建立 slab cache 的基本骨架,包括初始化 kmem_cache 结构中的其他重要属性,创建初始化本地 cpu 缓存结构以及 NUMA 节点缓存结构,这一部分的重要工作封装在 kmem_cache_open 函数中完成。
随后会检查内核 slab allocator 整个体系的状态,只有 slab_state = FULL
的状态才表示整个 slab allocator 体系已经在内核中建立并初始化完成了,可以正常运转了。
通过 slab allocator 的状态检查之后,就是 slab cache 整个创建过程的最后一步,利用 sysfs_slab_add 为其在 sys 文件系统中创建 /sys/kernel/slab/name
目录,该目录下的文件详细记录了 slab cache 运行时的各种信息。
4. slab allocator 整个体系的状态变迁
__kmem_cache_create 函数的整个逻辑还是比较好理解的,这里唯一不好理解的就是 slab allocator 整个体系的状态 slab_state。
只有 slab_state 为 FULL 状态的时候,才代表 slab allocator 体系能够正常运转,包括这里的创建 slab cache,以及后续从 slab cache 分配对象,释放对象等操作。
只要 slab_state 不是 FULL 状态,slab allocator 体系就是处于半初始化状态,下面笔者就为大家介绍一下 slab_state 的状态变迁流程,这里大家只做简单了解,因为随着后续源码的深入,笔者还会在相关章节重复提起。
在内核没有启动的时候,也就是 slab allocator 体系完全没有建立的情况下,slab_state 的初始化状态就是 DOWN
。
当内核启动的过程中,会开始创建初始化 slab allocator 体系,第一步就是为 struct kmem_cache_node 结构创建其专属的 slab cache —— kmem_cache_node
。后续再创建新的 slab cache 的时候,其中的 NUMA 节点缓存结构就是从 kmem_cache_node
里分配。
当 kmem_cache_node 专属的 slab cache 创建完毕之后, slab_state 的状态就变为了 PARTIAL
。
slab allocator 体系建立的最后一项工作,就是创建 kmalloc 内存池体系,kmalloc 体系成功创建之后,slab_state 的状态就变为了 UP
,其实现在 slab allocator 体系就可以正常运转了,但是还不是最终的理想状态。
当内核的初始化工作全部完成的时候,会在 arch_call_rest_init
函数中调用 do_initcalls()
,开启内核的 initcall 阶段。
在内核的 initcall 阶段,会调用内核中定义的所有 initcall,而建立 slab allocator 体系的最后一项工作就为其在 sys 文件系统中创建 /sys/kernel/slab
目录节点,这里会存放系统中所有 slab cache 的详细运行信息。
这一项工作就封装在 slab_sysfs_init
函数中,而 slab_sysfs_init 在内核中被定义成了一个 __initcall 函数。
当 /sys/kernel/slab
目录节点被创建之后,在 slab_sysfs_init 函数中会将 slab_state 变为 FULL。至此内核中的 slab allocator 整个体系就全部建立起来了。
5. 初始化 slab cache 的核心函数 kmem_cache_open
kmem_cache_open 是初始化 slab cache 内核数据结构 kmem_cache 的核心函数,在这里会初始化 kmem_cache 结构中的一些重要核心参数,以及为 slab cache 创建初始化本地 cpu 缓存结构 kmem_cache_cpu 和 NUMA 节点缓存结构 kmem_cache_node。
经历过 kmem_cache_open 之后,如下图所示的 slab cache 的整个骨架就全部创建出来了。
calculate_sizes 函数中封装了 slab 对象内存布局的全部逻辑,笔者在上篇文章《细节拉满,80 张图带你一步一步推演 slab 内存池的设计与实现》 中的 《5. 从一个简单的内存页开始聊 slab》小节中介绍的内容,背后的实现逻辑全部封装在此。
除了确定 slab 对象的内存布局之外,calculate_sizes 函数还会初始化 kmem_cache 的其他核心参数:
在完成了对 kmem_cache 结构的核心属性初始化工作之后,内核紧接着会调用 set_min_partial
来设置 kmem_cache->min_partial
,从而限制 slab cache 在 numa node 中缓存的 slab 个数上限。
调用 set_cpu_partial
来设置 kmem_cache->cpu_partial
,从而限制 slab cache 在 cpu 本地缓存 partial 链表中空闲对象个数的上限。
最后调用 init_kmem_cache_nodes
函数为 slab cache 在每个 NUMA 节点中创建其所属的缓存结构 kmem_cache_node。
调用 alloc_kmem_cache_cpus
函数为 slab cache 创建每个 cpu 的本地缓存结构 kmem_cache_cpu。
现在 slab cache 的整个骨架就被完整的创建出来了,下面我们来看一下这个过程中涉及到的几个核心函数。
6. slab 对象的内存布局
在上篇文章《细节拉满,80 张图带你一步一步推演 slab 内存池的设计与实现》的《5. 从一个简单的内存页开始聊 slab》小节的内容介绍中,笔者详细的为大家介绍了 slab 对象的内存布局,本小节,我们将从内核源码实现角度再来谈一下 slab 对象的内存布局,看一下内核是如何具体规划 slab 对象的内存布局的。
再开始本小节的内容之前,笔者建议大家先去回顾下前文第五小节的内容。
在内核对 slab 对象开始内存布局之前,为了提高 cpu 访问对象的速度,首先需要将 slab 对象的 object size 与 word size 进行对齐。如果 object size 与 word size 本来就是对齐的,那么内核不会做任何事情。如果不是对齐的,那么就需要在对象后面填充一些字节,达到与 word size 对齐的目的。
如果我们设置了 SLAB_RED_ZONE,表示需要再对象 object size 内存区域前后各插入一段 red zone 区域,目的是为了防止内存的读写越界。
如果对象 object size 与 word size 本来就是对齐的,并没有填充任何字节:size == s->object_size
,那么此时就需要在对象 object size 内存区域的后面插入一段 word size 大小的 red zone。
如果对象 object size 与 word size 不是对齐的,那么内核就会在 object size 区域后面填充字节达到与 word size 对齐的目的,而这段填充的字节恰好可以作为对象右侧 red zone ,而不需要额外为右侧 red zone 分配内存空间。
如果我们设置了 SLAB_POISON 或者开启了 RCU 或者设置了对象的构造函数,它们都会占用对象的实际内存区域 object size。
比如我们设置 SLAB_POISON 之后, slab 对象的 object size 内存区域会被内核用特殊字符 0x6b 填充,并用 0xa5 填充对象 object size 内存区域的最后一个字节表示填充完毕。
这样一来,用于指向下一个空闲对象的 freepointer 就没地方存放了,所以需要在当前对象内存区域的基础上再额外开辟一段 word size 大小的内存区域专门存放 freepointer。
除此之外,对象的 freepointer 指针就会放在对象本身内存区域 object size 中,因为在对象被分配出去之前,用户根本不会关心对象内存里到底存放的是什么。
如果我们设置了 SLAB_STORE_USER,表示我们期望跟踪 slab 对象的分配与释放相关的信息,而这些跟踪信息内核使用一个 struct track 结构来存储。
所以在这种情况下,内核需要在目前 slab 对象的内存区域后面额外增加两个 sizeof(struct track)
大小的区域出来,用来分别存储 slab 对象的分配和释放信息。
如果我们设置了 SLAB_RED_ZONE,最后,还需要再 slab 对象内存区域的左侧填充一段 red_left_pad 大小的内存区域作为左侧 red zone。另外还需要再 slab 对象内存区域的末尾再次填充一段 word size 大小的内存区域作为 padding 部分。
右侧 red zone,在本小节开始的地方已经被填充了。
现在关于 slab 对象内存布局的全部内容,我们就介绍完了,最终我们得到了 slab 对象真实占用内存大小 size,内核会根据这个 size,在物理内存页中划分出一个一个的对象出来。
那么一个 slab 到底需要多少个物理内存页呢?内核会通过 calculate_order 函数根据一定的算法计算出一个合理的 order 值。这个过程笔者后面会细讲,现在我们主要关心整体流程。
slab 所需的物理内存页个数计算出来之后,内核会根据 slab 对象占用内存的大小 size,计算出一个 slab 可以容纳的对象个数。并将这个结果保存在 kmem_cache 结构中的 oo
属性中。
内核会通过 struct kmem_cache_order_objects 这样一个结构来保存 slab 所需要的物理内存页个数以及 slab 所能容纳的对象个数,其中 kmem_cache_order_objects 的高 16 位保存 slab 所需要的物理内存页个数,低 16 位保存 slab 所能容纳的对象个数。
随后内核会通过 get_order 函数来计算,容纳一个 size 大小的对象所需要的最少物理内存页个数。用这个值作为 kmem_cache 结构中的 min 属性。
内核在创建 slab 的时候,最开始会按照 oo 指定的尺寸来向伙伴系统申请内存页,如果内存紧张,申请内存失败。那么内核会降级采用 min 的尺寸再次向伙伴系统申请内存。也就是说 slab 中至少会包含一个对象。
最后会设置 max 的值,从源码中我们可以看到 max 的值与 oo 的值是相等的
到现在为止,笔者在本文 《6.1 slab 的基础信息管理》小节中介绍的 kmem_cache 结构相关的重要属性就全部设置完成了。
7. 计算 slab 所需要的 page 个数
一个 slab 究竟需要多少个物理内存页就是在这里计算出来的,这里内核会根据一定的算法,尽量保证 slab 中的内存碎片最小化,综合计算出一个合理的 order 值。下面我们来一起看下这个计算逻辑:
首先内核会计算出 slab 需要容纳对象的最小个数 min_objects,计算公式: min_objects = 4 * (fls(nr_cpu_ids) + 1)
:
- nr_cpu_ids 表示当前系统中的 cpu 个数
- fls 获取参数二进制形式的最高有效 bit 的位数,比如 fls(0)=0,fls(1)=1,fls(4) = 3
这里我们看到 min_objects 是和当前系统中的 cpu 个数有关系的。
内核规定 slab 所需要的物理内存页个数的最大值 slub_max_order 初始化为 3,也就是 slab 最多只能向伙伴系统中申请 8 个内存页。
根据这里的 slub_max_order 和 slab 对象的 size 通过 order_objects 函数计算出 slab 所能容纳对象的最大值。
slab 所能容纳的对象个数越多,那么所需要的物理内存页就越多,slab 所能容纳的对象个数越少,那么所需要的物理内存页就越少。
内核通过刚刚计算出的 min_objects 可以计算出 slab 所需要的最小内存页个数,我们暂时称为 min_order。
随后内核会遍历 min_order 与 slub_max_order 之间的所有 order 值,直到找到满足内存碎片限制要求的一个 order。
那么内核对于内存碎片限制的要求具体如何定义呢?
内核会定义一个 fraction 变量作为 slab 内存碎片的控制系数,内核要求 slab 中内存碎片大小不能超过 (slab所占内存大小 / fraction)
,fraction 的值越大,表示 slab 中所能容忍的内存碎片就越小。fraction 的初始值为 16。
在内核寻找最佳合适 order 的过程中,最高优先级是要将内存碎片控制在一个非常低的范围内,在这个基础之上,遍历 min_order 与 slub_max_order 之间的所有 order 值,看他们产生碎片的大小是否低于 (slab所占内存大小 / fraction)
的要求。如果满足,那么这个 order 就是最终的计算结果,后续 slab 会根据这个 order 值向伙伴系统申请物理内存页。这个逻辑封装在 slab_order 函数中。
如果内核遍历完一遍 min_order 与 slub_max_order 之间的所有 order 值均不符合内存碎片限制的要求,那么内核只能尝试放宽对内存碎片的要求,将 fraction 调小一些——fraction /= 2
,再次重新遍历所有 order。但 fraction 系数最低不能低于 4。
如果 fraction 系数低于 4 了,说明内核已经将碎片限制要求放到最宽了,在这么宽松的条件下依然无法找到一个满足限制要求的 order 值,那么内核会在近一步的降级,放宽对 min_objects 的要求——min_objects--
,尝试在 slab 中少放一些对象。fraction 系数恢复为 16,在重新遍历,尝试查找符合内存碎片限制要求的 order 值。
最极端的情况就是,无论内核怎么放宽对内存碎片的限制,无论怎么放宽 slab 中容纳对象的最小个数要求,内核始终无法找到一个 order 值能够满足如此宽松的内存碎片限制条件。当 min_objects == 1 的时候就会退出 while (min_objects > 1)
循环停止寻找。
最终内核的托底方案是将 min_objects 调整为 1,fraction 调整为 1,再次调用 slab_order ,这里的语义是:在这种极端的情况下,slab 中最少只能容纳一个对象,那么内核就分配容纳一个对象所需要的内存页。
如果 slab 对象太大了,有可能突破了 slub_max_order = 3 的限制,内核会近一步放宽至 MAX_ORDER = 11,这里我们可以看出内核的决心,无论如何必须保证 slab 中至少容纳一个对象。
下面是 slab_order 函数的逻辑,它是整个计算过程的核心:
get_order(size)
函数的逻辑就比较简单了,它不会像 calculate_order 函数那样复杂,不需要考虑内存碎片的限制。它的逻辑只是简单的计算分配一个 size 大小的对象所需要的最少内存页个数,用于在 calculate_sizes 函数的最后计算 kmem_cache 结构的 min 值。
get_order 函数的计算逻辑如下:
- 如果给定的 size 在 [0,PAGE_SIZE] 之间,那么 order = 0 ,需要一个内存页面即可。
- size 在 [PAGE_SIZE + 1, 2^1 * PAGE_SIZE] 之间, order = 1
- size 在 [2^1 * PAGE_SIZE + 1, 2^2 * PAGE_SIZE] 之间, order = 2
- size 在 [2^2 * PAGE_SIZE + 1, 2^3 * PAGE_SIZE] 之间, order = 3
- size 在 [2^3 * PAGE_SIZE + 1, 2^4 * PAGE_SIZE] 之间, order = 4
现在,一个 slab 所需要的内存页个数的计算过程,笔者就为大家交代完毕了,下面我们来看一下 kmem_cache 结构的其他属性的初始化过程。
8. set_min_partial
该函数的主要目的是为了计算 slab cache 在 NUMA 节点缓存 kmem_cache_node->partial 链表中的 slab 个数上限,超过该值,空闲的 empty slab 则会被回收至伙伴系统中。
kmem_cache 结构中的 min_partial 初始值为 min = ilog2(s->size) / 2
,需要保证 min_partial 的值在 [5,10] 的范围之内。
9. set_cpu_partial
这里会设置 kmem_cache 结构的 cpu_partial 属性,该值限制了 slab cache 在 cpu 本地缓存的 partial 列表中所能容纳的最大空闲对象个数。
同时该值也决定了当 kmem_cache_cpu->partial 链表为空时,内核会从 kmem_cache_node->partial 链表填充 cpu_partial / 2
个 slab 到 kmem_cache_cpu->partial 链表中。相关详细内容可回顾上篇文章《细节拉满,80 张图带你一步一步推演 slab 内存池的设计与实现》 中的 《7.3 从 NUMA 节点缓存中分配》小节。
set_cpu_partial 函数的逻辑也很简单,就是根据上篇文章 《6 slab 对象的内存布局》小节中计算出的 slab 对象 size 大小来决定 cpu_partial 的值。
10. init_kmem_cache_nodes
到现在为止,kmem_cache 结构中的所有重要属性就已经初始化完毕了,slab cache 的创建过程也进入了尾声,最后内核需要为 slab cache 创建本地 cpu 缓存结构以及 NUMA 节点缓存结构
本小节的主要内容就是内核如何为 slab cache 创建其在 NUMA 节点中的缓存结构 :
slab cache 在每个 NUMA 节点中都有自己的缓存结构 kmem_cache_node,init_kmem_cache_nodes 函数需要遍历所有的 NUMA 节点,并利用 struct kmem_cache_node 专属的 slab cache —— 全局变量 kmem_cache_node,分配一个 kmem_cache_node 对象,并调用 init_kmem_cache_node 对其进行初始化。
11. alloc_kmem_cache_cpus
这里主要是为 slab cache 创建其 cpu 本地缓存结构 kmem_cache_cpu,每个 cpu 一个这样的结构,并调用 per_cpu_ptr
将创建好的 kmem_cache_cpu 结构与对应的 cpu 相关联初始化。
至此,slab cache 的整个骨架就全部被创建出来了,最终得到的 slab cache 完整架构如下图所示:
最后,我们可以结合上面的 slab cache 架构图与下面这副 slab cache 创建流程图加以对比,回顾总结。
12. 内核第一个 slab cache 是如何被创建出来的
在上小节介绍 slab cache 的创建过程中,笔者其实暗暗地埋下了一个伏笔,不知道,大家有没有发现,在 slab cache 创建的过程中需要创建两个特殊的数据结构:
- 一个是 slab cache 自身的管理结构 struct kmem_cache。
- 另一个是 slab cache 在 NUMA 节点中的缓存结构 struct kmem_cache_node。
而 struct kmem_cache 和 struct kmem_cache_node 同样也都是内核的核心数据结构,他俩各自也有一个专属的 slab cache 来管理 kmem_cache 对象和 kmem_cache_node 对象的分配与释放。
slab cache 的 cpu 本地缓存结构 struct kmem_cache_cpu 是一个 percpu 类型的变量,由
__alloc_percpu
直接创建,并不需要一个专门的 slab cache 来管理。
在 slab cache 的创建过程中,内核首先需要向 struct kmem_cache 结构专属的 slab cache 申请一个 kmem_cache 对象。
当 kmem_cache 对象初始化完成之后,内核需要向 struct kmem_cache_node 结构专属的 slab cache 申请一个 kmem_cache_node 对象,作为 slab cache 在 NUMA 节点中的缓存结构。
那么问题来了,kmem_cache
和 kmem_cache_node
这两个 slab cache 是怎么来的?
因为他俩本质上是一个 slab cache,而 slab cache 的创建又需要 kmem_cache
(slab cache)和 kmem_cache_node
(slab cache),当系统中第一个 slab cache 被创建的时候,此时并没有 kmem_cache
(slab cache),也没有 kmem_cache_node
(slab cache),这就变成死锁了,是一个先有鸡还是先有蛋的问题。
那么内核是如何来解决这个先有鸡还是先有蛋的问题呢?让我们先把思绪拉回到内核启动的阶段~~~
12.1 slab allocator 体系的初始化
内核启动的核心初始化逻辑封装 /init/main.c
文件的 start_kernel 函数中,在这里会初始化内核的各个子系统,内存管理子系统的初始化工作就在这里,封装在 mm_init 函数里。
在 mm_init 函数中会初始化内核的 slab allocator 体系 —— kmem_cache_init()。
而内核解决这个 “先有鸡还是先有蛋” 问题的秘密就藏在 /mm/slub.c
文件的 kmem_cache_init 函数中。
内核首先会定义两个静态的 static __initdata struct kmem_cache
结构 boot_kmem_cache,boot_kmem_cache_node ,用于在内核初始化内存管理子系统的时候临时静态地创建 kmem_cache(slab cache)和 kmem_cache_node (slab cache)所需要的 struct kmem_cache 和 struct kmem_cache_node 结构。
这样一来,内核就通过这两个临时的静态 kmem_cache 结构 :boot_kmem_cache,boot_kmem_cache_node 打破了死锁的循环等待条件。
当这两个临时的 boot_kmem_cache,boot_kmem_cache_node 被创建初始化之后,随后内核会通过 bootstrap 将这两个临时 slab cache 深拷贝到全局变量 kmem_cache(slab cache)和 kmem_cache_node (slab cache)中。
从此,内核就有了正式的 kmem_cache(slab cache)和 kmem_cache_node (slab cache),后续就可以按照正常流程动态地创建 slab cache 了,正常的创建流程就是笔者在本文前边几个小节中为大家介绍的内容。
下面我们来一起看下 slab allocator 体系的初始化过程:
初始化 slab allocator 体系的核心就是如何静态的创建和初始化这两个静态的 slab cache: boot_kmem_cache,boot_kmem_cache_node。
这个核心逻辑封装在 create_boot_cache 函数中,大家需要注意该函数第一个参数 struct kmem_cache *s
,参数 s 指向的是上面两个临时的静态的 slab cache。现在是内核初始化阶段,当前系统中并不存在一个正式完整的 slab cache,这一点大家在阅读本小节的时候要时刻注意。
这里在对静态 kmem_cache 结构进行简单初始化之后,内核又调用了 __kmem_cache_create 函数,这个函数我们已经非常熟悉了,忘记的同学可以回看下本文的 《3. __kmem_cache_create 初始化 kmem_cache 对象》小节。
__kmem_cache_create 函数的主要工作就是建立 slab cache 的基本骨架,包括初始化 kmem_cache 结构中的其他重要核心属性,创建初始化本地 cpu 缓存结构以及 NUMA 节点缓存结构。
这里我们来重点看下 init_kmem_cache_nodes 函数,在内核初始化静态 boot_kmem_cache_node(静态 slab cache)的场景下,这里的流程逻辑与 《10. init_kmem_cache_nodes》小节中介绍的会有所不同。
在 slab allocator 体系中,第一个被创建出来的 slab cache 就是这里的 boot_kmem_cache_node,当前 slab_state == DOWN
。当前流程正在创建初始化 boot_kmem_cache_node,所以目前内核无法利用 boot_kmem_cache_node 来动态的分配 kmem_cache_node 对象。
所以当创建初始化 boot_kmem_cache_node 的时候,流程会进入 if (slab_state == DOWN)
在 slab allocator 体系中,第二个被创建出来的 slab cache 就 boot_kmem_cache,在创建初始化 boot_kmem_cache 的时候,slab_state 就变为了 PARTIAL,表示 kmem_cache_node 结构的专属 slab cache 已经创建出来了,可以利用它来动态分配 kmem_cache_node 对象了。
12.2 kmem_cache_node 结构的临时静态创建
正如前面小节中所介绍的,在 slab allocator 体系中第一个被内核创建出来的 slab cache 正是 boot_kmem_cache_node,而它本身就是一个 slab cache,专门用于分配 kmem_cache_node 对象。
而创建一个 slab cache 最核心的就是要为其分配 struct kmem_cache 结构 ( slab cache 在内核中的数据结构)还有就是 slab cache 在 NUMA 节点的缓存结构 kmem_cache_node。
而针对 struct kmem_cache 结构内核已经通过定义静态结构 boot_kmem_cache_node 解决了。
而针对 kmem_cache_node 结构,内核中既没有定义这样一个静态数据结构,也没有一个 slab cache 专门管理,所以内核会通过这里的 early_kmem_cache_node_alloc 函数来创建 kmem_cache_node 对象。
注意:这里是为 boot_kmem_cache_node 这个静态的 slab cache 初始化它的 NUMA 节点缓存数组。
当 boot_kmem_cache_node 被初始化之后,它的整个结构如下图所示:
12.3 将临时静态的 kmem_cache 结构变为正式的 slab cache
流程到这里 boot_kmem_cache,boot_kmem_cache_node 这两个静态结构就已经被初始化好了,现在内核就可以通过他们来动态的创建 kmem_cache 对象和 kmem_cache_node 对象了。
但是这里的 boot_kmem_cache 和 boot_kmem_cache_node 只是临时的 kmem_cache 结构,目的是在 slab allocator 体系初始化的时候用于静态创建 kmem_cache (slab cache), kmem_cache_node (slab cache)。
既然是临时的结构,所以这里需要创建两个最终的全局 kmem_cache 结构,然后将这两个静态临时结构深拷贝到最终的全局 kmem_cache 结构中。
12.4 为什么要先创建 boot_kmem_cache_node 而不是 boot_kmem_cache
现在关于 slab alloactor 体系的初始化流程笔者就为大家全部介绍完了,最后我们借用这个问题,再对这个流程做一个简单的总体回顾。
首先 slab cache 创建要依赖两个核心的数据机构,kmem_cache,kmem_cache_node:
其中 kmem_cache 结构是 slab cache 在内核中的数据结构,同样也需要被一个专门的 slab cache 所管理,但是在内核初始化阶段 slab 体系还未建立,所以内核通过定义两个局部静态变量来解决 kmem_cache 结构的创建问题。
随后内核会在 calculate_size 函数中初始化 struct kmem_cache 结构中的核心属性。详细内容可回顾上篇文章的 《6 slab 对象的内存布局》小节的内容。
现在 kmem_cache 结构的问题解决了,但是这两个 slab cache 中的 kmem_cache_node 结构的问题又来了。
所以内核决定首先创建 boot_kmem_cache_node,并通过 early_kmem_cache_node_alloc 函数为 boot_kmem_cache_node 创建 kmem_cache_node 结构。
当 boot_kmem_cache_node 被创建出来之后,内核就可以动态的分配 kmem_cache_node 对象了。
所以最后创建 boot_kmem_cache,在遇到 kmem_cache_node 结构创建的时候,直接使用 boot_kmem_cache_node 进行动态创建。
最后通过 bootstrap 将这两个临时静态的 slab cache : boot_kmem_cache,boot_kmem_cache_node 深拷贝到最终的全局 slab cache 中:
从此以后,内核就可以动态创建 slab cache 了。
总结
本文笔者基于内核 5.4 版本,从源码角度详细讨论了 slab cache 的创建初始化过程,创建流程如下图所示:
经过该流程的创建之后,我们得到了如下图所示的 slab cache 架构:
在这个过程中,笔者又近一步从源码角度介绍了内核具体是如何对 slab 对象进行内存布局的。
在这个内存布局的基础上,笔者又近一步展开了内核如何计算一个 slab 到底需要多少个物理内存页,以及一个 slab 到底能够容纳多少内存块的相关逻辑。
最后我们介绍了 slab cache 在内核中的数据结构 struct kmem_cache 里的 min_partial,cpu_partial 的计算逻辑。以及 slab cache 的 cpu 缓存结构 cpu_slab 以及 NUMA 节点缓存结构 node[MAX_NUMNODES] 的详细初始化过程。
在介绍完 slab cache 的整个创建流程之后,笔者在本文的最后一个小节里又详细的为大家介绍了整个 slab allocator 体系的初始化过程,并从源码实现上,看到了内核是如何解决这个先有鸡还是先有蛋的问题
好了,本文的内容就到这里了,在下篇文章中,笔者会带大家继续深入到内核源码中,去看一下 slab cache 是如何进行内存分配的~~~
文章转载自公众号:bin技术小屋
