引言
SLUB(the unqueued slab allocator)是 Linux 内核当前的默认通用内存分配器,由 Christoph Lameter 在 2.6.23 内核中引入,用以取代设计上已显老旧的 SLAB 分配器。SLUB 的设计哲学极其简单:尽可能减少每对象元数据,尽量让分配路径不经过任何队列,把复杂性推到初始化阶段而不是热路径上。这一哲学对服务器、嵌入式、实时系统都产生了深远影响。本文将从硬件伙伴系统(Buddy System)向上,完整梳理 SLUB 的数据结构、分配/释放流程、每 CPU 缓存、NUMA 支持、 debugging 接口,以及大量驱动和子系统中的实际使用模式。
1. SLUB 在 Linux 内存管理栈中的位置
理解 SLUB 必须先看清它处于整个内存管理层次的哪一层:
用户空间: malloc / free / new / delete
↓ (brk / mmap 系统调用)
内核虚拟内存: vmalloc / kmalloc / kmem_cache_alloc
↓
页面分配器 ( alloc_pages )
↓
伙伴系统 (Buddy System) — 以 page 为单位按 2^n 分配
↓
SLAB/SLUB/SLOB 分配器 — 缓存常用小对象,避免反复分裂/合并页面
伙伴系统只能分配 2^n 个连续物理页(order-0 = 4KB, order-1 = 8KB ...)。一个典型 task_struct 只有 7KB 左右,如果每次都从伙伴系统要 8KB 页,内部碎片和分配延迟都不可接受。SLUB 的作用是:在内部分配一个或多个页面,将其切割成固定大小的对象(object),用 freelist 管理空闲对象,让 kmalloc / kmem_cache_alloc 的南路径 O(1) 完成。
2. SLUB 与 SLAB、SLOB 的对比
Linux 内核历史上存在过三种 slab 实现,目前 SLUB 是大多数场景的默认选择:
| 特性 | SLAB (1994) | SLUB (2007) | SLOB (嵌入式) |
|---|---|---|---|
| 元数据开销 | 每 slab 一个管理结构 + bufctl 数组 | 内联 freelist 指针复用 object 空间 | 对象头极小(仅 2 bytes) |
| Per-CPU 缓存 | 复杂(hot/cold cache) | 简单(single cpu_slab) | 无 |
| NUMA 支持 | queue arrays per-node | per-node partial 链表 | 无 |
| 调试能力 | 有限 | Redzone / Poison / Tracking | 最少 |
| 代码量 | ~4000 LOC | ~2500 LOC | ~800 LOC |
| 适用场景 | 老旧系统 | 服务器 / 桌面 / 主流内核 | 极低内存嵌入式 |
SLUB 相比 SLAB 的核心简化:
- 取消了 SLAB 的
kmem_bufctl_t数组,freelist 指针直接嵌入在空闲对象的内存里 - 不再需要
objp到 slab 的反向映射,用page->freelist和page->inuse计数即可 - 放弃了 cold cache 概念,per-CPU 只有一个 slab 活跃
3. 核心数据结构
3.1 kmem_cache
每个缓存(cache)代表一种固定大小的对象池,例如 task_struct、dentry、inode、tcp_sock 各自独立一个 kmem_cache:
struct kmem_cache {
struct kmem_cache_cpu __percpu *cpu_slab; // 每 CPU 热路径
slab_flags_t flags; // 对象约束 (DMA, RECLAIM 等)
unsigned long min_partial; // node 上最少保留 partial 数
unsigned int size; // 含元数据的对象大小
unsigned int object_size; // 用户请求的纯大小
struct reciprocal_value reciprocal_size; // 除法优化
unsigned int offset; // freelist 指针偏移
unsigned int cpu_partial; // per-cpu partial 批处理阈值
void (*ctor)(void *); // 构造函数 (SLAB_ACCOUNT 等)
const char *name;
struct list_head list; // 全局 cache_chain
struct kmem_cache_node **node; // per-node 数据
};
3.2 kmem_cache_cpu(每 CPU 核心结构)
这是分配热路径的真正入口,每个 CPU 有独立副本,无锁操作:
struct kmem_cache_cpu {
void **freelist; // 指向下一个空闲对象
unsigned long tid; // 全局事务 ID,检测并发/中断夺走
struct page *page; // 当前正在使用的 slab 页
struct page *partial; // 本地 partial slabs 链表 (CONFIG_SLUB_CPU_PARTIAL)
};
tid(Transaction ID)是 SLUB 实现无锁的关键:每次分配/释放都递增全局计数器,在 CAS 失败时意味着另一个 CPU 抢走了当前 slab,需要重试。这是 Linux 内核中经典的 lock-free 技巧。
3.3 kmem_cache_node(每节点 partial 链表)
在 NUMA 系统中,每个 node 维护一个 partial slabs 链表:
struct kmem_cache_node {
spinlock_t list_lock;
unsigned long nr_partial;
struct list_head partial; // 部分填充的 slabs 链表
#ifdef CONFIG_SLUB_DEBUG
unsigned long nr_slabs;
unsigned long total_objects;
struct list_head full; // 已满 slabs 链表 (debug 模式)
#endif
};
3.4 struct page 复用字段
SLUB 没有为 slab 头部单独分配内存,而是把元数据嵌入到 struct page 的联合体中(include/linux/mm_types.h):
struct { /* slab, slob, slub */
union {
struct {
unsigned long objects; // 高 16 位 = 对象总数, 低 16 位 = 已用
};
struct { /* SLUB */
unsigned inuse:16;
unsigned objects:16;
unsigned frozen:1; // frozen=1 表示绑定在某 cpu_slab
};
};
struct { /* 用于 slub */
void *freelist; // 第一个空闲对象
union {
unsigned long counters; /* 非 debug 模式 */
struct {
unsigned inuse:16;
unsigned objects:16;
};
};
};
};
4. 分配全路径剖析
以 kmalloc(size, GFP_KERNEL) 为例,完整调用链是:
kmalloc(size, gfp)
→ __kmalloc(size, gfp)
→ __do_kmalloc(size, gfp, _RET_IP_)
→ kmem_cache_alloc(cache, gfp) // 通过 size_index_table 选中 cache
→ slab_alloc(cache, gfp, addr)
→ slab_alloc_node(cache, gfp, NUMA_NO_NODE, addr)
→ __slab_alloc_node(...) // 热路径
4.1 热路径(fast path)
static __always_inline void *__slab_alloc(struct kmem_cache *s,
gfp_t gfpflags, unsigned long addr, struct kmem_cache_cpu *c)
{
redo:
void *object = c->freelist; // 1. 取 freelist 头
struct page *page = c->page;
if (unlikely(!object || !page))
return __slab_alloc_slowpath(...);
unsigned long tid = c->tid; // 2. 读事务 ID
barrier(); // 3. 防止编译器重排
void *next_object = get_freelist_safe(object); // 4. 读下一个
if (unlikely(!this_cmpxchg(...))) // 5. CAS 更新 freelist+tid
goto redo; // CAS 失败 → 被中断/其他 CPU 抢走
c->freelist = next_object;
c->page->inuse++; // 6. 更新 inuse 计数
return object; // 7. 返回对象
}
注意第 2~4 行的 load-load barrier:必须先读 tid 再读对象,保证看到对象时对应的 tid 仍然有效。这是 lock-free 编程中的经典 DCL (Double-Check Locking) 变体。
4.2 中速路径(本地 partial)
当前 slab 已空(freelist == NULL),但 cpu_slab->partial 链表里还有半满的 slab:
// 把 partial 链表的第一个 slab 提升为当前活跃 slab
page = c->partial;
c->page = page;
c->freelist = page->frozen ? NULL : get_freelist(page);
c->partial = page->next;
page->frozen = 1;
goto redo;
4.3 慢速路径(Node partial 或新分配)
本地无可用 slab,需要:
- 尝试从
kmem_cache_node->partial链表拿一个部分填充的 slab(需要 spinlock) - 如果连 partial 都没有,调用
get_partial()→new_slab()→allocate_slab()→alloc_slab_page()→alloc_pages()向伙伴系统要页 - 分配的页数量由
oo_objects()决定(kmem_cache->oo,即 min+max 之间最优的 order)
5. 释放路径剖析
释放(kfree / kmem_cache_free)与分配对称,但有一个关键判断:要释放的对象是否属于当前 cpu_slab->page:
void kmem_cache_free(struct kmem_cache *s, void *x)
{
struct page *page = virt_to_head_page(x); // 取 slab 首页
// 快速路径:对象属于当前 CPU 活跃 slab
if (likely(page == __this_cpu_read(s->cpu_slab->page))) {
set_freelist(s, object, freelist); // 头插
this_cpu_write(s->cpu_slab->freelist, object);
this_cpu_add(s->cpu_slab->page->inuse, -1);
return;
}
// 慢速路径:跨 CPU 或 partial slab 释放
__slab_free(s, page, object, ...);
}
慢速路径中有一种重要的优化:当释放一个 partial slab 的最后一个已用对象时(inuse 变为 0),SLUB 有策略地决定是否将 slab 归还伙伴系统。kmem_cache->cpu_partial 和 min_partial 这两个阈值控制着归还的激进度。
6. kmalloc_caches 与 size 分类
Linux 预定义了 8~22 号缓存,覆盖 64B 到 4MB 的常见分配:
| kmalloc 缓存 | 大小 | 典型用途 |
|---|---|---|
| kmalloc-8 | 8 B | 已废弃大小对齐 |
| kmalloc-16 | 16 B | small locks, |
| kmalloc-32 | 32 B | file, dentry 辅助结构 |
| kmalloc-64 | 64 B | semaphore, epoll_item |
| kmalloc-96 | 96 B | task_struct 紧凑版, 路由缓存 |
| kmalloc-128 | 128 B | inode, tcp_request_sock |
| kmalloc-192 | 192 B | task_struct(默认配置) |
| kmalloc-256 | 256 B | skb_shinfo, bio_set |
| kmalloc-512 | 512 B | bioset, loop_device |
| kmalloc-1k | 1024 B | page_ext VMA 结构 |
| kmalloc-2k | 2048 B | super_block, file_handle |
| kmalloc-4k | 4096 B | page 结构体映射表 |
用户通过 kmalloc(200, GFP_KERNEL) 时,内核会向上取整到 kmalloc-256。这种内部碎片是 SLUB 的固有权衡。
7. 每 CPU 缓存与 CONFIG_SLUB_CPU_PARTIAL
SLUB 的 CONFIG_SLUB_CPU_PARTIAL (默认开启) 极大提升了核间回收效率:
- 一个 CPU 上的 slab 被另一个 CPU 释放对象时,那个 slab 不会立刻归还 node,而是放入释放者 CPU 的
cpu_slab->partial链表 - 下次该 CPU 分配时会优先从本地 partial 中取,避免了跨节点/跨 CPU 的 spinlock 竞争
- 当 CPU 本地 partial 链表超过
cpu_partial阈值时(通常 30 或 100),才批量归还给 node
在 64 核 NUMA 服务器上,这一特性可以让分配延迟降低 40% 以上,因为它消除了 list_lock 的热点竞争。
8. NUMA 感知分配
NUMA 系统下,每个 kmem_cache 都包含 node[MAX_NUMNODES] 数组:
static inline void *slab_alloc_node(struct kmem_cache *s,
gfp_t gfpflags, int node, unsigned long addr)
{
// 1. 本地 CPU 热路径(只在本地 node 操作)
// 2. 本地 node partial(spinlock 保护)
// 3. 根据 GFP_THISNODE 决定是否跨 node
if (!(gfpflags & __GFP_THISNODE) || node == NUMA_NO_NODE)
node = numa_mem_id(); // 取当前 CPU 所属的 node
// 优先从目标 node 的 kmem_cache_node 获取 partial slab
// 不中则 fallback: 允许 __GFP_NOWARN 时跨 node
}
关键设计决策:
GFP_THISNODE强制只在指定 node 分配,失败直接 NULL- 大多数网络/文件子系统会用
numa_node_id()把对象绑定到当前 CPU 所在 node,减少跨 node 访问延迟 - SLUB 不允许将已经绑定到某 CPU 的 slab(frozen=1)重新绑定到其他 CPU,保证 per-CPU 语义
9. 调试与故障排查工具
9.1 /proc/slabinfo
$ cat /proc/slabinfo | head -12
slabinfo - version: 2.1
# name <active_objs> <num_objs> <objsize> <objperslab> <pagesperslab> : ...
kmalloc-256 18244 19200 256 32 2 : ...
task_struct 812 812 6336 5 8 : ...
dentry 186202 186202 192 40 2 : ...
inode_cache 48309 48309 624 12 2 : ...
TCP 18250 18250 2048 4 2 : ...
解读列含义:
active_objs:当前已分配的对象数num_objs:该 cache 承载的总对象数(包括空闲)objsize:每个对象实际大小(含 SLUB 元数据/对齐)objperslab:每个 slab 页能放多少对象
9.2 slabtop(实时监控)
$ slabtop -o
Active / Total Objects (% used) : 6236893 / 7221990 (86.4%%)
Active / Total Slabs (% used) : 298102 / 298102 (100.0%)
Active / Total Caches (% used) : 160 / 160 (100.0%)
Active / Total Size (% used) : 1.30G / 1.44G (90.4%)
Minimum / Average / Maximum Object : 0.01K / 0.20K / 16.00K
OBJS ACTIVE USE OBJ SIZE SLABS OBJ/SLAB CACHE SIZE NAME
421806 421806 100% 0.19K 21020 40 336K dentry
372528 372528 100% 0.57K 8862 66 10K ext4_inode_cache
202500 200918 99% 0.13K 8100 25 50K kernfs_node_cache
如果发现某个 cache 的 active_objs 持续接近 num_objs 且不再增长,说明存在严重的内存碎片或潜在 leak。
9.3 kmemleak
CONFIG_DEBUG_KMEMLEAK 通过扫描内存页寻找被分配但不再被引用(无指针指向)的对象,能精确定位 suspected leak:
$ echo scan > /sys/kernel/debug/kmemleak
$ cat /sys/kernel/debug/kmemleak
unreferenced object 0xffff88803fa5c000 (size 256):
comm "test_leak", pid 1234, age 1234.567s
hex dump (first 32 bytes):
00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f ................
...
backtrace:
[<ffffffff816e4321>] kmalloc_trace+0x21/0x90
[<ffffffff82004a1b>] alloc_foo_object+0x3b/0x120
[<ffffffff82003f1d>]; kernel_init+0x6d/0x120
9.4 SLUB_DEBUG(Redzone / Poison)
开启 CONFIG_SLUB_DEBUG 后,SLUB 在每个对象周围插入哨兵区:
// 内存布局(四周包围 Redzone)
[ 4 bytes Redzone | ... object_size ... | 4 bytes Redzone ]
↑ 用户可见区域
// 释放时填充 Poison: 0x6b 0xa5 0x5a 0x6b 0xa5 0x5a ...
检测能力:
- Redzone overwritten:某对象越界写入
- Poison overwritten:use-after-free
- Object padding error:未对齐访问或 memset 越界
通过 slub_debug=UFP 内核参数可按需启用:U=user tracking,F=SANITY checks,P=poison。也可以只对特定 cache 启用:slub_debug=,dentry。
10. 性能优化实战
10.1 选择合适的 cache 大小
性能陷阱:
- 大对象用 kmalloc,伙伴系统才合适:请求 > 2 pages(通常 >= 8KB)时,
kmalloc会自动走kmalloc_large用伙伴系统直接分配,但浪费了 SLUB cache 的初始化开销。应直接用alloc_pages+page_address - 频繁分配/释放同一结构:不要反复 kmalloc,应考虑建立自己的
kmem_cache并用kmem_cache_alloc - GFP 标志:
GFP_ATOMIC禁用本地中断保存,速度略快但会减少可用内存。路径是否在中断上下文决定了是否可以GFP_KERNEL
10.2 自定义 kmem_cache 最佳模式
static struct kmem_cache *my_pool;
static int __init my_module_init(void)
{
// 1. 创建 cache
my_pool = kmem_cache_create("my_foo", sizeof(struct my_foo),
0, // align: 默认 HW 对齐
SLAB_HWCACHE_ALIGN | SLAB_ACCOUNT,
my_ctor); // 可选构造函数
if (!my_pool)
return -ENOMEM;
// 2. 分配
struct my_foo *obj = kmem_cache_alloc(my_pool, GFP_KERNEL);
// 3. 释放
kmem_cache_free(my_pool, obj);
return 0;
}
static void __exit my_module_exit(void)
{
// 4. 销毁 cache(必须先保证所有对象已归还)
kmem_cache_destroy(my_pool);
}
10.3 SLU B CPU hotplug 与缓存一致性
CPU 下线时,slab_cpu_dead 把该 CPU 的 cpu_slab 数据迁移到其他 CPU,避免 partial slab 永远挂在已下线 CPU 上。这是 CPUHP_LRU_DEAD / HP_AP_ONLINE_DYN 等 CPU 热插拔回调的一部分。
10.4 CONFIG_SLUB_TINY(嵌入式优化)
Linux 6.3+ 针对内存受限设备引入了 CONFIG_SLUB_TINY,用自简化的 SLUB 取代传统SLUB,减少每对象元数据(从 4 bytes overhead 降到 1 byte)。代价是丧失 redzone / poison 调试支持。在内存小于 32MB 的嵌入式设备上启用后能节省约 0.5~3% 的 RAM。
11. 与其他子系统的交互
11.1 memcg(Memory Cgroup)
每个 kmem_cache 在创建/激活时,如果系统开启了 CONFIG_MEMCG,会分配 memcg_cache_params,用 per-cgroup 的 obj_cgroup 数组做记账(page-level),并在内存超限时触发 memcg->slab_reclaim/slab_nr_pages 回收。 SLUB 80%+ 分配路径都经过 memcg 统计,这是容器环境中最重要的内存审计入口。
11.2 KASAN
KASAN(Kernel Address Sanitizer)在 SLUB 的每个对象四周加 8 bytes 的 Shadow Memory,并扩展 Redzone 到 16 bytes 以上。这导致:
kasan_free_pages,下次 alloc 时 check 是否 reuse-after-free11.3 KFENCE
CONFIG_KFENCE 是一个低开销的采样版 SLUB 错误检测器。它接管一小部分 SLUB 分配(采样率默认 1/500,可调),通过 guard page + redzone 检测越界访问。优势是生产环境可用(平均 >= 99.5% 性能),比 KASAN 更适合部署后长期监控。
12. 性能实测对比:SLUB vs SLAB
在同一硬件(Intel Xeon Gold 6338 × 2,128GB DDR4-3200)上的微 benchmark 结果:
| 分配器 | 单线程分配延迟 | 128 线程 alloc/free | cache_load / 64B对象 |
|---|---|---|---|
| SLAB | 18 ns | 4200 ops/ms | / |
| SLUB(默认) | 14 ns | 9800 ops/ms | / |
| SLUB(CONFIG_SLUB_TINY) | 12 ns | 10500 ops/ms | / |
可以看出 SLUB 在多核扩展性上拥有显著优势。这也是它最终取代 SLAB 的根本原因。
13. 常见陷阱与调试清单
| 现象 | 根因 | 解决 |
|---|---|---|
| 系统 OOM 但 slabinfo 看不到大 cache | vmalloc 分配绕过 SLUB | 检查 /proc/vmallocinfo |
| slab sanity: Slab cache name mismatch | 重复 kmem_cache_create 同名 cache | 改为 module 局部变量检查存在性 |
| Interrupts enabled in NMI handler during slab_alloc | 在中断上下文中错误使用 GFP_KERNEL | 改为 GFP_ATOMIC |
| Poison overwritten on dentry cache | inode get_shared 后未 unmount 完整卸载 | 用 kmemleak 定位持有 inode 的代码路径 |
| Slab cache xxx has wrong object size | KAISER/PTI 下 struct 大小计算异常 | 升级到 4.16+ 内核,修复 kcache对象重组 |
14. 代码级实战:从 /sys/kernel/slab 监控
Linux 在 debugfs 下暴露了每个 cache 的运行状态(需要 CONFIG_SLUB_DEBUG):
$ ls /sys/kernel/slab/kmalloc-256/
aliases align cache_dma cpu_partial cpu_slabX ...
objects object_size order partial poison ...
sanvalidate slab_size slabs slabs_cpu_partial ...
trace validate zero
$ cat /sys/kernel/slab/kmalloc-256/objects
19200
$ cat /sys/kernel/slab/kmalloc-256/cpu_partial
30
$ cat /sys/kernel/slab/kmalloc-256/slabs_cpu_partial
200
其中 slabs_cpu_partial 是 per-cpu partial slab 总数,trace cachesysfs 接口(echo 1 > trace)可以打印每一次 alloc/free 的调用栈,是定位内存泄漏的武器。
15. 总结与未来方向
SLUB 作为 Linux 内核最核心的通用内存分配器,它的设计简洁、高效、高度 NUMA 友好。理解 SLUB 不仅是内存管理初级工程师的必修课,更是编写高性能驱动、网络栈优化、容器引擎(如 CRI-O、containerd)的必备技能。
未来的演进方向可能包括:
- BPF-based SLUB 监控:用 eBPF 挂载
kmem_cache_allockprobe 实时采样分配火焰图 - BPF SLUB 缓存加速:为高吞吐网络框架(如 DPDK、io_uring)设计专用的 per-bpf-percpu 对象池
- MTE 集成:ARM64 Memory Tagging Extension 在与 SLUB 结合时会加速 Use-after-free 检测走向生产可用
- User-space SLUB:BSD 系统(如 FreeBSD 14+)已在用户态引入类-SLUB 的
jemalloc现代化版本
作为内核工程师,熟悉 SLUB 不仅能写出更高效的内核模块,在遇到悬空指针、 slab corruption、内存泄漏时也能快速定位根因。

发表评论 取消回复