你好,我是LMOS。
上节课我们学习了伙伴系统,了解了它是怎样管理物理内存页面的。那么你自然会想到这个问题:Linux系统中,比页更小的内存对象要怎样分配呢?
带着这个问题,我们来一起看看**SLAB分配器的原理和实现。**在学习过程中,你也可以对照一下我们Cosmos的内存管理组件,看看两者的内存管理有哪些异同。
SLAB
与Cosmos物理内存页面管理器一样,Linux中的伙伴系统是以页面为最小单位分配的,现实更多要以内核对象为单位分配内存,其实更具体一点说,就是根据内核对象的实例变量大小来申请和释放内存空间,这些数据结构实例变量的大小通常从几十字节到几百字节不等,远远小于一个页面的大小。
如果一个几十字节大小的数据结构实例变量,就要为此分配一个页面,这无疑是对宝贵物理内存的一种巨大浪费,因此一个更好的技术方案应运而生,就是Slab分配器(由Sun公司的雇员Jeff Bonwick在Solaris 2.4中设计并实现)。
由于作者公开了实现方法,后来被Linux所借鉴,用于实现内核中更小粒度的内存分配。看看吧,你以为Linux很强大,真的强大吗?不过是站在巨人的肩膀上飞翔的。
走进SLAB对象
何为SLAB对象?在SLAB分配器中,它把一个内存页面或者一组连续的内存页面,划分成大小相同的块,其中这一个小的内存块就是SLAB对象,但是这一组连续的内存页面中不只是SLAB对象,还有SLAB管理头和着色区。
我画个图你就明白了,如下所示。

上图中有一个内存页面和两个内存页面的SLAB,你可能对着色区有点陌生,我来给你讲解一下。
这个着色区也是一块动态的内存块,建立SLAB时才会设置它的大小,目的是为了错开不同SLAB中的对象地址,降低硬件Cache行中的地址争用,以免导致Cache抖动效应,整个系统性能下降。
SLAB头其实是一个数据结构,但是它不一定放在保存对象内存页面的开始。通常会有一个保存SLAB管理头的SLAB,在Linux中,SLAB管理头用kmem_cache结构来表示,代码如下。
1 | struct array_cache { |
上述代码中,有多少个CPU,就会有多少个array_cache类型的变量。这种为每个CPU构造一个变量副本的同步机制,就是每CPU变量(per-cpu-variable)。array_cache结构中”entry[]”表示了一个遵循LIFO顺序的数组,”avail”和”limit”分别指定了当前可用对象的数目和允许容纳对象的最大数目。

第一个kmem_cache
第一个kmem_cache是哪里来的呢?其实它是静态定义在代码中的,如下所示。
1 | static struct kmem_cache kmem_cache_boot = { |
管理kmem_cache
我们建好了第一个kmem_cache,以后kmem_cache越来越多,而且我们并没有看到kmem_cache结构中有任何指向内存页面的字段,但在kmem_cache结构中有个保存kmem_cache_node结构的指针数组。
kmem_cache_node结构是每个内存节点对应一个,它就是用来管理kmem_cache结构的,它开始是静态定义的,初始化时建立了第一个kmem_cache结构之后,init_list函数负责一个个分配内存空间,代码如下所示。
1 | #define NUM_INIT_LISTS (2 * MAX_NUMNODES) |
我们第一次分配对象时,肯定没有对应的内存页面存放对象,那么SLAB模块就会调用cache_grow_begin函数获取内存页面,然后用获取的页面来存放对象,我们一起来看看代码。
1 | static void slab_map_pages(struct kmem_cache *cache, struct page *page,void *freelist) |
上述代码中的注释已经很清楚了,cache_grow_begin函数会为kmem_cache结构分配用来存放对象的页面,随后会调用与之对应的cache_grow_end函数,把这页面挂载到kmem_cache_node结构的链表中,并让页面指向kmem_cache结构。
这样kmem_cache_node,kmem_cache,page这三者之间就联系起来了。你再看一下后面的图,就更加清楚了。

上图中page可能是一组连续的pages,但是只会把第一个page挂载到kmem_cache_node中,同时,在slab_map_pages函数中又让page指向了kmem_cache。
但你要特别留意kmem_cache_node中的三个链表,它们分别挂载的pages,有一部分是空闲对象的page、还有对象全部都已经分配的page,以及全部都为空闲对象的page。这是为了提高分配时查找kmem_cache的性能。
SLAB分配对象的过程
有了前面对SLAB数据结构的了解,SLAB分配对象的过程你自己也能推导出来,无非是根据请求分配对象的大小,查找对应的kmem_cache结构,接着从这个结构中获取arry_cache结构,然后分配对象。
如果没有空闲对象了,就需要在kmem_cache对应的kmem_cache_node结构中查找有空闲对象的kmem_cache。如果还是没找到,最后就要分配内存页面新增kmem_cache结构了。

下面我们从接口开始了解这些过程。
SLAB分配接口
其实在Linux内核中,用的最多的是kmalloc函数,经常用于分配小的缓冲区,或者数据结构分配实例空间,这个函数就是SLAB分配接口,它是用来分配对象的,这个对象就是一小块内存空间。
下面一起来看看代码。
1 | static __always_inline void *__do_kmalloc(size_t size, gfp_t flags,unsigned long caller) |
上面代码的流程很简单,就是在__do_kmalloc函数中,查找出分配大小对应的kmem_cache结构,然后调用slab_alloc函数进行分配。可以说,slab_alloc函数才是SLAB的接口函数,但是它的参数中必须要有kmem_cache结构。
具体是如何查找的呢?我们这就来看看。
如何查找kmem_cache结构
由于SLAB的接口函数slab_alloc,它的参数中必须要有kmem_cache结构指针,指定从哪个kmem_cache结构分配对象,所以在调用slab_alloc函数之前必须给出kmem_cache结构。
我们怎么查找到它呢?这就需要调用kmalloc_slab函数了,代码如下所示。
1 | enum kmalloc_cache_type { |
从上述代码,不难发现kmalloc_caches就是个全局的二维数组,kmalloc_slab函数只是根据分配大小和分配标志计算出了数组下标,最后取出其中kmem_cache结构指针。
那么kmalloc_caches中的kmem_cache,它又是谁建立的呢?我们还是接着看代码。
1 | struct kmem_cache *__init create_kmalloc_cache(const char *name, |
到这里,__do_kmalloc函数中根据分配对象大小查找的所有kmem_cache结构,我们就建立好了,保存在kmalloc_caches数组中。下面我们再去看看对象是如何分配的。
分配对象
下面我们从slab_alloc函数开始探索对象的分配过程,slab_alloc函数的第一个参数就kmem_cache结构的指针,表示从该kmem_cache结构中分配对象。
1 | static __always_inline void *slab_alloc(struct kmem_cache *cachep, gfp_t flags, unsigned long caller) |
接口函数总是简单的,真正干活的是__do_cache_alloc函数,下面我们就来看看这个函数。
1 | static inline void *____cache_alloc(struct kmem_cache *cachep, gfp_t flags) |
上述代码中真正做事的函数是**____cache_alloc函数**,它首先获取了当前kmem_cache结构中指向array_cache结构的指针,找到它里面空闲对象的地址(如果你不懂array_cache结构,请回到SLAB对象那一小节复习),然后在array_cache结构中取出一个空闲对象地址返回,这样就分配成功了。
这个速度是很快的,如果array_cache结构中没有空闲对象了,就会调用cache_alloc_refill函数。那这个函数又干了什么呢?我们接着往下看。代码如下所示。
1 | static struct page *get_first_slab(struct kmem_cache_node *n, bool pfmemalloc) |
调用cache_alloc_refill函数的过程,主要的工作都有哪些呢?我给你梳理一下。
首先,获取了cachep所属的kmem_cache_node。
然后调用get_first_slab,获取kmem_cache_node结构还有没有包含空闲对象的kmem_cache。但是请注意,这里返回的是page,因为page会指向kmem_cache结构,page所代表的物理内存页面,也保存着kmem_cache结构中的对象。
最后,如果kmem_cache_node结构没有包含空闲对象的kmem_cache了,就必须调用cache_grow_begin函数,找伙伴系统分配新的内存页面,而且还要找第一个kmem_cache分配新的对象,来存放kmem_cache结构的实例变量,并进行必要的初始化。
这些步骤完成之后,再调用cache_grow_end函数,把刚刚分配的page挂载到kmem_cache_node结构的slabs_list链表上。因为cache_grow_begin和cache_grow_end函数在前面已经分析过了,这里不再赘述。
重点回顾
今天的内容讲完了,我来帮你梳理一下本课程的重点。
1.为了分配小于1个page的小块内存,Linux实现了SLAB,用kmem_cache结构管理page对应内存页面上小块内存对象,然后让该page指向kmem_cache,由kmem_cache_node结构管理多个page。
2.我们从Linux内核中使用的kmalloc函数入手,了解了SLAB下整个内存对象的分配过程。
到此为止,我们对SLAB的研究就告一段落了,是不是感觉和Cosmos内存管理有些相像而又不同呢?甚至我们Cosmos内存管理要更为简洁和高效。
思考题
Linux的SLAB,使用kmalloc函数能分配多大的内存对象呢?
欢迎你在留言区跟我交流互动,也欢迎你把这节课分享给你的同事、朋友,跟他一起研究SLAB相关的内容。
我是LMOS,我们下节课见!
- 搬铁少年ai 👍(5) 💬(2)
回答有的同学关于为什么是196大小的问题
这里196大小的对象,应该是专门针对256B以下小内存进行的优化,正常情况支持的对象大小都是2的n次方,2的七次方是128,8次方就是256了。所以这里在不违反缓存对其的前提下,单独支持了196大小的对象。
如果cache line 是32的话,192/32=6,也是缓存对其的,那么如果申请的内存在129到192之间时,就不必去分配256大小的对象,而是可以分配192大小的对象,可以在满足缓存对齐的前提下节省空间。
除了192,另外在2的6次方和7次方之间,也特殊支持了96b大小的对象,同样是类似的原理。
理论上能够背cache line大小整除的都可以特殊支持,只不过256以上的对象可能不常见,slab申请了特殊大小的对象却没有人用,反倒是一种浪费
2021-11-05 - neohope 👍(15) 💬(2)
一、数据结构
系统有一个全局kmem_cache_node数组,每一个kmem_cache_node结构,对应一个内存节点kmem_cache_node结构,用三个链表管理内存节点的全部kmem_cache【slab管理结构】,包括:
slabs_partial,对象部分已分配的kmem_cache结构;
slabs_full,对象全部已分配的kmem_cache结构;
slabs_free ,对象全部空闲kmem_cache结构;kmem_cache结构,slab管理头,包括:
array_cache,每个CPU一个,用于管理空闲对象。 array_cache的entry数组,用于管理这些空闲对象,出入遵循LIFO原则;
num,表示对象个数;
gfporder,表示页面的大小 (2^n);
colour,表示着色区大小。着色区,主要利用SLAB划分对象剩余的空间,让SLAB前面的几个对象,根据cache line大小进行偏移,以缓解缓存过热的问题,防止Cache地址争用,防止引起Cache抖动;此外,全局有一个slab_caches链表中,记录了系统中全部的slab
二、初始化
全局有一个kmem_cache结构,kmem_cache_boot,用于初始化
全局有一个kmem_cache_node数组结构init_kmem_cache_node,用于初始化x86_64_start_kernel->x86_64_start_reservations->start_kernel->mm_init -> kmem_cache_init
1、将变量kmem_cache指向静态变量kmem_cache_boot
2、初始化全局的init_kmem_cache_node结构
3、调用create_boot_cache,初始化kmem_cache_boot结构
4、将kmem_cache_boot其加入全局slab_caches链表中
5、调用create_kmalloc_cache,建立第一个kmem_cache,供kmalloc函数使用
6、调用init_list函数,将静态init_kmem_cache_node,替换为用kmalloc生成的kmem_cache_node
7、 调用create_kmalloc_caches,创建并初始化了全部 kmalloc_caches中的kmem_cache
路径为:kmem_cache_init->create_kmalloc_caches-> new_kmalloc_cache-> create_kmalloc_cache三、对象分配
kmalloc->__kmalloc->__do_kmalloc
->kmalloc_slab
从kmalloc_caches中,根据类型和大小,找到对应的 kmem_cache
->slab_alloc->__do_cache_alloc->____cache_alloc
1、第一级分配,如果array_cache.entry中有空闲对象,直接分配
2、如果一级分配失败,调用cache_alloc_refill,进行第二级分配
->->cache_alloc_refill从全局的slab中进行refill
1、如果没有空闲对象,而且shared arry没有共享对象可用,需要扩容
2、如果shared arry有空闲对象,直接分配,否则继续
3、尝试从kmem_cache_node结构中其它kmem_cache获取slab页面
4、如果都失败了就扩容如果一、二级分配都失败了,那就扩容,并进行第三级分配:
1、再次尝试在不扩容情况下,分配新的kmem_cache并初始化,如果成功就返回
2、调用cache_grow_begin 函数,找伙伴系统分配新的内存页面,找第一个 kmem_cache 分配新的对象,来存放 kmem_cache 结构的实例变量,并进行必要的初始化
3、调用 cache_grow_end 函数,把这页面挂载到 kmem_cache_node 结构的空闲链表中
4、返回一个空闲对象四、对象回收
2021-07-07
kfree->__cache_free->___cache_free->__free_one
将对象清空后,还给了CPU的对应的array_cache - tony 👍(2) 💬(1)
既然已经有了slab分配机制,为什么在用户态还有ptmalloc以及tcmalloc?它们侧重点有什么不一样
2022-11-21 - 西门吹牛 👍(2) 💬(1)
之前学 netty,netty 中用到了伙伴算法实现内存分配与释放,说下 netty 中的实现吧: 首先会预申请一大块内存 PoolArena,内部由 6 个 PoolChunkList,和俩个 PoolSubpage[] ● 6 个 PoolChunkList:分别是 qInit、q000、q025、q050、q075、q100 ○ Netty 根据 PoolChunk 的使用率,将他们分别放入对应的 PoolChunkList 中,目的减少内存碎片 ○ 每个 PoolChunk 默认 16MB,每个 PoolChunk 有划分为 2048 个 Subpage,每个 Subpage 8KB,16MB/2048 = 8KB ○ PoolChunk 划分的 2048 个 8KB 的 Subpage 构成满二叉树 ● PoolSubpage[]:用于分配小于 8KB 的内存 ○ PoolSubpage[] 中的元素是指向 8KB 大小的 Subpage 地址,同时又把 8KB 的 subpage 分割成大小相等的段,比如 32B,64B......
分配大于 8 KB 的内存,直接走 PoolChunk 对应满二叉树,这样们更好的避免内存碎片,比如:
○ 先分配 8KB:需要一个 Page ,满二叉树最下一层满足要求,故分配这层的第一个节点page0
○ 在分配 16KB:需要两个Page ,满二叉树倒数第二层满足要求,因为这层的下一层的第一个节点page0已被分配,所以选这层第二个节点,就是相当分配 page2和page3
○ 在分配 8KB:需要一个 Page ,满二叉树最下一层满足要求,page0 以占用,往后page1可用,直接分配
经过这样分配,最终分配的是page0、page1、page2、page3 刚好这四个页是连续的。对于小于 8KB 的分配:比如32B:
○ 定位到 PoolSubpage[] 中的元素,看有没有值,没有代表之前没有分配过,执行分配,有值代表之前分配过 32B 的空间
○ 如果没有分配过,那么先取一个 8KB的page,将数组中对应的元素指向该page
○ 在将 8KB 的page 按 32B 划分成相等的段,然后取划分好的第一个 32B 的段拿出使用,并把该段标记为占用
○ 等下次在分配 32B 的时候,先定位到数组对应的元素,有值代表之前分配过 32B 的空间,那么该元素指向的 page 已经是被按 32B 划分好的相同的小段
○ 那么就可以直接从划分好的小段中,依次遍历,取出没有使用的那个 32B 的段来分配
也就是说,第一次分配小于 8KB (比如32B)的内存的时候,已经在内存中分配好了若干相同的32B 的段了,后续可以直接取用第一次分配好的当然,其中还有很多细节,比如是否池化,内存释放之后,是直接归还,还是先缓存起来,下次在用,多线程申请的时候,怎么避免竞争等问题
2022-07-06 - 朱熙 👍(2) 💬(1)
linux内核包括三种小对象管理方式,slab,slub和slob,其中slob效率较低用于嵌入式等,linux默认使用slub
2021-07-03 - 艾恩凝 👍(1) 💬(1)
打卡,看了记,记了忘,忘了看,内存的相关学习,我在路上
2022-04-22 - 搬铁少年ai 👍(1) 💬(1)
请教老师,为什么有的资料说struct page就是slab,您这里说kmem_cache是描述slab,有点糊涂。
2021-11-03 - 搬铁少年ai 👍(1) 💬(1)
请教老师,我看kmem_cache源码里的node是一个指向kmem_cache_node的指针数组,您这里给的是一个指针,如果是指针我是理解的,但是如果是指针数组,我不明白为什么需要多个node管理kmem_cache(slab头)
2021-11-02 - Samaritan. 👍(1) 💬(1)
“在 Linux 中,SLAB 管理头用 kmem_cache 结构来表示,代码如下”,请问一下,作者引用的是linux的哪个内核版本的代码呀?
2021-09-22 - 青玉白露 👍(1) 💬(1)
其实在 kmalloc_slab 函数已经写明了,最大是192,单位应该是B吧?
2021-07-13 - pedro 👍(1) 💬(1)
Cosmos YYDS!!! 问题答案看代码注释,最大192
2021-06-30 - 小灰象 👍(0) 💬(1)
翻过内存管理的大山啦!可喜可贺!!!
2024-08-26 - 弘文要努力 👍(0) 💬(1)
请问老师的源码从哪里获取呢?
2022-04-17 - 青玉白露 👍(0) 💬(1)
思考题有点像脑筋急转弯······ 几处的注释都表明了最大值是192 不过这个值是怎么定的呢?
2021-06-30 - blentle 👍(0) 💬(1)
最多192吧,
//计算出index
2021-06-30
if (size <= 192) {
if (!size)
return ZERO_SIZE_PTR;
index = size_index[size_index_elem(size)];
} else {
if (WARN_ON_ONCE(size > KMALLOC_MAX_CACHE_SIZE))
return NULL;
index = fls(size - 1);
}