引言
操作系统内核的内存管理是整个系统性能的基石。在 Linux 内核中,伙伴系统(Buddy System)负责以页(Page)为单位管理物理内存,而 Slab 分配器则在其之上构建了一套高效的对象级内存分配机制。理解 Slab 分配器,是从"会用 kmalloc"走向"理解内核内存治理"的关键一步。
1. 为什么需要 Slab 分配器?
伙伴系统以页(通常 4KB)为粒度分配内存,而内核中大量数据结构(task_struct、inode、dentry等)往往只有几百字节。如果直接用伙伴系统:
- 内部碎片严重:一个 200 字节的
task_struct占用整页 4KB,浪费超过 95%。 - 初始化开销大:每次创建进程都要分配、初始化
task_struct,但初始化逻辑是固定的。 - 内存碎片化:频繁分配/释放不同大小的对象导致无法合并成大页。
Slab 分配器的核心思想:对象缓存 + 复用构造。预分配一页内存,切分成多个固定大小的对象槽位,用过的对象归还后保留初始化状态,下次直接复用。
2. Slab 分配器架构层次
完整的对象分配路径由四层构成:
┌─────────────────────────────────────────────────┐
│ 上层 API: kmalloc / kzalloc / kmem_cache_alloc │
├─────────────────────────────────────────────────┤
│ 通用缓存层: kmalloc_caches[] (大小分级缓存) │
├─────────────────────────────────────────────────┤
│ Slab 缓存层: kmem_cache (每个缓存管理一类对象) │
├─────────────────────────────────────────────────┤
│ 伙伴系统层: alloc_pages / free_pages │
└─────────────────────────────────────────────────┘
2.1 Slab 缓存(kmem_cache)
每一种频繁分配的内核对象对应一个 kmem_cache。例如 task_struct 对应 task_struct_cachep,inode 对应 inode_cachep。每个缓存管理一组 slab,每个 slab 占用一页或多页,被切成固定大小的槽位。
2.2 Slab 的三种状态
- Full:所有槽位都已分配。
- Partial:部分槽位已分配,部分空闲——新分配优先从这里取。
- Empty:全部空闲,可被释放回伙伴系统。
2.3 分配策略
分配请求到达时的搜索顺序:
- 当前 CPU 的 CPU 本地缓存(per-cpu cache)——最快路径。
- 当前 NUMA 节点的 partial slab 链表。
- 其他节点的 partial slab 链表。
- 从伙伴系统申请新页,创建新 slab(慢路径)。
3. SLAB vs SLUB:演进的分配器实现
Linux 内核历史上存在三种 Slab 实现:
- SLAB:原始实现,元数据复杂,已逐渐弃用。
- SLOB:面向嵌入式的小内存实现。
- SLUB:现代默认实现,简化元数据、NUMA 友好、调试能力强。
SLUB 的核心简化思想:
- 取消独立的 slab 管理结构,复用
struct page中的字段。 - 每个 slab 只维护一个空闲链表头(freelist),空闲对象本身存储链表指针。
- CPU 本地缓存使用
page的 partial 链表,避免额外分配。
4. 工程实践:如何创建自定义缓存
驱动或子系统可以创建专属的对象缓存:
/* 定义缓存指针 */
static struct kmem_cache *my_cachep;
/* 模块初始化时创建缓存 */
static int __init my_init(void)
{
my_cachep = kmem_cache_create("my_struct",
sizeof(struct my_obj),
0, /* alignment */
SLAB_HWCACHE_ALIGN,
NULL); /* ctor */
if (!my_cachep)
return -ENOMEM;
return 0;
}
/* 分配/释放对象 */
struct my_obj *obj = kmem_cache_alloc(my_cachep, GFP_KERNEL);
kmem_cache_free(my_cachep, obj);
/* 模块退出时销毁缓存 */
static void __exit my_exit(void)
{
kmem_cache_destroy(my_cachep);
}
5. 调试与问题排查
5.1 SLUB 调试选项
内核配置 CONFIG_SLUB_DEBUG 开启后可使用:
slub_debug=UZP:开启 User Poison(释放后填充 poison)、Red Zone(越界检测)、Tracking(记录分配/释放调用栈)。/proc/slabinfo:查看所有 slab 缓存的实时统计。/sys/kernel/slab/<cache>/:每个缓存的调试参数和跟踪信息。
5.2 常见内存踩踏排查
- 释放后使用(Use-After-Free):对象被释放后 freelist 中填充
0x6b(POISON_FREE),访问时触发异常。 - 越界写入:Red Zone 区域被修改,内核打印
SLUB: Redzone overwritten。 - 重复释放:检测 freelist 中出现同一对象两次。
5.3 slabtop 实时监控
类似 top 命令的 slab 分配监控工具:
$ slabtop -o
Active / Total Objects (% used) : 1245893 / 1279634 (97.4%)
Active / Total Slabs (% used) : 35200 / 35200 (100.0%)
Active / Total Caches (% used) : 81 / 139 (58.3%)
Active / Total Size (% used) : 282.68K / 288.01K (98.1%)
Minimum / Average / Maximum Object : 0.01K / 0.23K / 8.00K
OBJS ACTIVE USE OBJ SIZE SLABS OBJ/SLAB CACHE SIZE NAME
96192 96192 100% 0.06K 1503 64 6012K kmalloc-64
70098 69896 99% 0.19K 1669 42 6676K dentry
60864 60536 99% 0.12K 951 64 3804K kernfs_node_cache
32016 32016 100% 1.00K 501 64 8016K ext4_inode_cache
30976 30932 99% 0.57K 553 56 8848K task_struct
6. 性能调优要点
- HWCACHE_ALIGN:cache line 对齐,避免 false sharing。
- 对象复用:高频创建/销毁的对象必须用 slab 缓存,不要直接用
kmalloc。 - NUMA 亲和:多节点系统优先使用当前节点的缓存。
- 避免大对象:超过 1 页的对象直接走伙伴系统更高效。
7. 内核演化趋势
当前 Linux 内核社区正在推进几个方向:
- KF_SHARED_MEDIUM:更灵活的对象分组。
- Memcg 集成:slab 分配计入 cgroup 内存限制,容器场景下更精确的内存管控。
- Folio 页:正在将 slab 与 folio(复合页)框架整合,进一步简化大页管理。
总结
Slab 分配器是 Linux 内核内存管理的重要中间层,它将伙伴系统的页级分配细化为对象级的高效复用。掌握 Slab/SLUB 的原理和调试手段,对于驱动开发、性能优化和故障排查都至关重要。从 kmem_cache_create 到 slab_debug,每一层都藏着性能提升和 bug 定位的钥匙。

发表评论 取消回复