Linux 内存管理深度剖析(二):Slab/Slub 分配器源码解析

在上一篇文章中,我们梳理了 Linux Buddy 系统如何以页为单位管理物理内存。然而内核中大量数据结构(task_structinodedentry……)都远小于一页(4 KiB),若每次都向 Buddy 系统申请整页,会造成严重的内部碎片。为此 Linux 在 Buddy 之上引入了 Slab 分配器层,专为固定大小的内核对象服务。

本文基于 Linux 6.4-rc1commit ac9a78681b92)源码,深入剖析三种 Slab 实现的设计哲学、SLUB 的核心数据结构与分配/释放路径,并延伸到 vmalloc 虚拟连续内存分配。


一、三种分配器:slab / slub / slob

Linux 内核历史上存在三套互斥的 slab 实现,通过内核 Kconfig 编译选项选择其中之一。

1.1 SLAB(原始实现)

SLAB 于 1994 年由 Sun Microsystems 的 Jeff Bonwick 设计,1996 年移植到 Linux。其核心思想是对象缓存(object caching):对象释放后不立即归还给系统,而是保留在 cache 中,下次分配时直接复用,省去构造/析构开销。

SLAB 的主要特征:

  • 每个 CPU 有独立的 array_cache(本地缓存),加速分配
  • 每个 NUMA 节点有 shared cachealien cache,跨节点对象可归还本地节点
  • 支持 cache coloring(缓存行着色),减少 L1/L2 cache 行冲突
  • 数据结构极为复杂,struct kmem_cache 在 SLAB 下超过 200 字节

SLAB 的缺点也同样明显:多层链表(slabs_partial / slabs_full / slabs_free)维护开销大;SMP 下锁竞争严重;代码可读性差,难以维护。

1.2 SLUB(默认选择)

SLUB 由 Christoph Lameter 于 2007 年编写,Linux 2.6.23 起成为默认分配器。其设计哲学是去掉队列、直接操作 slab

“SLUB : A Slab allocator without object queues.” —— mm/slub.c 开头注释

SLUB 的核心优势:

  • 无全局 full/free 链表:只维护 partial 链表,极大简化了 slab 状态机
  • 无锁 fast path:利用 this_cpu_cmpxchg_double 原子操作,分配/释放 fast path 完全无锁
  • per-CPU partial 链表:减少 NUMA node 层面的 list_lock 竞争
  • 内存占用更小,代码更清晰(mm/slub.c 约 6500 行 vs SLAB 约 4500+ 行但逻辑更复杂)

1.3 SLOB(嵌入式系统)

SLOB(Simple List Of Blocks)是为内存极端受限的嵌入式系统设计的超简化版本。它将所有空闲内存组织成一个全局的自由块链表,不区分 cache,不区分 CPU,没有任何 per-CPU 结构。

SLOB 特征:

  • 代码极短(约 600 行)
  • 内存效率最高,开销最小
  • 没有任何并发优化,性能差,不适合 SMP
  • 从 Linux 6.2 起被标记为 deprecated,预计将来移除

1.4 编译配置

三种分配器在 init/Kconfig 中互斥:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
choice
prompt "Choose SLAB allocator"

config SLAB
bool "SLAB"

config SLUB
bool "SLUB (Unqueued Allocator)"
default y

config SLOB
depends on EXPERT
bool "SLOB (Simple Allocator)"
endchoice

此外,SLUB 还有两个重要的子选项:

选项 说明
CONFIG_SLUB_DEBUG 启用 red zone、poison、对象追踪,用于调试
CONFIG_SLUB_CPU_PARTIAL 启用 per-CPU partial 链表,提升多核性能(默认开启)
CONFIG_SLUB_TINY 超精简 SLUB,去掉 per-CPU 结构,适合 CONFIG_EXPERT 场景

二、SLUB 核心数据结构

2.1 struct kmem_cache

struct kmem_cache 是 slab cache 的”描述符”,在 include/linux/slub_def.h 中定义:

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
struct kmem_cache {
#ifndef CONFIG_SLUB_TINY
struct kmem_cache_cpu __percpu *cpu_slab; /* per-CPU 热路径数据 */
#endif
slab_flags_t flags;
unsigned long min_partial; /* node->partial 最小保留数量 */
unsigned int size; /* 对象大小(含元数据) */
unsigned int object_size; /* 对象大小(不含元数据) */
struct reciprocal_value reciprocal_size;
unsigned int offset; /* freepointer 在对象内的偏移 */
#ifdef CONFIG_SLUB_CPU_PARTIAL
unsigned int cpu_partial; /* per-CPU partial 最大对象数 */
unsigned int cpu_partial_slabs;/* per-CPU partial 最大 slab 数 */
#endif
struct kmem_cache_order_objects oo; /* 首选分配 order 及对象数 */
struct kmem_cache_order_objects min;/* 退化分配 order 及对象数 */
gfp_t allocflags;
int refcount;
void (*ctor)(void *); /* 构造函数(可为 NULL) */
unsigned int inuse; /* 元数据起始偏移 */
unsigned int align;
unsigned int red_left_pad; /* 左侧 redzone 填充 */
const char *name;
struct list_head list; /* 全局 slab_caches 链表 */
/* ... SYSFS、KASAN、NUMA、usercopy 等条件编译字段 ... */
struct kmem_cache_node *node[MAX_NUMNODES]; /* NUMA 节点管理 */
};

关键字段解析:

  • **size vs object_size**:object_size 是用户请求大小,size 还包含 freepointer、red zone、padding 等元数据,size 必须对齐到 align
  • offset:空闲对象的 next 指针存放在对象内部 offset 字节处(而非独立管理),这是 SLUB “无队列”设计的关键——空闲链表嵌入在对象本身中
  • **min_partial**:node 层面 partial 链表的最低水位线。当 partial 数量超过此值且一个 slab 变为全空时,该 slab 直接释放给 buddy,防止 partial 链表无限增长。
  • **oo**:kmem_cache_order_objects 将 buddy order(高位)和该 order 下的对象数(低位)打包进一个 unsigned int,查询时用位移即可。

2.2 struct kmem_cache_cpu

每个 CPU 拥有独立的 struct kmem_cache_cpuinclude/linux/slub_def.h):

1
2
3
4
5
6
7
8
9
10
11
12
struct kmem_cache_cpu {
void **freelist; /* 指向下一个可用对象(无锁单链表头) */
unsigned long tid; /* 全局唯一事务 ID,用于 cmpxchg 检测竞争 */
struct slab *slab; /* 当前活跃 slab("frozen" 状态) */
#ifdef CONFIG_SLUB_CPU_PARTIAL
struct slab *partial; /* per-CPU partial slab 链表头 */
#endif
local_lock_t lock; /* 保护上述字段(slow path 使用) */
#ifdef CONFIG_SLUB_STATS
unsigned stat[NR_SLUB_STAT_ITEMS];
#endif
};
  • **freelist**:指向当前活跃 slab 的空闲对象链表头,fast path 直接从这里取对象。
  • **tid**(transaction ID):每次分配/释放成功后自增。与 freelist 一起参与 this_cpu_cmpxchg_double,保证在无锁操作期间没有被抢占并切换 CPU 或被中断修改。
  • **slab**:当前”冻结”(frozen)的活跃 slab。一个 slab 被某 CPU 的 cpu_slab 持有时处于 frozen 状态,脱离 partial/full 链表管理。

2.3 struct kmem_cache_node

每个 kmem_cache 在每个 NUMA 节点都有一个 kmem_cache_node,在 mm/slab.h 中定义:

1
2
3
4
5
6
7
8
9
10
11
12
struct kmem_cache_node {
#ifdef CONFIG_SLUB
spinlock_t list_lock;
unsigned long nr_partial;
struct list_head partial; /* 部分使用的 slab 链表 */
#ifdef CONFIG_SLUB_DEBUG
atomic_long_t nr_slabs;
atomic_long_t total_objects;
struct list_head full; /* 调试模式下追踪满 slab */
#endif
#endif
};

partial 链表通过 slab->slab_list 字段串联所有”部分使用”的 slab。list_lock 是该链表的保护锁,SLUB 设计的核心目标之一就是尽量减少对此锁的竞争。

2.4 slab 对象内存布局

一个 slab 的内存布局(开启 SLUB_DEBUG 时)如下:

1
2
3
4
5
6
7
8
9
┌──────────────────────────────────────────────────────────────┐
│ slab page(s) — 从 buddy 分配,阶数 = oo.order │
├──────────────┬────────────────────────────────┬──────────────┤
│ red_left_pad│ object[0] │ object[1] │
│ (redzone) ├─────────┬──────────┬───────────┤ ... │
│ │ payload │ freeptr │ redzone │ │
│ │(object_ │(offset │+ padding │ │
│ │ size) │ bytes) │ │ │
└──────────────┴─────────┴──────────┴───────────┴──────────────┘
  • payload:用户实际使用的内存(object_size 字节)
  • freeptr:对象空闲时,该位置存储指向下一个空闲对象的指针(位于 offset 处);对象分配后此位置可被覆盖(除非开启 CONFIG_SLAB_FREELIST_HARDENED
  • redzone:调试时在对象两侧填充特征字节(0xbb),释放时检测越界写
  • padding:使每个对象对齐到 align 字节边界

若开启 CONFIG_SLAB_FREELIST_HARDENED,freepointer 在存储时会与 s->random 进行 XOR 加密,防止指针被恶意篡改。


三、SLUB 分配路径

3.1 调用链总览

1
2
3
4
5
6
7
8
kmem_cache_alloc(s, gfpflags)
└─ __kmem_cache_alloc_lru(s, NULL, gfpflags)
└─ slab_alloc(s, lru, gfpflags, addr, orig_size)
└─ slab_alloc_node(s, lru, gfpflags, NUMA_NO_NODE, addr, orig_size)
├─ kfence_alloc() /* KFENCE 采样路径 */
└─ __slab_alloc_node(s, ...)
├─ [fast path] this_cpu_cmpxchg_double
└─ [slow path] __slab_alloc → ___slab_alloc

3.2 Fast Path:无锁 per-CPU freelist 分配

fast path 的核心在 __slab_alloc_nodemm/slub.c:3298):

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
static __always_inline void *__slab_alloc_node(struct kmem_cache *s,
gfp_t gfpflags, int node, unsigned long addr, size_t orig_size)
{
struct kmem_cache_cpu *c;
struct slab *slab;
unsigned long tid;
void *object;

redo:
c = raw_cpu_ptr(s->cpu_slab);
tid = READ_ONCE(c->tid);

barrier(); /* 保证 tid 先于 freelist/slab 读取 */

object = c->freelist;
slab = c->slab;

if (!USE_LOCKLESS_FAST_PATH() ||
unlikely(!object || !slab || !node_match(slab, node))) {
object = __slab_alloc(s, gfpflags, node, addr, c, orig_size);
} else {
void *next_object = get_freepointer_safe(s, object);

if (unlikely(!this_cpu_cmpxchg_double(
s->cpu_slab->freelist, s->cpu_slab->tid,
object, tid,
next_object, next_tid(tid)))) {
note_cmpxchg_failure("slab_alloc", s, tid);
goto redo;
}
prefetch_freepointer(s, next_object);
stat(s, ALLOC_FASTPATH);
}

return object;
}

分析

  1. raw_cpu_ptr 获取当前 CPU 的 kmem_cache_cpu不禁用抢占
  2. 先读 tid,再读 freelist/slab(barrier 防止乱序)。如果在这两步之间发生 CPU 迁移,cmpxchg 会因 tid 不匹配而失败,重试。
  3. this_cpu_cmpxchg_double 原子地验证并替换 (freelist, tid) 二元组:若当前 CPU 上的值仍等于之前读到的 (object, tid),则将其更新为 (next_object, next_tid(tid))。若被中断/抢占修改了,则回到 redo
  4. 这一机制完全无锁、无禁用中断,是 SLUB 高性能的关键所在。

3.3 Slow Path:___slab_alloc

当 fast path 失败(freelist 为空,或 slab 不匹配,或不支持 cmpxchg_double),进入 ___slab_allocmm/slub.c:3064)。该函数的控制流如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
___slab_alloc

├─ c->slab 存在且 freelist 非空?→ load_freelist(从当前 slab 补充 freelist)

├─ slub_percpu_partial(c) 非空?→ 从 per-CPU partial 取一个 slab 激活
│ slab = c->slab = slub_percpu_partial(c);
│ slub_set_percpu_partial(c, slab); /* 将 partial 链表头前移 */

├─ get_partial(s, node, &pc) → 从 node->partial 链表取 slab
│ (持有 node->list_lock)

└─ new_slab(s, gfpflags, node) → 向 buddy 申请新页,构建 slab
allocate_slab
└─ alloc_slab_page(gfp, node, oo) /* __alloc_pages */
初始化 freelist 链表(for 循环串联所有对象)
slab->frozen = 1

关键路径代码(mm/slub.c:3161):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
new_slab:
if (slub_percpu_partial(c)) {
/* 优先消费 per-CPU partial,避免访问 node->list_lock */
slab = c->slab = slub_percpu_partial(c);
slub_set_percpu_partial(c, slab);
stat(s, CPU_PARTIAL_ALLOC);
goto redo;
}

new_objects:
freelist = get_partial(s, node, &pc); /* 尝试 node->partial */
if (freelist)
goto check_new_slab;

slab = new_slab(s, gfpflags, node); /* 向 buddy 申请 */
if (unlikely(!slab)) {
slab_out_of_memory(s, gfpflags, node);
return NULL;
}
/* 新 slab 直接设为 frozen,整个 freelist 归当前 CPU */
freelist = slab->freelist;
slab->freelist = NULL;
slab->inuse = slab->objects;
slab->frozen = 1;

new_slab 内部调用 allocate_slab,后者通过 alloc_slab_page 向 Buddy 系统申请页,然后将所有对象串联成单链表(mm/slub.c:2032):

1
2
3
4
5
6
7
8
slab->freelist = start;
for (idx = 0, p = start; idx < slab->objects - 1; idx++) {
next = p + s->size;
next = setup_object(s, next);
set_freepointer(s, p, next); /* 在 p+offset 处写入 next 指针 */
p = next;
}
set_freepointer(s, p, NULL); /* 链表尾 */

3.4 kmalloc:通用内存分配

kmalloc 是内核最常用的内存分配接口,底层使用预创建的 kmalloc_caches 数组:

1
2
3
/* include/linux/slab.h */
extern struct kmem_cache *
kmalloc_caches[NR_KMALLOC_TYPES][KMALLOC_SHIFT_HIGH + 1];

每种类型(NORMAL、RECLAIM、DMA、CGROUP)都有一套大小分级的 cache,分级逻辑由 __kmalloc_index 确定(include/linux/slab.h:403):

1
2
3
4
5
6
7
8
9
10
11
if (size <=    8) return 3;   /* kmalloc-8    */
if (size <= 16) return 4; /* kmalloc-16 */
if (size <= 32) return 5; /* kmalloc-32 */
if (size <= 64) return 6; /* kmalloc-64 */
if (size <= 128) return 7; /* kmalloc-128 */
if (size <= 256) return 8; /* kmalloc-256 */
if (size <= 512) return 9; /* kmalloc-512 */
if (size <= 1024) return 10; /* kmalloc-1k */
if (size <= 2048) return 11; /* kmalloc-2k */
if (size <= 4096) return 12; /* kmalloc-4k */
/* ... 直到 2MB */

kmalloc 调用 kmem_cache_alloc(kmalloc_caches[type][index], flags),其分配路径与普通 slab cache 完全相同。超过 KMALLOC_MAX_CACHE_SIZE(通常为 8KB)的请求直接调用 alloc_pages


四、SLUB 释放路径

4.1 调用链总览

1
2
3
4
5
kmem_cache_free(s, x)
└─ slab_free(s, virt_to_slab(x), x, NULL, &x, 1, addr)
└─ do_slab_free(s, slab, head, tail, cnt, addr)
├─ [fast path] this_cpu_cmpxchg_double(归还到 per-CPU freelist)
└─ [slow path] __slab_free

4.2 Fast Path:归还到 per-CPU freelist

do_slab_freemm/slub.c:3707)的快路径:

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
static __always_inline void do_slab_free(struct kmem_cache *s,
struct slab *slab, void *head, void *tail, int cnt,
unsigned long addr)
{
void *tail_obj = tail ? : head;
struct kmem_cache_cpu *c;
unsigned long tid;
void **freelist;

redo:
c = raw_cpu_ptr(s->cpu_slab);
tid = READ_ONCE(c->tid);
barrier();

if (unlikely(slab != c->slab)) {
/* 被释放的对象不属于当前 CPU 的活跃 slab → 走 slow path */
__slab_free(s, slab, head, tail_obj, cnt, addr);
return;
}

if (USE_LOCKLESS_FAST_PATH()) {
freelist = READ_ONCE(c->freelist);
set_freepointer(s, tail_obj, freelist); /* tail_obj->next = freelist */

if (unlikely(!this_cpu_cmpxchg_double(
s->cpu_slab->freelist, s->cpu_slab->tid,
freelist, tid,
head, next_tid(tid)))) {
note_cmpxchg_failure("slab_free", s, tid);
goto redo;
}
}
stat(s, FREE_FASTPATH);
}

fast path 条件:释放的对象所在 slab 恰好是当前 CPU 的活跃 slabslab == c->slab)。此时直接将对象头插到 freelist,同样用 this_cpu_cmpxchg_double 原子完成,无锁无禁中断。

4.3 Slow Path:__slab_free

当对象不属于当前 CPU 的活跃 slab 时,进入 __slab_freemm/slub.c:3573),该函数用 cmpxchg_double_slab 原子操作更新 slab 的 (freelist, counters) 二元组,并按照以下逻辑决定 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
__slab_free 逻辑(简化):

do {
prior = slab->freelist
counters = slab->counters
set_freepointer(s, tail, prior) /* 将对象插入 slab 的 freelist */
new.inuse -= cnt
was_frozen = new.frozen

if ((!new.inuse || !prior) && !was_frozen) {
if (cpu_partial 支持 && !prior) {
/* slab 从全满变为部分空,尝试冻结并加入 per-CPU partial */
new.frozen = 1
} else {
/* 需要操作 node->partial,获取 list_lock */
n = get_node(s, slab_nid(slab))
spin_lock_irqsave(&n->list_lock, flags)
}
}
} while (!cmpxchg_double_slab(s, slab, prior, counters,
head, new.counters, "__slab_free"))

/* cmpxchg 成功后处理后续 */
if (!n) {
if (was_frozen)
stat(s, FREE_FROZEN) /* slab 仍被某 CPU 持有,无需操作链表 */
else if (new.frozen)
put_cpu_partial(s, slab, 1) /* 加入 per-CPU partial */
return
}

/* n != NULL,已持有 list_lock */
if (!new.inuse && n->nr_partial >= s->min_partial)
goto slab_empty /* slab 全空且 partial 充足 → 释放回 buddy */

if (!prior)
add_partial(n, slab, DEACTIVATE_TO_TAIL) /* 从满变部分空 → 加入 node->partial */

slab_empty:
remove_partial(n, slab)
spin_unlock_irqrestore(&n->list_lock, flags)
discard_slab(s, slab) /* 归还给 buddy */

三条关键决策:

  1. slab 仍被某 CPU 持有(frozen):只更新 freelist,不动链表,stat(FREE_FROZEN)
  2. slab 从全满变为部分空:若支持 cpu_partial,冻结后加入 per-CPU partialCPU_PARTIAL_FREE);否则加入 node->partialFREE_ADD_PARTIAL)。
  3. **slab 全空,且 node->nr_partial >= min_partial**:slab 多余,直接 discard_slab 返还给 buddy,防止内存长期占用(FREE_SLAB)。

五、vmalloc:虚拟连续内存分配

5.1 vmalloc vs kmalloc

特性 kmalloc vmalloc
物理内存 连续 不连续(分散的页帧)
虚拟地址 线性映射区(直接映射) vmalloc 区域(需建页表)
性能 极快(fast path 无锁) 较慢(需分配页表、TLB flush)
大小上限 通常 ≤ 8 KiB(超大走 alloc_pages) 仅受虚拟地址空间限制
适用场景 小型内核对象、DMA 缓冲区 内核模块、大块临时缓冲区

5.2 struct vmap_area:虚拟地址空间管理

vmalloc 区域的每一段虚拟地址范围都由 struct vmap_areainclude/linux/vmalloc.h:63)描述:

1
2
3
4
5
6
7
8
9
10
11
12
13
struct vmap_area {
unsigned long va_start;
unsigned long va_end;

struct rb_node rb_node; /* 按地址排序的红黑树节点 */
struct list_head list; /* 按地址排序的链表节点 */

union {
unsigned long subtree_max_size; /* 在 "free" 树中:子树最大空洞 */
struct vm_struct *vm; /* 在 "busy" 树中:对应的 vm_struct */
};
unsigned long flags;
};

vmalloc 维护两棵红黑树:

  • **free_vmap_area_root**:空闲虚拟地址段,使用 subtree_max_size 做增强型红黑树,可 O(log n) 找到足够大的空洞
  • **vmap_area_root**:已分配的虚拟地址段,关联到 vm_struct

5.3 __vmalloc_node 分配流程

1
2
3
4
5
6
7
/* mm/vmalloc.c:3319 */
void *__vmalloc_node(unsigned long size, unsigned long align,
gfp_t gfp_mask, int node, const void *caller)
{
return __vmalloc_node_range(size, align, VMALLOC_START, VMALLOC_END,
gfp_mask, PAGE_KERNEL, 0, node, caller);
}

__vmalloc_node_rangemm/vmalloc.c:3172)的核心步骤:

  1. 分配虚拟地址区间:调用 __get_vm_area_node,在 free_vmap_area_root 中找到合适的虚拟地址段,创建 vm_structvmap_area,插入 vmap_area_root
  2. 分配物理页__vmalloc_node_range 内部调用 __vmalloc_area_node,逐页向 buddy 申请物理页(alloc_pages_node)。
  3. 建立页表映射vmap_pages_rangevmap_pte_rangemm/vmalloc.c:93):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
static int vmap_pte_range(pmd_t *pmd, unsigned long addr, unsigned long end,
phys_addr_t phys_addr, pgprot_t prot,
unsigned int max_page_shift, pgtbl_mod_mask *mask)
{
pte_t *pte;
u64 pfn;

pfn = phys_addr >> PAGE_SHIFT;
pte = pte_alloc_kernel_track(pmd, addr, mask);
if (!pte)
return -ENOMEM;
do {
BUG_ON(!pte_none(*pte));
set_pte_at(&init_mm, addr, pte, pfn_pte(pfn, prot));
pfn++;
} while (pte += PFN_DOWN(size), addr += size, addr != end);
*mask |= PGTBL_PTE_MODIFIED;
return 0;
}

每个物理页分别建立 PTE,使得虚拟地址连续但物理地址可以分散。

1
2
3
4
5
6
/* vmalloc 最终对外接口 */
void *vmalloc(unsigned long size) /* mm/vmalloc.c:3353 */
{
return __vmalloc_node(size, 1, GFP_KERNEL, NUMA_NO_NODE,
__builtin_return_address(0));
}

六、SLUB 调试特性

6.1 CONFIG_SLUB_DEBUG

开启后,每个 slab 对象会附加以下验证信息(mm/slub.c 中的 setup_slab_debug):

特性 说明
Red Zone 对象左右两侧填充 0xbb,释放时检查越界写(buffer overrun)
Poison 空闲对象填充 0x6bPOISON_FREE),分配后填充 0x5aPOISON_INUSE),检测 use-after-free
Track 记录每个对象最后一次分配/释放的调用栈(存储在 slab 末尾)
Consistency Check 分配/释放前后对 red zone 和 poison 值做一致性验证

调试模式下所有分配都走 slow path(持 list_lock),性能会显著下降,仅用于开发调试。

6.2 /proc/slabinfo

1
2
3
4
5
6
7
8
9
10
$ cat /proc/slabinfo
slabinfo - version: 2.1
# name <active_objs> <num_objs> <objsize> <objperslab> <pagesperslab>
# : tunables <limit> <batchcount> <sharedfactor>
# : slabdata <active_slabs> <num_slabs> <sharedavail>
ext4_inode_cache 12450 12480 1032 31 8
dentry 8256 9360 192 42 2
kmalloc-512 1024 1024 512 32 4
kmalloc-256 2048 2048 256 32 2
...

各列含义:active_objs(正在使用的对象数)、num_objs(总容量)、objsize(对象大小)、objperslab(每个 slab 含对象数)、pagesperslab(每个 slab 占页数)。

6.3 /sys/kernel/slab/ 目录

每个 cache 在 sysfs 下有独立目录:

1
2
3
4
5
6
7
8
9
10
11
12
/sys/kernel/slab/
├── kmalloc-256/
│ ├── object_size # 对象大小
│ ├── objs_per_slab # 每 slab 对象数
│ ├── order # buddy order
│ ├── partial # node->nr_partial 总数
│ ├── slabs # 总 slab 数
│ ├── alloc_fastpath # fast path 分配次数(统计)
│ ├── alloc_slowpath
│ ├── free_fastpath
│ ├── free_slowpath
│ └── ...

通过 cat /sys/kernel/slab/<name>/alloc_fastpath 可实时观察 fast/slow path 比例,辅助性能调优。

6.4 kmemleak 内存泄漏检测原理

kmemleak(mm/kmemleak.c)在每次 kmalloc/kmem_cache_alloc/vmalloc 时向自身的红黑树中插入一条记录,记录分配地址和大小。在后台扫描线程中,kmemleak 对所有内存区域做灰色标记扫描(类似 GC 的 mark-and-sweep):

  1. 标记根集:内核 .data.bss、栈、CPU 寄存器等中的指针都是根。
  2. 传播扫描:从根集出发,凡被某个已知指针指向的分配块,标记为”可达”。
  3. 报告泄漏:扫描结束后,不可达的分配块视为泄漏,输出到 /sys/kernel/debug/kmemleak

使用方式:

1
2
3
4
# 开启扫描
echo scan > /sys/kernel/debug/kmemleak
# 查看泄漏报告
cat /sys/kernel/debug/kmemleak

七、诊断方法

7.1 slabtop:实时监控

slabtop 按内存占用排序,实时刷新(类似 top):

1
2
3
4
5
6
7
8
9
10
$ slabtop
Active / Total Objects (% used) : 1234567 / 1345678 (91.7%)
Active / Total Slabs (% used) : 45678 / 45900 (99.5%)
Active / Total Caches (% used) : 85 / 120 (70.8%)
Active / Total Size (% used) : 512.34M / 560.12M (91.5%)

OBJS ACTIVE USE OBJ SIZE SLABS OBJ/SLAB CACHE SIZE NAME
127680 127680 100% 1.03K 4256 30 34048K ext4_inode_cache
98304 97820 99% 0.19K 2341 42 9364K dentry
65536 64000 97% 0.50K 2048 32 16384K kmalloc-512

7.2 vmstat -m:slab 内存汇总

1
2
3
4
$ vmstat -m
Cache Num Total Size Pages
ext4_inode_cache 12450 12480 1032 31
dentry 8256 9360 192 42

vmstat -m 等价于读取 /proc/slabinfo,输出格式更友好。

7.3 BPFtrace 追踪分配事件

1
2
3
4
5
6
7
8
# 追踪所有 kmem_cache_alloc,打印 cache 名称和分配大小
bpftrace -e '
kprobe:kmem_cache_alloc {
printf("comm=%-16s cache=%s size=%d\n",
comm,
((struct kmem_cache *)arg0)->name,
((struct kmem_cache *)arg0)->object_size);
}' 2>/dev/null | head -20

或者追踪特定 cache 的分配栈:

1
2
3
4
5
6
7
bpftrace -e '
kprobe:kmem_cache_alloc /
((struct kmem_cache *)arg0)->name == "dentry"
/ {
@[kstack] = count();
}
interval:s:10 { print(@); exit(); }'

7.4 perf stat 统计 kmalloc 频率

1
2
3
4
5
6
7
8
9
# 统计 10 秒内 kmalloc 事件次数及分布
$ perf stat -e kmem:kmalloc,kmem:kmem_cache_alloc -a sleep 10

Performance counter stats for 'system wide':

2,345,678 kmem:kmalloc
987,654 kmem:kmem_cache_alloc

10.001234567 seconds time elapsed

7.5 kmemleak 排查泄漏

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 1. 确认内核开启了 CONFIG_DEBUG_KMEMLEAK
grep CONFIG_DEBUG_KMEMLEAK /boot/config-$(uname -r)

# 2. 触发一次完整扫描
echo scan > /sys/kernel/debug/kmemleak

# 3. 查看疑似泄漏
cat /sys/kernel/debug/kmemleak

# 典型输出:
# unreferenced object 0xffff88801234abcd (size 256):
# comm "nginx", pid 1234, jiffies 4294956789
# hex dump (first 32 bytes): ...
# backtrace:
# kmalloc include/linux/slab.h:580
# some_module_alloc+0x45 [some_module]
# ...

# 4. 清除已记录的泄漏(重新开始追踪)
echo clear > /sys/kernel/debug/kmemleak

7.6 常见问题与调优建议

现象 诊断命令 可能原因
slab 占用持续增长 watch -n1 slabtop 对象泄漏,检查 kmemleak
alloc_slowpath 比例高 /sys/kernel/slab/*/alloc_slowpath cpu_partial 太小,调整 cpu_partial 参数
NUMA 节点间 slab 迁移频繁 alloc_node_mismatch 计数 进程绑核策略不合理,或 NUMA 不均衡
kmalloc-512 使用率低 slabtop 的 USE% 列 分配大小离散,考虑专用 cache
order_fallback 频繁 /sys/kernel/slab/*/order_fallback 内存碎片严重,触发 kcompactd

八、总结

SLUB 分配器的设计哲学可以用三个词概括:简化、无锁、分层

  • 简化:去掉 per-CPU 队列和全局 full/free 链表,只保留 per-CPU slab + partial 链表,状态机更清晰。
  • 无锁:fast path 借助 this_cpu_cmpxchg_double 实现无锁分配/释放,即便在高频 kmalloc/kfree 场景下,锁竞争也极低。
  • 分层cpu_slab(per-CPU,无锁)→ cpu->partial(per-CPU,本地锁)→ node->partial(NUMA,spinlock)→ buddy(全局,zone lock),分层缓存大幅减少向 buddy 申请的频率。

理解这套机制不仅能帮助内核开发者写出对分配器更友好的代码(例如:优先使用专用 kmem_cache 而非通用 kmalloc,保持对象大小一致),更能在遇到内存问题时快速定位根因。下一篇文章将进入页面回收机制(LRU、kswapd、内存压力通知),探讨内核如何在内存紧张时”腾出”空间。


参考资料