简介
Jeff BonwickSolaris 2.4
除了提供小内存外,slab 分配器的第二个任务是维护常用对象的缓存。对于内核中使用的许多结构,初始化对象所需的时间可等于或超过为其分配空间的成本。当创建一个新的slab 时,许多对象将被打包到其中并使用构造函数(如果有)进行初始化。释放对象后,它会保持其初始化状态,这样可以快速分配对象。
struct fs_structsizeof(fs_struct)
SLAB分配器的最后一项任务是提高CPU硬件缓存的利用率。 如果将对象包装到SLAB中后仍有剩余空间,则将剩余空间用于为SLAB着色。 SLAB着色是一种尝试使不同SLAB中的对象使用CPU硬件缓存中不同行的方案。 通过将对象放置在SLAB中的不同起始偏移处,对象可能会在CPU缓存中使用不同的行,从而有助于确保来自同一SLAB缓存的对象不太可能相互刷新。 通过这种方案,原本被浪费掉的空间可以实现一项新功能。
sudo cat /proc/slabinfovm_area_structvm_area_struct
可以看到,系统中存在的 slab 有些形如 kmalloc-xxx 的 slab,我们称其为通用型 slab,用来满足分配通用内存。其他含有具体名字的 slab 我们称其为 专用 slab,用来为特定结构体分配内存,如 vm_area_struct、mm_struct 等。
为什么要分专用和通用 slab ? 最直观的一个原因就是通用 slab 会造成内存浪费:出于 slab 管理的方便,每个 slab 管理的对象大小都是一致的,当我们需要分配一个处于 64-96字节中间大小的对象时,就必须从保存 96 字节的 slab 中分配。而对于专用的 slab,其管理的都是同一个结构体实例,申请一个就给一个恰好内存大小的对象,这就可以充分利用空间。
slab 分配器的实现源码:
- include/linux/slab_def.h
- include/linux/slab.h
- mm/slab.c
Slab API
kmalloc()kfree()kmalloc()include/linux/slab.h__kmalloc()__do_kmalloc()
GFP_XXX
实现原理
内存缓存struct kmem_cacheinclude/linux/slab_def.h
cat /proc/slabinfomm_structfs_cachekmem_cache->nameobject_sizegfpordernum
kmem_cachekmem_cache_node
kmem_cache_nodemm/slab.h
kmem_cache_noed 记录了3种slab:
- slabs_full :已经完全分配的 slab
- slabs_partial: 部分分配的slab
- slabs_free:空slab,或者没有对象被分配
struct page
struct kmem_cache
[注意]上述例子有点不严谨,实际上操作系统启动创建kmem_cache完成后,这三个链表都为空,只有在申请对象时发现没有可用的 slab 时才会创建一个新的SLAB,并加入到这三个链表中的一个中。也就是说kmem_cache中的SLAB数量是动态变化的,当SLAB数量太多时,kmem_cache会将一些SLAB释放回页框分配器中。
struct pageinclude/linux/mm_types.h
void *s_memstruct kmem_cache *slab_cachestruct kmem_cache_nodestruct list_head slab_listvoid *freelist
空闲对象链表是一个由数组制成的简单链表,它保存的地方有两种情况[6]:
1.保存在外部,会从SLAB中分配一个对象用于保存新的SLAB的空闲对象链表。
2.保存在内部,保存在这个SLAB所代表的连续页框的头部。
在空闲对象链表都是保存在内部(通常也是保存在内部)的情况下,slab 描述符描述一块连续的内存关键结构如下:
active
Slab缓存建立过程
kmem_cache_bootmm/slab.c
kmem_cache_init()create_boot_cache()kmem_cache_bootoffsetofsizeof(struct kmem_cache_node *)
LIST_HEAD(slab_caches)mm/slab_common.c
struct kmem_cache *mm/slab_common.ckmem_cachecreate_kmalloc_cachekmem_cache_nodeINDEX_NODE kmalloc_index(sizeof(struct kmem_cache_node))
create_kmalloc_cache()kmem_cache_zalloc(kmem_cache, GFP_NOWAIT);struct kmem_cache_nodecreate_boot_cache()
此时 kmem_cache 形成的结构如下图。这部分可能有些绕,为了便于区分,此处额外添加了一个名为 vm_area_struct 的 kmem_cache。
[问题] 图中有一点不严谨的地方。按照代码逻辑来看,记录 kmem_cache_node的 kmem_cache 的 name 并不为 “kmem_cache_node”,而是 "kmalloc-sizeof(struct kmem_cache_node)",为什么这里不使用专用的 slab 来为 kmem_cache_node 来分配内存呢? 按道理有多少 kmem_cache 实例就需要多少 kmem_cache_node 实例,为什么 kmem_cache 有专用的 slab,而 kmem_cache_node 没有? 百思不得其解,望大佬赐教,万分感激!
__init proc_caches_init(void)kernel/fork.c
kmem_cachenew_kmalloc_cache()mm/slab_common.c
Slab缓存分配过程
kmem_cache_zalloc()
____cache_alloc()array_cachestruct array_cachecache_alloc_refill()array_cache
struct array_cache 本地 CPU 空闲对象链表
struct array_cache __percpu *cpu_cache;
struct array_cachemm/slab.c
- avail: 表示当前可用对象的数量
- limit:可拥有的最大对象数
- batchcount:要从 slab_list 转移进本地高速缓存对象的数量,或从本地高速缓存中转移出去的 obj 数量
- touched: 是否在收缩后访问过
- entry: 伪数组,保存释放的对象指针
本地CPU空闲对象链表在系统初始化完成后是一个空的链表,只有释放对象时才会将对象加入这个链表。链表对象个数是有限制的(最大值就是limit),链表数超过这个值时,会将 batchcount 个数的对象返回到所有CPU共享的空闲对象链表(也是这样一个结构)中。
所有 CPU 共享的空闲对象链表
struct kmem_cache_nodearray_cache
在此基础上,一个常规的对象申请流程是这样的:
- 内核首先会从本地 CPU 空闲对象链表中尝试获取一个对象用于分配:如果失败,则检查所有CPU共享的空闲对象链表链表中是否存在,并且空闲链表中是否存在空闲对象,若有就转移 batchcount 个空闲对象到本地 CPU空闲对象链表中;
- 如果第 1 步失败,就尝试从 SLAB中分配;这时如果还失败,kmem_cache会尝试从页框分配器中获取一组连续的页框建立一个新的SLAB,然后从新的SLAB中获取一个对象。
对象释放流程如下[6]: - 首先会先将对象释放到本地CPU空闲对象链表中,如果本地CPU空闲对象链表中对象过多,kmem_cache 会将本地CPU空闲对象链表中的batchcount个对象移动到所有CPU共享的空闲对象链表链表中, - 如果所有CPU共享的空闲对象链表链表的对象也太多了,kmem_cache也会把所有CPU共享的空闲对象链表链表中batchcount个数的对象移回它们自己所属的SLAB中, - 这时如果SLAB中空闲对象太多,kmem_cache会整理出一些空闲的SLAB,将这些SLAB所占用的页框释放回页框分配器中。
array_cache 填充
cache_alloc_refill()
- transfer_objects():从全局共享空闲对象中转移空闲对象到本地 CPU 空闲对象链表上
- get_first_slab() : 从 slab 中获取空闲 slab
- alloc_block() : 从空闲 slab 中获取 空闲对象,转移到 本地 CPU 空闲对象链表上
- cache_grow_begin():当 slab 中都没有空闲对象时,开始从 buddy 系统中获取内存
cache_alloc_refill()
继续 kmalloc API
kmem_cache_alloc()kernel/fork.cvm_area_alloc()vm_area_structvma = kmem_cache_alloc(vm_area_cachep, GFP_KERNEL);
__do_kmalloc()
kmalloc_slab()kmem_cachekmalloc_slab()
首先计算出在对应的 index,用于索引之前介绍的全局二维数组 kmalloc_caches。 流程如下:
size_index_elem()
fls()
参考资料
[1]. Linux kernel 5.10.20 源码