引言
Linux 内核的内存管理子系统是整个系统性能的关键基石。当应用程序调用 kmalloc()、kmem_cache_alloc() 或 vmalloc() 时,内核必须在微秒级的延迟内完成内存分配,同时还要应对内存碎片、NUMA 拓扑、并发竞争等复杂挑战。本文将从伙伴系统(Buddy System)的底层算法出发,深入剖析 Slab 与 Slub 分配器的完整实现链路,结合真实生产案例,给出系统性的调优方法论。
一、伙伴系统(Buddy System):物理内存的分配基石
1.1 核心设计思想
伙伴系统是 Linux 内核管理物理内存页的基础算法。它将所有物理页面按 2 的幂次(order)组织成 11 个链表(order 0~10),分别管理 1、2、4、8...1024 个连续物理页框。分配时向上取整到最近的 2^n,若该 order 无空闲则从更大 order 递归拆分;释放时检查"伙伴"(地址相邻且大小相同)是否空闲,若空闲则向上合并。
1.2 关键数据结构
// mmzone.h
struct free_area {
struct free_list free_list[MIGRATE_TYPES];
unsigned long nr_free;
};
struct zone {
struct free_area free_area[MAX_ORDER]; // 11 个 orders
...
};
每个内存域(Zone)维护一组 free_area,每种迁移类型(MIGRATE_UNMOVABLE、MIGRATE_MOVABLE、MIGRATE_RECLAIMABLE 等)独立管理,从源头减少碎片。
1.3 分配路径详解
alloc_pages() 是伙伴系统的入口函数。其执行路径为:alloc_pages() → alloc_pages_node() → __alloc_pages_nodemask()。后者依次经过快速路径(get_page_from_freelist,基于 per-CPU hot page cache)、慢速路径(__alloc_pages_slowpath,触发 compaction/reclaim)和 OOM 兜底(out_of_memory())。
1.4 页面迁移与反碎片
物理页按迁移类型分类,将不可移动页(kernel 线性映射)与可移动页(用户态匿名页、文件缓存)隔离。内存紧缩(compaction)阶段可回收/迁移可移动页,整理出连续大块内存。相关 sysctl 参数:vm.extfrag_threshold(外部碎片阈值,默认 500)、vm.min_free_kbytes(最小保留内存)。
二、Slab 分配器:小对象高性能分配
2.1 为什么需要 Slab
伙伴系统以页(通常 4KB)为单位分配,而内核中大量数据结构(task_struct、dentry、inode、file 等)远小于一页。若直接用伙伴系统,内部碎片严重(如 96 字节的对象放在 4096 字节的页中,浪费 97%),且频繁分配/释放带来高昂的初始化/销毁开销。Slab 分配器正是为这一问题而生。
2.2 三层架构
Slab 采用 kmem_cache → slab → object 的三层结构:
- kmem_cache:描述一种对象类型的缓存,记录对象大小、对齐、构造函数、着色偏移等信息。系统启动时会为常用内核对象自动创建对应的 cache(如
task_struct_cachep、dentry_cache)。 - slab:每个 slab 占一页或多页连续内存,内部等分为 N 个对象槽位。Slab 有三种状态:full(全部分配)、partial(部分分配)、free(全空闲)。
- object:实际分配给调用者的内存单元,有 inuse 和 free 两种状态。
2.3 kmem_cache 创建与销毁
// 创建专用 cache
struct kmem_cache *kmem_cache_create(
const char *name, // cache 名称
size_t size, // 对象大小
size_t align, // 对齐要求
unsigned long flags, // SLAB_HWCACHE_ALIGN 等
void (*ctor)(void *) // 构造函数
);
// 销毁 cache(确保无活跃对象)
void kmem_cache_destroy(struct kmem_cache *cachep);
2.4 分配与释放路径
kmem_cache_alloc() 流程:
- 检查 per-CPU CPU slab cache(热路径,无锁)
- 从 kmem_cache_node 的 partial 链表取 slab
- partial 空则向伙伴系统请求新页面,初始化 slab
- 返回对象指针(free 列表中弹出)
kmem_cache_free() 反向操作:对象归还到 slab 的 free 列表,若 slab 从 partial 变为 empty 则归还给 node 管理,可释放回伙伴系统。
2.5 缓存着色(Cache Coloring)
现代 CPU L1/L2 cache 使用 set-associative 映射。若大量 slab 起始地址相同偏移的对象被访问,会造成 cache line 冲突(cache thrashing)。着色机制在每个 slab 初始化时加入随机的 coloroff 偏移,使不同 slab 中同序号对象映射到不同 cache set。相关字段:kmem_cache.colour_off(每次递增 cache line 大小)、kmem_cache.colour(颜色总数)。
三、Slub 分配器:Slab 的继任者
3.1 Slub 解决的核心痛点
随着 NUMA 系统和核心数增长,经典 Slab 暴露三个问题:
- 每个 node 的 partial 链表在无 NUMA 系统中无法利用本地节点优势,链表操作开销大
- 调试代码(如 redzone、poison)增加元数据开销,导致对齐和着色复杂化
- 代码可维护性差,多种分配路径条件分支交织(SLOB、SLAB、Slub 共存)
Slub(Unqueued Slab)从 2.6.23 起作为默认分配器,去掉了 Slab 的复杂队列机制,简化为"CPU 本地 slab + node partial 链表"的两层结构。
3.2 Slub 核心数据结构
struct kmem_cache_cpu {
void **freelist; // 指向下一个空闲对象
unsigned long tid; // 全局唯一事务 ID(防止 ABA)
struct page *page; // 当前 CPU 使用的 slab 页
struct page *partial; // CPU local partial 链表(v5.9+)
};
struct kmem_cache_node {
spinlock_t list_lock;
unsigned long nr_partial;
struct list_head partial; // node 级 partial slab 链表
};
3.3 Redzone 与 Poison 机制
当开启 CONFIG_DEBUG_SLAB 时,Slub 在对象边界放置 redzone 标记(0x12345678 模式),释放时校验是否被踩。POISON 在释放时用固定模式(如 0x5a)填充对象内存,使 use-after-free 即时暴露。
3.4 Slub 分配流程图
slab_alloc() 执行流程:
- 读 per-CPU freelist(无锁快速路径)
- freelist 空但 page->partial 非空 → 从 local partial 取 slab
- local partial 空 → 加 node 锁,从 node->partial 链表移动一个 slab 到 CPU
- node 也没 partial → 释放锁,向伙伴系统请求新页面(
new_slab()),获得后初始化 freelist - 返回对象指针并更新 freelist(指针跳转到对象内嵌入的 next 指针)
四、生产环境实战:诊断与调优
4.1 读懂 /proc/slabinfo
$ cat /proc/slabinfo | head -20
# name <active_objs> <num_objs> <objsize> <objperslab> <pagesperslab> : tunables ...
dentry 1204567 1589920 192 20 2 : tunables ...
inode_cache 982341 1105080 648 5 2 : tunables ...
kmalloc-64 452312 512000 64 62 1 : tunables ...
关键字段含义:active_objs 已分配对象数、num_objs 总对象数(active + free)、objsize 对象大小、objperslab 每 slab 对象数、pagesperslab 每 slab 占用页数。碎片率 = 1 - active_objs / num_objs,若接近 1 说明该 cache 存在大量未使用对象。
4.2 slabtop 实时监控
sudo slabtop -o --sort=a
OBJS ACTIVE USE OBJ SIZE SLABS OBJ/SLAB CACHE SIZE NAME
512k 512k 100% 0.00K 128 4 128k kmalloc-128
320k 298k 93% 0.19K 60 10 240k dentry
256k 220k 86% 0.25K 32 8 256k inode_cache
4.3 Reaper 与主动回收
内核线程 kwork_reaper 通过 kmem_cache_shrink() 扫描所有 cache 的空闲 slab 并归还伙伴系统。Slub 引入"周期性 slab 老化"机制:slab_alloc_node() 分配失败时检查 node->nr_partial,若 min_partial 较大则触发回收。关键参数:
vm.slub_min_objects:node partial 最低水位线下限vm.slab_ratio:每个 cache 应分配的 page 数量与 node 总内存比例min_partial:node 维护的最小 partial slab 数,默认值与 node 内存大小成正比
4.4 OOM 场景分析
当伙伴系统所有 order 都无空闲且回收也无法满足时,触发 OOM killer。OOM 评分算法(oom_badness())考虑进程占用内存、运行时间、oom_score_adj、是否为 init 进程等。Slab 泄漏是常见诱因:若某 cache 持续分配却不释放(如未关闭的 fd、未清理的 timer),会导致num_objs - active_objs 持续增加,进而耗尽某 zone 内存。
4.5 常见内存碎片处理
# 查看碎片指数
cat /proc/buddyinfo
cat /proc/pagetypeinfo
# 手动触发内存紧凑
echo 1 > /proc/sys/vm/compact_memory
# 调整 compaction 激进程度
sysctl -w vm.extfrag_threshold=100 # 更积极(默认 500)
sysctl -w vm.zone_reclaim_mode=1 # NUMA 开启本地回收
五、Slab 与 Slub 关键差异总结
| 维度 | Slab | Slub |
|---|---|---|
| 队列复杂度 | 每 CPU + 每 node 三级队列(full/partial/empty) | 两层:CPU slab + node partial 链表 |
| 调试开销 | 元数据在 slab 外部,coloring 与 redzone 交织 | 元数据嵌入页结构,统一 redzone + poison |
| NUMA 友好度 | empty slab 分配到 node 可能不均衡 | 本地 node 优先 + fallback 策略更明确 |
| 默认启用 | 2.6.23 之前的默认分配器 | 2.6.23 至今的默认分配器(x86/ARM) |
| 代码体量 | ~5000 行 | ~2000 行(更简洁) |
六、性能调优实践建议
- 监控碎片率:通过
slabtop -s c按缓存大小排,若某 cache 碎片率持续 > 50%,排查对应的内核泄漏点。 - 合理设置 min_free_kbytes:HPC / 数据库场景建议物理内存的 2%~5%,保证 compaction 和紧急分配有足够预留。
- 开启 compaction:大块内存分配频繁的场景(DPDK、GPU 直通),建议设为
vm.extfrag_threshold=100。 - 关闭不必要的调试:生产环境不要使用
slab_debug=FPZU,每个对象增加 ~40 字节开销。 - NUMA 感知:关键线程绑核 +
mbind()本地分配,减少跨 node 访问。配合vm.zone_reclaim_mode=1和numactl --localalloc。 - dentry 缓存控制:海量文件操作场景,定期
sync && echo 3 > /proc/sys/vm/drop_caches清理 dentry/inode cache,避免 slab 膨胀。
七、前沿演进:SLUB-TINY 与 Rust 抽象
针对嵌入式/小内存场景,SLUB-TINY(~Linux 6.4)将对象级元数据进一步压缩,省去 free list pointer,改用 slab 内偏移计算。Linux 6.x 开始,Rust 模块可利用 SlabAllocator trait 为驱动提供类型安全的 cache 分配接口。随着 CXL.mem 和 tiered memory 的引入,Slub 的 node fallback 链表也将扩展为"DRAM → CXL tier"的多层结构。
八、总结
伙伴系统通过 2^n 阶连续页面管理物理内存,是内核内存的底层银行。Slab/Slub 在其上构建对象级分配池,通过着色、per-CPU hot cache 和延缓归还等策略实现微秒级分配延迟、高 cache 命中率与低碎片化的平衡。深入理解这套机制,是性能调优、故障排查和内核开发的必经之路。

发表评论 取消回复