前言
本文基于 6.1 版本的 Linux 内核代码,阐述笔者对 SLUB 分配器原理的个人理解。本文的分析忽略以下内容:
- 所有 debug 相关特性(如 KASAN)
- 所有安全增强特性(如
CONFIG_SLAB_FREELIST_HARDENED
) - SLUB per-cpu partial cache 特性(
CONFIG_SLUB_CPU_PARTIAL=n
)
换言之,本文只关注最底层最基础的 SLUB 运行逻辑——一种类三级缓存的结构。该想法是在上班通勤途中阅读此文 获得的启发,遂希望通过阅读代码和画图整理,将之梳理清楚。
概述
- 一级缓存:per-cpu SLAB cache
- 代表结构:
struct kmem_cache_cpu
中的freelist
- 单向链表,缓存的是 slab 对象
- 代表结构:
- 二级缓存:Slab-page SLAB cache
- 代表结构:
struct page
(v5.17-rc1 后改为struct slab
)中的freelist
- 单向链表,缓存的是 slab 对象
- 代表结构:
- 三级缓存:Node cache, or partial list
- 代表结构:
struct kmem_cache_node
中的partial
- 双向链表,缓存的是 SLAB 页
- 代表结构:
一、二级缓存之间的交互
当一个内存页被 kmem_cache_cpu
占用,若此时别的 cpu 向它归还 slab objects,则是归还到二级缓存,即该页的 page.freelist
中。二级缓存 page.freelist
的生长与 kmem_cache_cpu.freelist
不冲突,两者是同一个内存页上互不相交的两条单链。前者总是从 NULL
开始生长,但凡长出一段,就随时可能被 kmem_cache_cpu
整个拿走,即上升到一级缓存。拿走后 page.freelist
又会重新被置为 NULL
。
二级缓存的 page.frozen
表示该页是否已被某 CPU 独占(i.e. 为 kmem_cache_cpu.page
所指)。被独占期间,一级缓存可以从该页攫取空间。攫取过程的关键点在于 get_freelist()
函数:
- 若获取成功,则二级缓存对应的 page 仍是被独占的(即
page.frozen
仍为 1)。该过程被称为ALLOC_REFILL
。 - 若获取失败,则说明该 page 占满,已无可用空间。此时一级缓存会放弃对该 page 的独占(即将
page.frozen
改为 0),转而去获取新的1 slab page 并独占之;二级缓存待有空间被归还后,加入三级缓存的 partial list。
需要说明的是,一级、二级缓存只是逻辑上的区分。实际上两者处于同一个内存页上。
二级、三级缓存之间的交互
二级退入三级
当一级缓存从二级缓存获取空间失败时,一级缓存会放弃对二级缓存的独占(参见 get_freelist()
)。此时的二级缓存俨然成了“孤魂野鬼”:既不为 kmem_cache_cpu
所引用,又没有挂在 partial list 上。
当有属于该页的 slab objects 被归还,该页的 struct page
可通过 virt_to_head_page()
(v5.17-rc1 后改为 virt_to_slab()
)找回(参见 kmem_cache_free()
中的实现)。归还后该页的剩余空间从无到有,而该页又未被任何 cpu 独占,因此将该页加入到三级缓存 partial list 中。
若该页一直在 partial list 上未被取走,则待其剩余空间全部被归还后,则将其从 partial list 上摘下并退回给 buddy system。
三级进入二级
当一级缓存放弃对旧的二级缓存的独占时,会尝试从 partial list 上获取一个新的内存页来作为新的二级缓存。后者就完成从三级缓存向二级缓存的转移。
代码详解
一级缓存
从一级缓存获取空间属于快速路径:slab_alloc_node()
中除 __slab_alloc()
以外的其他部分。获取空间前先就 c->freelist
和 c->slab
进行检查(check per-cpu kmem_cache_cpu),有问题的话就会进入慢速路径 __slab_alloc()
:
1 object = c->freelist;
2 slab = c->slab;
3
4 if (!USE_LOCKLESS_FAST_PATH() ||
5 unlikely(!object || !slab || !node_match(slab, node))) {
6 object = __slab_alloc(s, gfpflags, node, addr, c, orig_size);
7 } else {
8 ...
没问题的话,就获取一级缓存头部的 slab object(即 c->freelist
的首节点),并让一级缓存的 freelist 指向下一项(即次节点 )。后者需要从当前 free obj 内存空间中获取指向下一个 free obj 的指针,该过程涉及多个因素(s->offset
以及 KASAN,SLAB_FREELIST_HARDENED 等特性),因此用一个专门的函数 get_freepointer_safe()
来处理:
1redo:
2 ...
3 object = c->freelist;
4 ...
5 } else {
6 void *next_object = get_freepointer_safe(s, object);
7 if (unlikely(!this_cpu_cmpxchg_double(
8 s->cpu_slab->freelist, s->cpu_slab->tid,
9 object, tid,
10 next_object, next_tid(tid)))) {
11 note_cmpxchg_failure("slab_alloc", s, tid);
12 goto redo;
13 }
14 prefetch_freepointer(s, next_object);
15 stat(s, ALLOC_FASTPATH);
16 ...
二级缓存
从二级缓存开始到三级缓存,整个都属于慢速路径,由 __slab_alloc()
实现。
先检查有没有二级缓存,没有二级缓存的话直接去三级缓存搞一个内存页回来:
1reread_slab:
2 slab = READ_ONCE(c->slab);
3 if (!slab) {
4 ...
5 goto new_slab;
6 }
随后对二级缓存进行各种检查(check if c->slab is good),若有不对劲的情况都把当前的二级缓存卸了(deactivate c->slab),然后去三级缓存获取新的内存页:
1redo:
2 if (unlikely(!node_match(slab, node))) {
3 if (!node_isset(node, slab_nodes)) {
4 node = NUMA_NO_NODE;
5 } else {
6 stat(s, ALLOC_NODE_MISMATCH);
7 goto deactivate_slab;
8 }
9 }
10 if (unlikely(!pfmemalloc_match(slab, gfpflags)))
11 goto deactivate_slab;
12 ...
13deactivate_slab:
14 ...
15new_slab:
若二级缓存没问题,则把 kmem_cache_cpu 锁上(因为后面涉及到对该结构的调整),然后再确认一下在“之前检查”与“当前上锁”之间的这段时间里 c->slab 没有被改变(lock s->cpu_slab and compare c->slab):在可抢占内核上,这种情况是完全可能的。若真的改变了,则跳回前面的步骤重新获取并检查。
1 local_lock_irqsave(&s->cpu_slab->lock, flags);
2 if (unlikely(slab != c->slab)) {
3 local_unlock_irqrestore(&s->cpu_slab->lock, flags);
4 goto reread_slab;
5 }
6 ...
在确保 slab page 存在且各种检查通过之后,再来尝试获取一级缓存(get c->freelist2)。若其存在,则直接复用 load_freelist
的代码,(假装)把这条 freelist 链表(重新)装载到 kmem_cache_cpu 上。
1 freelist = c->freelist;
2 if (freelist)
3 goto load_freelist;
所谓“装载”,其实就是获取这条 freelist 的次节点并将其赋予 c->freelist
,然后将首节点返回出去供外界使用(load freelist to c2)。该过程与一级缓存中的无异,分配的过程逻辑上从一级缓存走向结果。
1load_freelist:
2 lockdep_assert_held(this_cpu_ptr(&s->cpu_slab->lock));
3 VM_BUG_ON(!c->slab->frozen);
4 c->freelist = get_freepointer(s, freelist);
5 c->tid = next_tid(c->tid);
6 local_unlock_irqrestore(&s->cpu_slab->lock, flags);
7 return freelist;
若是发现一级缓存上没有获取到空间,则调用 get_freelist()
攫取二级缓存(get freelist from c->slab):若成功则顺道向下执行,将获取的 freelist 装载到 kmem_cache_cpu 上;
1 freelist = c->freelist;
2 if (freelist)
3 ...
4
5 freelist = get_freelist(s, slab);
6 if (!freelist) {
7 ...
8 }
9 stat(s, ALLOC_REFILL);
10load_freelist:
11 ...
若失败则走入三级缓存。
1 freelist = get_freelist(s, slab);
2 if (!freelist) {
3 c->slab = NULL;
4 local_unlock_irqrestore(&s->cpu_slab->lock, flags);
5 stat(s, DEACTIVATE_BYPASS);
6 goto new_slab;
7 }
三级缓存
三级缓存先尝试从 paritial list 中获取 slab page 并取走它的可用空间(get partial slab page and take away freelist)。若成功,则直接跳去检查这个页(check):
1new_slab:
2 ...
3new_objects:
4 pc.flags = gfpflags;
5 pc.slab = &slab;
6 pc.orig_size = orig_size;
7 freelist = get_partial(s, node, &pc);
8 if (freelist)
9 goto check_new_slab;
1get_partial
2 get_partial_node // 加载 page 至 kmem_cache_cpu
3 // 不开 SLUB_CPU_PARTIAL 时,`object` 只会等于 NULL,循环只有一次
4 acquire_slab // `mode` 一定为 true,new.freelist 一定为 NULL
若失败,则创建一个全新的 slab page(new slab page),设置其为独占并拿走它的 freelist(freeze it and take away freelist)。这些步骤完成后最终也走向检查(check):
1new_objects:
2 ...
3 slab = new_slab(s, gfpflags, node);
4 ...
5 stat(s, ALLOC_SLAB);
6 ...
7 freelist = slab->freelist;
8 slab->freelist = NULL;
9 slab->inuse = slab->objects;
10 slab->frozen = 1;
11 ...
12check_new_slab:
检查(check)这一步其实没有什么重要的内容,完成后就会将(从 partial list 获取的或全新创建的)slab page 装载到 kmem_cache_cpu 上(load slab page to c),然后(逻辑上)返回二级缓存:
1check_new_slab:
2 ...
3retry_load_slab:
4 local_lock_irqsave(&s->cpu_slab->lock, flags);
5 ...
6 c->slab = slab;
7 goto load_freelist;