引言:为什么需要 Slab 分配器?
在 Linux 内核中,内存管理是一个至关重要的话题。操作系统需要高效地管理物理内存,为各种内核子系统提供内存分配服务。很多人了解伙伴系统(Buddy System),它在页级别(通常4KB)管理物理内存,但伙伴系统存在一个关键问题——它无法高效处理小对象的频繁分配与释放。
当内核需要为 task_struct、inode、dentry 等数据结构分配内存时,它们通常只有几百字节大小。如果每次分配都使用伙伴系统申请一整页(4KB),会造成严重的内部碎片。此外,频繁的对象创建和销毁还会带来初始化开销和内存碎片问题。
Slab 分配器正是在这种背景下诞生的。它由 SunOS 开发者 Jeff Bonwick 于 1994 年提出,随后被引入 Linux 2.1.23 版本,成为内核内存管理的核心组件之一。本文将从原理到实践,深度剖析 Slab 分配器的设计哲学、实现机制和最佳实践。
一、Linux 内存管理全景图
理解 Slab 分配器之前,需要先了解它在整个内存管理体系中的位置:
应用程序 malloc()/free()
↓
brk()/mmap() 系统调用
┌────────────────────────┐
│ 虚拟内存管理 (VMA) │
├────────────────────────┤
│ 页分配器 (Page Allocator) │
│ ← 伙伴系统 (Buddy System) │
├────────────────────────┤
│ Slab/Slub/Slob 分配器 │ ← 本文主角
├────────────────────────┤
│ 物理页 (Physical Pages) │
└────────────────────────┘
Slab 分配器位于伙伴系统之上,作为对象级别的缓存层。它从伙伴系统获取页面,并将页面划分为更小的对象,提供给内核各类子系统使用。这种分层设计既避免了伙伴系统内部碎片的问题,又减少了对象初始化的_cpu_开销。
二、Slab 分配器的核心设计理念
2.1 对象缓存(Object Cache)
Slab 分配器的核心思想是对象缓存。对于每种频繁使用的内核对象(如 task_struct、file、inode 等),Slab 维护一个专用的缓存池(kmem_cache)。对象从缓存池中分配,释放后也归还到池中,而不需要每次执行完整的初始化(构造)和析构流程。
这种设计基于一个关键观察:内核对象的初始化操作(如设置锁、链表头、引用计数等)往往开销不小,且这些初始化内容在对象生命周期内保持不变。通过重用已初始化的对象,Slab 大幅减少了这部分开销。
2.2 硬件缓存友好
Slab 的名称来源于其内存组织方式——将内存视为连续的"板块"(Slab)。每个 Slab 是一个或多个连续的物理页面,其中等大小的对象紧密排列。这种组织方式天然具有良好的空间局部性,有利于 CPU 缓存命中。
2.3 缓存着色(Cache Coloring)
现代 CPU 的 L1/L2 缓存采用组相联映射策略。如果多个 Slab 中相同偏移的对象映射到同一缓存行,会导致缓存冲突(Cache Thrashing)。Slab 分配器通过"着色"——在不同 Slab 之间加入不同大小的偏移量——来解决这个问题,使得相同偏移的对象映射到不同的缓存行,从而减少冲突。
2.4 构造与析构(Constructor/Destructor)
每个 kmem_cache 可以指定构造函数和析构函数。当从缓存中获取一个空闲对象时,之前设置的构造函数状态可能仍然存在,因此分配时无需重新初始化。只有在首次从伙伴系统获取页面时,才需要对全部对象调用构造函数。
三、核心数据结构
3.1 kmem_cache
kmem_cache 是 Slab 分配器的核心抽象,代表一种特定类型的对象缓存。其关键成员如下:
struct kmem_cache {
struct kmem_cache_cpu *cpu_slab; // 每CPU高速缓存
unsigned long flags; // 标志位(如 SLAB_POISON、SLAB_RED_ZONE)
unsigned int size; // 对象实际大小
unsigned int object_size; // 包含元数据的对象大小
unsigned int offset; // 空闲对象链表指针的偏移
struct kmem_cache_node *node[MAX_NUMNODES]; // NUMA节点上的缓存
const char *name; // 缓存名称(如 "task_struct")
int refcount; // 引用计数
void (*ctor)(void *); // 构造函数
struct list_head list; // 全局链表
unsigned int colour_off; // 着色偏移量
unsigned int colour; // 颜色数量
unsigned int colour_next; // 下一个颜色值
gfp_t allocflags; // 分配标志
unsigned int freelist_size; // 空闲列表大小
};
其中 cpu_slab 是指向每 CPU 缓存的指针。现代 Slub 分配器使用每 CPU 缓存实现无锁分配,这是性能和可扩展性的关键。
3.2 kmem_cache_node
在 NUMA 架构下,每个 NUMA 节点维护独立的缓存数据:
struct kmem_cache_node {
spinlock_t list_lock; // 保护链表的自旋锁
unsigned long nr_partial; // 部分空闲Slab数量
unsigned long nr_slabs; // 该节点Slab总数
struct list_head partial; // 部分空闲Slab链表
struct list_head full; // 全满Slab链表
};
3.3 Slab 描述符(slab)
在 4.x 内核之后,slab 描述符被">
在 4.x 内核之后,slab 描述符被移到页面结构体(struct page)中,不再使用独立的 slab 管理结构。每个 Slab 起始位置的 struct page 中存储了指向 kmem_cache 和空闲对象的指针链。
四、Slab 的内存布局
4.1 单 Slab 内部结构
┌──────────────────────────────────────────────────┐
│ struct page + slab 元数据(kmem_cache 指针等) │
├──────────────────────────────────────────────────┤
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Object 0 │ │ Object 1 │ │ Object 2 │ ... │
│ ├──────────┤ ├──────────┤ ├──────────┤ │
│ │ 已使用 │ │ 空闲 │ │ 已使用 │ │
│ │(无指针) │ │(指向下一 │ │(无指针) │ │
│ │ │ │ 空闲对象) │ │ │ │
│ └──────────┘ └──────────┘ └──────────┘ │
├──────────────────────────────────────────────────┤
│ 着色区域 (coloring space) │ ← 用于缓存着色
└──────────────────────────────────────────────────┘
空闲对象通过一个单向链表串联,freelist 指针保存在空闲对象的内存区域中。有数据表明空闲指针紧挨着元数据存储,利用对象末尾的未使用空间,避免了额外的内存开销。
4.2 Slab 的状态
一个 Slab 可能处于以下三种状态之一:
- Empty(空):所有对象都空闲,通常保留或可归还给伙伴系统
- Partial(部分使用):部分对象已分配,部分空闲
- Full(满):所有对象都已分配
五、内存分配流程详解
从 kmem_cache_alloc() 开始的完整分配流程:
kmem_cache_alloc(cache, flags)
│
├── 1. 尝试从 cpu_slab (per-CPU freelist) 获取 ← 快速路径
│ 如果 freelist 非空 → 直接返回对象,仅 O(1) 操作
│
├── 2. 如果 cpu_slab 为空,尝试从 kmem_cache_node 转移
│ ├── 从 node->partial 链表获取一个部分Slab
│ │ 将该 Slab 设为 cpu_slab 的 freelist
│ │ 返回第一个空闲对象
│ │
│ └── 如果 partial 也为空 → 进入步骤3
│
├── 3. 从伙伴系统 (buddy allocator) 分配新页
│ ├── alloc_pages_node() 获取可用页
│ ├── 将页初始化为 Slab 状态
│ ├── 设置每个对象的构造(如果是首次分配)
│ ├── 将新 Slab 加入 partial 链表
│ └── 返回第一个对象
│
└── 4. 分配失败处理(__GFP_NOFAIL 等场景)
通过内存回收机制或分配器重试
步骤1即是快速路径(fast path),现代 Slub 分配器通过 per-CPU 缓存使其成为绝大多数分配的路径,完全无锁。步骤2是慢速路径(slow path),仅在 CPU 缓存为空时触发。步骤3最慢,涉及向伙伴系统申请新页面。
六、内存释放流程详解
kmem_cache_free() 释放对象的流程:
kmem_cache_free(cache, objp)
│
├── 1. 将对象放回 cpu_slab 的 freelist(快速路径,无锁)
│
├── 2. 如果 cpu_slab 已满,执行 flush操作:
│ ├── cpu_slab 原 freelist 转移到 kmem_cache_node
│ │ (将对应 Slab 从 partial 移到 full)
│ ├── 如有任何空 Slab,释放回伙伴系统(可选)
│ └── 尝试获取一个 partial Slab 作为新 cpu_slab
│
└── 3. 释放过程中可能触发内存压缩:
如果所有 Slab 都被使用且没有空间退还伙伴系统
内核定期通过 shrink 接口触发页面回收
七、三种 Slab 实现的对比
Linux 内核提供了三种 Slab 分配器实现,它们共享相同的 API,但内部实现策略不同。内核编译时通过 CONFIG_SLAB / CONFIG_SLUB / CONFIG_SLOB 选择:
| 特性 | Slab(原始) | Slub(默认) | SLOB |
|---|---|---|---|
| 全名 | Slab Allocator | Unqueued Slab | Simple List Of Blocks |
| 复杂度 | 高(大量元数据) | 中等(精简元数据) | 低(简单链表) |
| 性能 | 中等 | 高(默认选择) | 低 |
| 内存占用 | 较高 | 较低 | 最低 |
| 适用场景 | 早期系统 | 服务器、通用桌面 | 嵌入式系统 |
| 支持 NUMA | 完全 | 完全 | 基础支持 |
| 调试功能 | 齐备 | 齐备 | 基础 |
Slub 自 2.6.23 成为默认分配器,它在保持接口兼容的前提下大幅简化了代码路径,减少了每 CPU 锁操作,提升了多核扩展性。
八、调试与监控工具
8.1 /proc/slabinfo
catslabinfo 提供了所有活跃缓存的详细统计:
$ cat /proc/slabinfo | head -20
slabinfo - version: 2.1
# name <active_objs> <num_objs> <objsize> <objperslab> <pagesperslab> : tunables <limit> <batchcount> <sharedfactor> : slabdata <active_slabs> <num_slabs> <sharedavail>
kmalloc-8192 18 18 8192 4 8 ...
kmalloc-4096 152 174 4096 8 8 ...
kmalloc-2048 327 410 2046 16 8 ...
kmalloc-1024 873 1056 1024 16 4 ...
task_struct 309 476 5800 5 8 ...
mm_struct 70 120 1624 20 8 ...
各列含义:名称、活跃对象数、总对象数、对象大小、每Slab对象数、每Slab页数、各调优参数、活跃Slab数、总Slab数。
8.2 slabtop
实时查看前 N 个缓存的使用情况,类似 top 命令。
8.3 内存泄漏检测
当怀疑 Slab 内存泄漏时,可利用 SLAB_POISON 标志和 /proc/slabinfo 监控。结合 perf、systemtap 或 BPF 工具,可实时追踪对象分配和释放行为。
九、Slab 在真实子系统中的运用
9.1 VFS 层
Linux 虚拟文件系统使用 Slab 管理 inode、dentry、file 对象。这些对象频繁创建和销毁,使用 Slab 缓存极大提升了文件系统性能。例如 inode_cachep 是专用的 inode 缓存,dentry_cache 管理 dentry。
9.2 进程管理
task_struct 通常占据几个 KB 内存,每次 fork/vfork 都需要通过 task_struct_cachep(或其内部调用)获取。
9.3 网络协议栈
套接字(socket)、路由缓存(rtable)、sk_buff 等网络对象全部通过 Slab 分配。高性能网络应用中,Slab 性能直接影响网络吞吐量。
十、性能优化最佳实践
- 批量分配:如有大量同类型对象需求,考虑使用内存池或预分配策略,减少逐对象分配的开销。
- 避免跨节点访问:NUMA 架构下,使用
kmalloc_node()按节点分配内存,减少跨 CPU 节点访问延迟。 - 合理设置缓存大小:通过
/proc/slabinfo和slabtop 监控对象使用模式,调整limit和batchcount参数。 - 利用 SLAB_HWCACHE_ALIGN:将对象对齐到硬件缓存行大小(如64字节或128字节),避免伪共享(false sharing)。
- SLAB_ACCOUNT:在内存 cgroup 环境下,使用
SLAB_ACCOUNT确保对象被正确记账,便于精确统计和控制。
十一、Slab 与当今的云原生内存管理
在容器化和云原生时代,Slab 分配器面临着新的挑战:
- 多租户隔离:通过 memory cgroup 实现 Slab 记账与限额控制
- 内存回收压力:容器 OOM 回收时 Slab 释放策略直接影响系统稳定性
- 大规模分配效率:高密度工作负载下,传统的单 Slab 设计是否需要优化
- 瞬时工作负载:频繁的 scale up/down 要求 Slab 具有低初始化和高效回收能力
Linux 内核社区持续对 Slab 分配器进行优化,在保持简洁性的同时提升其在不同工作负载下的表现。
总结
Slab 分配器作为 Linux 内存管理的核心组件,以其精巧的设计思想影响深远。从对象缓存减少初始化开销、per-CPU 缓存实现无锁分配到缓存行着色提升硬件缓存利用率,每一个设计点都体现了跨层设计和硬件感知的理念。
理解 Slab 分配器不仅是学习内核内存管理的必经之路,也是编写高性能内核模块的基础。无论是驱动开发、文件系统实现还是网络协议栈优化,良好的 Slab 使用实践都可能带来显著的性能提升。
在未来,随着非易失内存(NVM)、CXL 互联内存等新硬件的出现,Slab 分配器也将继续演化,继续作为 Linux 内核内存管理的坚实基石。

发表评论 取消回复