本文的内核源码分析基于linux-6.8.7版本
Overview
Linux 中内核经常会有需要为各种对象分配内存的场景,但直接用 page allocator 分配的话有浪费和内存碎片的问题,于是出现了 slab allocator 来应对小内存对象的分配需求。slab allocator 基于 cache 的思想。对于内核的每个对象有其对应的 slab cache ,首先 slab allocator 会向 buddy system 申请一个或多个连续页(看作一个复合页)作为 slab ,再将 slab 划分成若干个同样大小的小内存块,一个 slab cache 就是若干个同种 slab 的集合,此后当内核需要分配对象或者释放时,就可以直接从相应的 slab cache 的 slab 中取放 object。
关于 slab 对象池的三种实现:slab,slub,slob。
- slab:臃肿复杂
- slub:简化高效,是默认且使用最广泛的版本
- slob:为嵌入式小型机器小内存的场景设计
本文仅讨论 slub allocator,下文提到的 slab 均指 slub 实现。
object & slab layout
如果配置了CONFIG_SLUB_DEBUG_ON
又配置了CONFIG_KASAN
的话,slab 中每个 object 的布局就是这样:
但是一般日常生活中我们不会开以上配置,那么单个 object 的布局如下:
slab 里的所有空闲 object 组织成链表 freelist,free pointer(FP)指向下一个空闲 object 。FP 是内嵌在 object 里的,偏移是kmem_cache->offset
。每个 object 需要加 padding 对齐到 word size 。
slab 的布局如下:
正常初始化时 freelist 里 object 是顺序连接,但如果配置了
CONFIG_SLAB_FREELIST_RANDOM
,就会随机化 freelist 里 object 的连接顺序
kmem_cache
不同内核对象有相应的 slab cache,每个 slab cache 都由 kmem_cache
这个对象来表示,该对象包含管理 slab cache 的所有信息。
1 | struct kmem_cache { |
内核各处都会用kmem_cache_create()
首先为内核对象创建kmem_cache
对象,管理其 slab cache ,举个例子cred
对象的kmem_cache
是这样创建的:
1 | // >>> linux-6.8.7/kernel/cred.c:606 |
然后就可以用kmem_cache_alloc()
从这个 slab cache 中申请 object 了:
1 | // >>> linux-6.8.7/kernel/cred.c:206 |
struct slab
就像 struct page 对象管理一张 page 一样,一张 slab 由 struct slab 对象管理,因为 slab 本质是单页或复合页,所以 struct slab 复用 struct folio 以及 struct page
1 | struct slab { |
slab cache layout
slab cache 的这些 slabs 是如何组织的?接下来看 slab cache 的布局:
slab cache 在每个 cpu 上有个本地缓存,这个本地缓存用 kmem_cache_cpu
对象表示,可以通过 kmem_cache
的 percpu 变量成员 cpu_slab
找到。
1 | struct kmem_cache_cpu { |
kmem_cache_cpu
缓存了一张 slab 和一个备用 slab 列表(需要配置CONFIG_SLUB_CPU_PARTIAL
)。
从 slab cache 的布局图中可以看出
kmem_cache_cpu->freelist
和kmem_cache_cpu->slab->freelist
都指向kmem_cache_cpu->slab
上的空闲 object,但是它们是两个不同的 freelist,前者是无锁 freelist ,后者是常规 freelist ,在对象分配一章会进一步讨论。
当进程需要分配对象时,首先找到当前 cpu 的kmem_cache_cpu
对象
- 一开始尝试到
kmem_cache_cpu->freelist
取 object ,这个过程无锁且不用禁用中断和抢占 - 如果
kmem_cache_cpu->freelist
没有空闲 object ,会到kmem_cache_cpu->slab->freelist
取 object - 如果以上两步都失败,说明本地缓存的 slab 没有空闲 object 了,就将
kmem_cache_cpu->partial
上第一张 slab 提升为kmem_cache_cpu->slab
,将其 freelist 移交给kmem_cache_cpu->freelist
(slab->freelist
置 0 )
如果 slab cache 的 per-cpu 本地缓存没有空闲 object 了,就要从本地 node 的后备内存池补充一些 slab,这个 per-node 的内存池由kmem_cache_node
对象管理
1 | struct kmem_cache_node { |
和kmem_cache_cpu
一样有一个 partial list 缓存了一些 slab,当尝试从 cpu 本地缓存分配失败时就会从本地 node 的kmem_cache_node->partial
上取一张 slab 提升为kmem_cache_cpu->slab
并且移一些 slab 到kmem_cache_cpu->partial
上
除非配置了
CONFIG_SLUB_DEBUG
否则没必要去维护一个 full list ,因为当一个 full slab 中的 object 被释放时,我们可以由 object 地址找到这个 slab 并且得知这个 slab 由 full 变成 partial,就可以将这个 slab 放入正确的 partial list 中
虽然
kmem_cache_cpu
和kmem_cache_node
的 slab 池都叫 “ partial list ” 但其实上面的 slab 可以是半空的也可以是全空的
Allocate an object
上层 API
内核会为核心对象创建专属 slab cache ,例如:
每个不同的 slab cache 分别由kmem_cache
对象管理,当内核需要分配对象时就会从其专属的 slab cache 中取
除了专属 slab cache 以外,内核还准备了尺寸从 8 到 8K 字节的通用 slab cache:
这是因为内核还经常有分配通用内存块的需求,就会从合适尺寸的通用 slab cache 中取
kmem_cache_alloc() 系列
从专属 slab cache 中分配的 API 是kmem_cache_alloc()
:
参数
s
指定从哪个 slab cache 中分配,参数gfpflags
是内存分配标志位(具体见)
1 | // >>> linux-6.8.7/mm/slub.c:3865 |
如果还要指定从哪个 node 上分配,就用kmem_cache_alloc_node()
kmalloc() 系列
kmalloc()
系列函数用来从通用 slab cache 中分配内存,这块内存可以用于存储任何信息。
先看kmalloc()
:
参数
size
指定分配的大小,参数gfpflags
是内存分配标志位
1 | // >>> linux-6.8.7/include/linux/slab.h:581 |
1 | // >>> linux-6.8.7/mm/slub.c:3992 |
1 | // >>> linux-6.8.7/mm/slub.c:3961 |
1 | // >>> linux-6.8.7/mm/slub.c:4005 |
kmalloc()
和kmem_cache_alloc()
一样最后都会调用到slab_alloc_node()
使用底层 slab allocator ,但在那之前kmalloc()
的首要任务是通过参数size
和gfpflags
来确定到底应该从哪个 slab cache 中分配
kmalloc()
的内存池有以下 slab cache:
kmalloc()
根据gfpflags
判断 slab cache 类型:(如果同时指定了多个 flag 就按以下优先级顺序)
- dma-kmalloc-* :内存来自
ZONE_DMA
区域,指定 flag 为GFP_DMA
- kmalloc-rcl-* :可以被回收的内存,指定 flag 为
__GFP_RECLAIMABLE
- kmalloc-cg-* :需要统计( memcg 相关)的内存,指定 flag 为
GFP_KERNEL_ACCOUNT
- kmalloc-* :通用类型,如果没有指定以上 flag 就用这个
再根据size
计算找到合适尺寸的 slab cache 的kmem_cache
对象,然后调用slab_alloc_node()
但如果
size
大于 8K 字节的话就不会走 slab allocator 而是会调用__kmalloc_large_node()
直接用 page allocator 向 buddy system 请求连续的页kmalloc()
里根据size
是否是编译时常量选择了两种计算kmem_cache
的方式,其中一个方式是编译时优化的,所以看起来绕了个弯
kmalloc()
系列还有:
kzalloc()
:和kmalloc()
一样但会将内存初始化为 0kmalloc_array()
:用kmalloc()
分配大小为n * size
的内存kcalloc()
:和kmalloc_array()
一样但会将内存初始化为 0*_node()
:指定从哪个 node 上分配,比如kmalloc_node()
,以上那些没指定 node 的 API 都默认从最近的 node 上分配,如果分配失败的话就会到其他 node 上尝试,但是如果一开始指定了 node 的话就不会到其他 node 上尝试
slab allocator 层
slab_alloc_node()
以上的 API 都会调用到slab_alloc_node()
即 slab allocator 的接口
1 | // >>> linux-6.8.7/mm/slub.c:3835 |
slab_alloc_node()
是__slab_alloc_node()
的 wrapper,还包含前后处理:
slab_pre_alloc_hook()
内会处理 memcg 相关- 3846 - 3848 行用于 kfence 机制采样
- 在
maybe_wipe_obj_freeptr()
里在一定条件下会把分配下来的 object 的 FP 置 0- 如果
gfpflags
还有__GFP_ZERO
,则slab_post_alloc_hook()
会把 object 初始化为 0 ,此外还有 KASan ,KMSan ,kmemleak 相关以及 memcg 的后处理
__slab_alloc_node():fastpath
__slab_alloc_node()
首先尝试从当前 cpu 的本地缓存的无锁 freelist ,即kmem_cache_cpu->freelist
上分配 object ,这个过程无锁且不禁用抢占和中断,是开销最小的路径,称为 fastpath
1 | // >>> linux-6.8.7/mm/slub.c:3632 |
如果kmem_cache_cpu->freelist
上有 object 就取出来返回
第 3696 行的__update_cpu_freelist_fast()
用cmpxchg128
原子地做了以下事情:
- 将当前 cpu 的
kmem_cache_cpu->tid
与之前读取的tid
比较 - 如果前后
tid
一致就更新kmem_cache_cpu->freelist = next_object
以及kmem_cache_cpu->tid = tid + 1
,否则说明发生过抢占:当前进程被调度到其他 cpu 上了,需要回到redo
用当前 cpu 的本地缓存重新分配
___slab_alloc():slowpath
如果kmem_cache_cpu
的 slab 或 freelist 为空或者分配的时候指定了 node 而 slab 不在这个 node 上,就会进入 slowpath,首先__slab_alloc()
会禁抢占和中断
1 | // >>> linux-6.8.7/mm/slub.c:3611 |
然后进入___slab_alloc()
:
1 | // >>> linux-6.8.7/mm/slub.c:3388 |
首先在reread_slab
处读kmem_cache_cpu->slab
,然后:
- 如果
kmem_cache_cpu
没有 slab ,需要跳转到new_slab
处缓存一张新的 slab - 如果该 slab 的 node 不是指定的 node 或者该 slab 属于
pfmemalloc
池而gfpflags
表明不允许从该区域分配内存,则需要跳转到deactivate_slab
处把这张 slab 从kmem_cache_cpu
上取下来 - 检查一下前面读取的 slab 是否还是
kmem_cache_cpu->slab
,如果不是说明发生过抢占并且kmem_cache_cpu->slab
被修改了,需要回到reread_slab
重来 - 先看
kmem_cache_cpu->freelist
,如果不为空那么跳转到load_freelist
处把 object 取出 - 再看
kmem_cache_cpu->slab->freelist
,如果不为空就移交给kmem_cache_cpu->freelist
,然后往下走到load_freelist
处把 object 取出,否则说明kmem_cache_cpu->slab
没空闲 object 了,解冻(置 slab->frozen 为 0 ,意味着这张 slab 被移出本地缓存kmem_cache_cpu
了)并到new_slab
处缓存一张新的 slabkmem_cache_cpu
上有两个 freelist ,但它们在同一个 slab 也就是kmem_cache_cpu->slab
上。区别在于kmem_cache_cpu->freelist
是无锁 freelist,也就是用于 fastpath 分配的 freelist。当一个 slab 成为kmem_cache_cpu->slab
时,其 freelist 会被移交给kmem_cache_cpu->freelist
,而原 freelist 会被置 0 ,此后当这张 slab 上的 object 被其他 cpu 释放时会进入slab->freelist
,于是就形成了两个 freelist。slowpath 中如果kmem_cache_cpu->freelist
为空但kmem_cache_cpu->slab->freelist
不为空,还要将后者移交给前者。如果来到1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47// >>> linux-6.8.7/mm/slub.c:3398
/* 3398 */ reread_slab:
/* 3399 */
/* 3400 */ slab = READ_ONCE(c->slab);
/* 3401 */ if (!slab) {
------
/* 3406 */ if (unlikely(node != NUMA_NO_NODE &&
/* 3407 */ !node_isset(node, slab_nodes)))
/* 3408 */ node = NUMA_NO_NODE;
/* 3409 */ goto new_slab;
/* 3410 */ }
/* 3411 */
/* 3412 */ if (unlikely(!node_match(slab, node))) {
------
/* 3417 */ if (!node_isset(node, slab_nodes)) {
/* 3418 */ node = NUMA_NO_NODE;
/* 3419 */ } else {
/* 3420 */ stat(s, ALLOC_NODE_MISMATCH);
/* 3421 */ goto deactivate_slab;
/* 3422 */ }
/* 3423 */ }
------
/* 3430 */ if (unlikely(!pfmemalloc_match(slab, gfpflags)))
/* 3431 */ goto deactivate_slab;
/* 3432 */
/* 3433 */ /* must check again c->slab in case we got preempted and it changed */
/* 3434 */ local_lock_irqsave(&s->cpu_slab->lock, flags);
/* 3435 */ if (unlikely(slab != c->slab)) {
/* 3436 */ local_unlock_irqrestore(&s->cpu_slab->lock, flags);
/* 3437 */ goto reread_slab;
/* 3438 */ }
/* 3439 */ freelist = c->freelist;
/* 3440 */ if (freelist)
/* 3441 */ goto load_freelist;
/* 3442 */
/* 3443 */ freelist = get_freelist(s, slab);
/* 3444 */
/* 3445 */ if (!freelist) {
/* 3446 */ c->slab = NULL;
/* 3447 */ c->tid = next_tid(c->tid);
/* 3448 */ local_unlock_irqrestore(&s->cpu_slab->lock, flags);
/* 3449 */ stat(s, DEACTIVATE_BYPASS);
/* 3450 */ goto new_slab;
/* 3451 */ }
/* 3452 */
/* 3453 */ stat(s, ALLOC_REFILL);
/* 3454 */load_freelist
,说明分配成功,返回空闲 object ,更新kmem_cache_cpu->freelist
(指向下一个空闲 object )和kmem_cache_cpu->tid
到1
2
3
4
5
6
7
8
9
10// >>> linux-6.8.7/mm/slub.c:3455
/* 3455 */ load_freelist:
/* 3456 */
/* 3457 */ lockdep_assert_held(this_cpu_ptr(&s->cpu_slab->lock));
------
/* 3464 */ VM_BUG_ON(!c->slab->frozen);
/* 3465 */ c->freelist = get_freepointer(s, freelist);
/* 3466 */ c->tid = next_tid(c->tid);
/* 3467 */ local_unlock_irqrestore(&s->cpu_slab->lock, flags);
/* 3468 */ return freelist;deactivate_slab
处说明需要解冻当前缓存的 slab ,即将kmem_cache_cpu->slab
从kmem_cache_cpu
上移除。将kmem_cache_cpu->slab
和kmem_cache_cpu->freelist
置 0 ,更新kmem_cache_cpu->tid
,然后调用deactivate_slab()
,deactivate_slab()
做了以下工作:
- 将
kmem_cache_cpu->freelist
移交给slab->freelist
- 置
slab->frozen
为 0 - 将这个 slab 放入合适的 list
关于上述第 3 点的进一步解释:根据 slab 的状态
- 如果是 full slab 则不管
- 如果是 partial slab 就放入其所在 node 的
kmem_cache_node->partial
- 如果是 empty slab 则要看
kmem_cache_node->partial
上已有的 slab 数是否大于阈值kmem_cache->min_partial
,如果大于就销毁这个 slab ,即将其还给 buddy system,否则还是放入kmem_cache_node->partial
走到1
2
3
4
5
6
7
8
9
10
11
12
13
14// >>> linux-6.8.7/mm/slub.c:3470
/* 3470 */ deactivate_slab:
/* 3471 */
/* 3472 */ local_lock_irqsave(&s->cpu_slab->lock, flags);
/* 3473 */ if (slab != c->slab) {
/* 3474 */ local_unlock_irqrestore(&s->cpu_slab->lock, flags);
/* 3475 */ goto reread_slab;
/* 3476 */ }
/* 3477 */ freelist = c->freelist;
/* 3478 */ c->slab = NULL;
/* 3479 */ c->freelist = NULL;
/* 3480 */ c->tid = next_tid(c->tid);
/* 3481 */ local_unlock_irqrestore(&s->cpu_slab->lock, flags);
/* 3482 */ deactivate_slab(s, slab, freelist);new_slab
处说明kmem_cache_cpu
需要缓存一张新的 slab,首先遍历kmem_cache_cpu->partial
,从上面取一张 slab
- 如果该 slab 的 node 不是指定的 node 或者该 slab 属于
pfmemalloc
池而gfpflags
表明不允许从该区域分配内存,就用__put_partials()
把这张 slab 放回其所属 node 的kmem_cache_node->partial
上,同样如果这张 slab 为 empty 且kmem_cache_node->partial
上 slab 数超过阈值就要销毁这张 slab - 否则就冻结这张 slab 并跳转到
retry_load_slab
处准备缓存这张 slab 作为kmem_cache_cpu->slab
如果1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32// >>> linux-6.8.7/mm/slub.c:3484
/* 3484 */ new_slab:
/* 3485 */
/* 3486 */
/* 3487 */ while (slub_percpu_partial(c)) {
/* 3488 */ local_lock_irqsave(&s->cpu_slab->lock, flags);
/* 3489 */ if (unlikely(c->slab)) {
/* 3490 */ local_unlock_irqrestore(&s->cpu_slab->lock, flags);
/* 3491 */ goto reread_slab;
/* 3492 */ }
/* 3493 */ if (unlikely(!slub_percpu_partial(c))) {
/* 3494 */ local_unlock_irqrestore(&s->cpu_slab->lock, flags);
/* 3495 */ /* we were preempted and partial list got empty */
/* 3496 */ goto new_objects;
/* 3497 */ }
/* 3498 */
/* 3499 */ slab = slub_percpu_partial(c);
/* 3500 */ slub_set_percpu_partial(c, slab);
/* 3501 */ local_unlock_irqrestore(&s->cpu_slab->lock, flags);
/* 3502 */ stat(s, CPU_PARTIAL_ALLOC);
/* 3503 */
/* 3504 */ if (unlikely(!node_match(slab, node) ||
/* 3505 */ !pfmemalloc_match(slab, gfpflags))) {
/* 3506 */ slab->next = NULL;
/* 3507 */ __put_partials(s, slab);
/* 3508 */ continue;
/* 3509 */ }
/* 3510 */
/* 3511 */ freelist = freeze_slab(s, slab);
/* 3512 */ goto retry_load_slab;
/* 3513 */ }
/* 3514 */kmem_cache_cpu->partial
没有 slab ,则会走到new_objects
处 get_partial()
会到别的 partial list 上取一些 slab 补充到kmem_cache_cpu->partial
里:- 首先到本地 node 的
kmem_cache_node->partial
中取一张 slab ,然后将剩下的最多kmem_cache->cpu_partial_slabs / 2
个 slab 放入kmem_cache_cpu->partial
- 如果
kmem_cache_node->partial
也没有 slab ,分配的时候也没有指定 node ,就去别的 node 上取(从近到远)
- 首先到本地 node 的
- 如果取到了 slab ,就冻结这张 slab 并跳转到
retry_load_slab
处准备缓存这张 slab 作为kmem_cache_cpu->slab
- 否则 per-cpu 和 per-node 的 slab cache 都没有 slab 了,只能调用
new_slab()
,最后会用alloc_pages_node()
向 buddy system 申请内存作为一张 slab ,如果还是失败只能返回NULL
,冻结这张 slab,然后向下走到retry_load_slab
处准备缓存这张 slab 作为kmem_cache_cpu->slab
到1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55// >>> linux-6.8.7/mm/slub.c:3516
/* 3516 */ new_objects:
/* 3517 */
/* 3518 */ pc.flags = gfpflags;
/* 3519 */ pc.orig_size = orig_size;
/* 3520 */ slab = get_partial(s, node, &pc);
/* 3521 */ if (slab) {
/* 3522 */ if (kmem_cache_debug(s)) {
/* 3523 */ freelist = pc.object;
------
/* 3529 */ if (s->flags & SLAB_STORE_USER)
/* 3530 */ set_track(s, freelist, TRACK_ALLOC, addr);
/* 3531 */
/* 3532 */ return freelist;
/* 3533 */ }
/* 3534 */
/* 3535 */ freelist = freeze_slab(s, slab);
/* 3536 */ goto retry_load_slab;
/* 3537 */ }
/* 3538 */
/* 3539 */ slub_put_cpu_ptr(s->cpu_slab);
/* 3540 */ slab = new_slab(s, gfpflags, node);
/* 3541 */ c = slub_get_cpu_ptr(s->cpu_slab);
/* 3542 */
/* 3543 */ if (unlikely(!slab)) {
/* 3544 */ slab_out_of_memory(s, gfpflags, node);
/* 3545 */ return NULL;
/* 3546 */ }
/* 3547 */
/* 3548 */ stat(s, ALLOC_SLAB);
/* 3549 */
/* 3550 */ if (kmem_cache_debug(s)) {
/* 3551 */ freelist = alloc_single_from_new_slab(s, slab, orig_size);
/* 3552 */
/* 3553 */ if (unlikely(!freelist))
/* 3554 */ goto new_objects;
/* 3555 */
/* 3556 */ if (s->flags & SLAB_STORE_USER)
/* 3557 */ set_track(s, freelist, TRACK_ALLOC, addr);
/* 3558 */
/* 3559 */ return freelist;
/* 3560 */ }
------
/* 3566 */ freelist = slab->freelist;
/* 3567 */ slab->freelist = NULL;
/* 3568 */ slab->inuse = slab->objects;
/* 3569 */ slab->frozen = 1;
/* 3570 */
/* 3571 */ inc_slabs_node(s, slab_nid(slab), slab->objects);
/* 3572 */
/* 3573 */ if (unlikely(!pfmemalloc_match(slab, gfpflags))) {
------
/* 3578 */ deactivate_slab(s, slab, get_freepointer(s, freelist));
/* 3579 */ return freelist;
/* 3580 */ }retry_load_slab
处,如果kmem_cache_cpu
上本来缓存了 slab ,则要先调用deactivate_slab()
将旧 slab 从kmem_cache_cpu
上移除,令新 slab 作为kmem_cache_cpu->slab
并跳转到load_freelist
取空闲 object1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24// >>> linux-6.8.7/mm/slub.c:3582
/* 3582 */ retry_load_slab:
/* 3583 */
/* 3584 */ local_lock_irqsave(&s->cpu_slab->lock, flags);
/* 3585 */ if (unlikely(c->slab)) {
/* 3586 */ void *flush_freelist = c->freelist;
/* 3587 */ struct slab *flush_slab = c->slab;
/* 3588 */
/* 3589 */ c->slab = NULL;
/* 3590 */ c->freelist = NULL;
/* 3591 */ c->tid = next_tid(c->tid);
/* 3592 */
/* 3593 */ local_unlock_irqrestore(&s->cpu_slab->lock, flags);
/* 3594 */
/* 3595 */ deactivate_slab(s, flush_slab, flush_freelist);
/* 3596 */
/* 3597 */ stat(s, CPUSLAB_FLUSH);
/* 3598 */
/* 3599 */ goto retry_load_slab;
/* 3600 */ }
/* 3601 */ c->slab = slab;
/* 3602 */
/* 3603 */ goto load_freelist;
/* 3604 */ }
全流程
总结一下 slab allocator 中对象分配的全流程:
Free an object
上层 API
kfree()
用于释放任意对象kmem_cache_free()
用于释放指定 slab cache 里的对象
1 | // >>> linux-6.8.7/mm/slub.c:4389 |
kree()
里如果 slab 所在页没有设置PG_slab
这个 flag ,说明当时分配的时候是直接用的 page allocator,需要调用free_large_kmalloc()
,然后调用folio_put()
交给 page allocator
1 | // >>> linux-6.8.7/mm/slub.c:4367 |
kmem_cache_free()
用cache_from_obj()
检查要释放的对象是不是真的来自指定的 kmem_cache
,如果不是的话就直接返回
1 | // >>> linux-6.8.7/mm/slub.c:4357 |
slab allocator 层
kfree()
和kmem_cache_free()
都会调用到slab_free()
进而到do_slab_free()
这里也有一个 memcg 相关的处理
1 | // >>> linux-6.8.7/mm/slub.c:4293 |
在 do_slab_free()
中首先看 kmem_cache_cpu->slab
,如果就是要释放的 object 所在 slab,直接将 object 放入kmem_cache_cpu->freelist
就 ok(按 LIFO 顺序),否则就走 slowpath :__slab_free()
一般来说会走无锁的 fastpath ,但如果配置了
CONFIG_PREEMPT_RT
即实时内核,则需要加锁,然而这两条分支把 object 放回 slab 的逻辑都是一样的:使kmem_cache_cpu->freelist
指向释放的 object ,使释放 object 的 FP 指向原来的kmem_cache_cpu->freelist
,更新kmem_cache_cpu->tid
。此外,还需要检测前后kmem_cache_cpu->slab
是否一致,如果不一致说明发生过抢占就跳回redo
重来
1 | // >>> linux-6.8.7/mm/slub.c:4230 |
如果释放的 object 不在kmem_cache_cpu->slab
上,就要到__slab_free()
里处理两件事:
- 将 object 放到合适的 freelist 中
- 如果 object 所在的 slab 的状态因 object 的释放而发生了改变(例如由 full 变为 partial),可能需要将这张 slab 移到合适的 list 上
1
2
3
4
5
6
7
8
9
10
11
12
13// >>> linux-6.8.7/mm/slub.c:4098
/* 4098 */ static void __slab_free(struct kmem_cache *s, struct slab *slab,
/* 4099 */ void *head, void *tail, int cnt,
/* 4100 */ unsigned long addr)
/* 4101 */
/* 4102 */ {
/* 4103 */ void *prior;
/* 4104 */ int was_frozen;
/* 4105 */ struct slab new;
/* 4106 */ unsigned long counters;
/* 4107 */ struct kmem_cache_node *n = NULL;
/* 4108 */ unsigned long flags;
/* 4109 */ bool on_node_partial;
首先先把 object 放回 slab 里,用 do-while 循环保证slab_update_freelist()
成功更新 slab 的 freelist 和 counter (可能由于并发问题更新失败)
与第4131行分支相关的几个条件:
new.inuse -= cnt
后!new.inuse
的话说明这张 slab 由于 object 的释放将会变成 empty slab!prior
即slab->freelist == null
,说明这张 slab 是 full slab ,由于 object 的释放将会变成 partial slab!was_frozen
说明这张 slab 不是kmem_cache_cpu->slab
kmem_cache_has_cpu_partial()
判断有没有配置CONFIG_SLUB_CPU_PARTIAL
,即kmem_cache_cpu
有没有 partial list
要进入分支,首先 slab 不能是 kmem_cache_cpu->slab
,有以下两种可能:
kmem_cache_cpu
没有 partial list ,slab 原来是 full 或者即将变为 emptykmem_cache_cpu
有 partial list ,slab 原来是 partial 即将变为 empty
进入分支说明可能需要移动这张 slab 到kmem_cache_node->partial
上或者从kmem_cache_node->partial
上移出来,不管怎样先获取 slab 所在 node 和 list 锁以及判断 slab 是不是在该 node 的kmem_cache_node->partial
上
1 | // >>> linux-6.8.7/mm/slub.c:4118 |
如果!n
,对应没有进入第4131行的情况,说明 slab 无需移动到kmem_cache_node
的 list 上或从中移出,而且:
- 如果这张 slab 由 full slab 变为了 partial slab ,且
kmem_cache_cpu
有 partial list,则用put_cpu_partial()
把其放入kmem_cache_cpu->partial
,但如果此时kmem_cache_cpu->partial
上 slab 的数量超过了阈值kmem_cache->cpu_partial_slabs
,就会将整条kmem_cache_cpu->partial
都移到kmem_cache_node->partial
上 - 否则 slab 无需移动,则直接返回这张 slab 原本不在
1
2
3
4
5
6
7
8
9
10
11
12
13
14// >>> linux-6.8.7/mm/slub.c:4153
/* 4153 */ if (likely(!n)) {
/* 4154 */
/* 4155 */ if (likely(was_frozen)) {
------
/* 4160 */ stat(s, FREE_FROZEN);
/* 4161 */ } else if (kmem_cache_has_cpu_partial(s) && !prior) {
------
/* 4166 */ put_cpu_partial(s, slab, 1);
/* 4167 */ stat(s, CPU_PARTIAL_FREE);
/* 4168 */ }
/* 4169 */
/* 4170 */ return;
/* 4171 */ }kmem_cache_node->partial
上(有可能在kmem_cache_cpu->partial
上或者是 full slab),也不是由 full slab 变为 partial slab 的情况,则说明它在kmem_cache_cpu->partial
上,无需移动这张 slab ,开锁后返回如果这张在1
2
3
4
5// >>> linux-6.8.7/mm/slub.c:4177
/* 4177 */ if (prior && !on_node_partial) {
/* 4178 */ spin_unlock_irqrestore(&n->list_lock, flags);
/* 4179 */ return;
/* 4180 */ }kmem_cache_node->partial
上的 slab 变为 empty 且kmem_cache_node->partial
上 slab 的数量超过阈值kmem_cache->min_partial
,就到slab_empty
处销毁这张 slab如果1
2
3// >>> linux-6.8.7/mm/slub.c:4182
/* 4182 */ if (unlikely(!new.inuse && n->nr_partial >= s->min_partial))
/* 4183 */ goto slab_empty;kmem_cache_cpu
没有 partial list(这种情况一般是配置了CONFIG_SLUB_DEBUG
,于是也会启用kmem_cache_node
的 full list)且 slab 由 full slab 变为 partial slab,就将这张 slab 从kmem_cache_node->full
上移除,移到kmem_cache_node->partial
上1
2
3
4
5
6
7
8// >>> linux-6.8.7/mm/slub.c:4189
/* 4189 */ if (!kmem_cache_has_cpu_partial(s) && unlikely(!prior)) {
/* 4190 */ remove_full(s, n, slab);
/* 4191 */ add_partial(n, slab, DEACTIVATE_TO_TAIL);
/* 4192 */ stat(s, FREE_ADD_PARTIAL);
/* 4193 */ }
/* 4194 */ spin_unlock_irqrestore(&n->list_lock, flags);
/* 4195 */ return;slab_empty
处理把 empty slab 还给 buddy system 的场景
首先将 slab 从原所在的 list 上移除(kmem_cache_node->partial
或kmem_cache_node->full
),然后调用discard_slab()
最后调用__free_pages()
释放回 buddy system1
2
3
4
5
6
7
8
9
10
11
12
13
14
15// >>> linux-6.8.7/mm/slub.c:4197
/* 4197 */ slab_empty:
/* 4198 */ if (prior) {
------
/* 4202 */ remove_partial(n, slab);
/* 4203 */ stat(s, FREE_REMOVE_PARTIAL);
/* 4204 */ } else {
/* 4205 */ /* Slab must be on the full list */
/* 4206 */ remove_full(s, n, slab);
/* 4207 */ }
/* 4208 */
/* 4209 */ spin_unlock_irqrestore(&n->list_lock, flags);
/* 4210 */ stat(s, FREE_SLAB);
/* 4211 */ discard_slab(s, slab);
/* 4212 */ }
全流程
总结一下 slab allocator 中对象释放的全流程: