引言
Linux 内核的内存管理是系统性能的核心支柱之一。在物理内存分配层面,伙伴系统(Buddy System)以页(通常为 4KB)为粒度管理内存,但实际应用中,内核经常需要分配远小于一页的对象——如 task_struct(约 1.7KB)、inode、dentry 等。如果直接用页级分配器来处理这些小型对象,将造成严重内存碎片。Slab 分配器应运而生,它作为伙伴系统之上的二级分配器,专门解决内核中小对象的分配与释放问题。
1. Slab(Slab Allocator)的演进历程
1.1 经典 Slab(Linux 2.1.x)
1994 年,Jeff Bonwick 在 Solaris 2.4 中首次提出了 Slab 分配器的设计思想。Linux 在 2.1.x 版本中引入了原版 Slab 分配器,其核心思想是对象缓存(Object Cache)——预先分配并初始化好对象实例,下次分配时直接取出使用,避免重复的构造/析构开销。
1.2 SLUB(Unqueued Slab)—— 现代默认分配器
SLUB(Simple Linear Mapping Unqueued Allocator)由 Christoph Lameter 在 Linux 2.6.23 中引入,现在是大多数 Linux 发行版的默认分配器。SLUB 简化了 SLAB 的队列管理,每个 CPU 维护独立的 slab 列表(kmem_cache_cpu),消除了多处理器环境下的队列锁争用,大幅提升了 SMP 系统的扩展性。
1.3 SLOB(Simple List Of Blocks)—— 嵌入式优化
SLOB 专为内存极度受限的嵌入式系统设计。它使用简单的首次适应(first-fit)算法管理内存块,代码量极小(约 2000 行),但外部碎片严重。适合资源受限的 IoT 设备和嵌入式 Linux 平台。
1.4 选择策略
| 分配器 | 适用场景 | 特点 |
|---|---|---|
| SLUB | 通用服务器、桌面 | 默认选择,性能最佳 |
| SLAB | 调试场景 | 提供更丰富的调试信息 |
| SLOB | 嵌入式设备 | 最小代码占用,内存碎片多 |
2. SLUB 核心数据结构
2.1 kmem_cache
struct kmem_cache 是 SLUB 最核心的数据结构,代表一个"对象缓存"。每个内核子系统在初始化时会创建自己的缓存,例如 task_struct_cachep 用于进程描述符,inode_cachep 用于索引节点。
struct kmem_cache {
struct kmem_cache_cpu *cpu_slab; // 每CPU的slab页
unsigned long flags; // 标志位
unsigned int size; // 对象实际大小
unsigned int object_size; // 用户请求的原始大小
unsigned int offset; // 下一个空闲对象的偏移
struct kmem_cache_node *node[MAX_NUMNODES]; // NUMA节点管理
struct kmem_cache_order_objects oo; // 每页容纳的对象数
...
};
2.2 kmem_cache_cpu(每 CPU 热路径)
这是 SLUB 性能优化的关键。每个 CPU 维护自己的 kmem_cache_cpu 结构,分配时直接从 CPU 本地 slab 页取空闲对象,完全不需要加锁。
struct kmem_cache_cpu {
void **freelist; // 空闲对象链表头
unsigned long tid; // 事务ID,检测并发
struct page *page; // 当前活跃slab页
struct page *partial; // 部分空闲的slab页链表
};
2.3 kmem_cache_node(NUMA 节点级)
当 CPU 本地 slab 页没有空闲对象时,SLUB 会从 NUMA 节点的 partial 链表中获取部分空闲的 slab 页。kmem_cache_node 使用 spinlock 保护,是二级备用路径。
struct kmem_cache_node {
spinlock_t list_lock;
unsigned long nr_partial;
struct list_head partial; // 部分空闲slab页的链表
...
};
3. SLUB 分配与释放流程
3.1 快速分配路径(Hot Path)
SLUB 的分配优化遵循一个原则:尽可能在 CPU 本地无锁完成。快速路径的完整流程:
- 检查
kmem_cache_cpu->freelist是否非空 - 若非空,从中取出第一个空闲对象(指针解链),直接返回
- 更新 freelist 指针,原子操作完成
- 整个过程无锁、无中断禁用,性能极高
3.2 慢速分配路径
当快速路径失败时进入慢速路径:
- 检查
kmem_cache_cpu->partial(CPU 本地的部分空闲 slab 页) - 检查
kmem_cache_node->partial(NUMA 节点的部分空闲 slab 页),需要获取 spinlock - 如果仍无可用对象,向伙伴系统申请新的 slab 页(
alloc_pages()) - 将新页加入 CPU 本地 slab 管理
3.3 释放流程
释放时同样优先操作 CPU 本地:
- 将对象链入
freelist(头插法) - 检查 slab 页是否变为"全空闲"状态
- 若是,将页从 CPU 本地移除,归还伙伴系统(减少内存占用)
- 若变为"部分空闲",链接入 partial 链表
4. 内存碎片对抗策略
4.1 Slab 着色(Slab Coloring)
"着色"是一种减少缓存冲突的精妙技巧。Slab 创建时,在不同 slab 页之间加入随机偏移量(color offset),改变了对象在缓存行中的位置。这避免了多个 slab 中相同偏移的对象映射到同一缓存行,减少缓存抖动(cache thrashing)。
着色范围由 colour_off(通常为处理器缓存行大小,即 64 字节)和总着色空间(colour)决定,每个 slab 页的偏移为 colour_off * n(n = 0, 1, 2, ... colour-1)。
4.2 对象对齐与大小计算
SLUB 根据 kmalloc 请求的大小自动选择最合适的 slab 缓存。对于 kmalloc 系列 API,内核维护了一组预创建的"通用"缓存,覆盖从 8B 到 8KB(通常以 2 的幂次递增)的常用大小。当请求大小不精确匹配时,向上取整到最近的可用大小——例如请求 600 字节会使用 1024 字节的缓存。
5. 调试与诊断工具
5.1 slabinfo 与 /proc/slabinfo
/proc/slabinfo 提供了全局 slab 分配器状态的概览输出,包含每个缓存的名称、活跃对象数、总对象数、每个对象的大小、页数等信息。
$ 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-1024 1440 1440 1024 4 1 : tunables 0 0 0 : slabdata 360 360
task_struct 380 416 1664 2 1 : tunables 0 0 0 : slabdata 208 208
inode_cache 956 1050 384 8 1 : tunables 0 0 0 : slabdata 132 132
dentry 2880 2880 192 20 1 : tunables 0 0 0 : slabdata 144 144
vm_area_struct 3514 3520 88 45 1 : tunables 0 0 0 : slabdata 79 79
5.2 slabtop 实时监控
slabtop 是 slab 版的 top 命令,可以实时观察各 slab 缓存的变化。按 O 键可按不同列排序,按 c 键可切换着色信息。
5.3 SLUB 调试模式
内核配置中启用 CONFIG_SLUB_DEBUG 后,SLUB 提供丰富的调试功能:
- Poison 检测:释放后的对象填充特定模式字节(0x5a 用于未初始化,0x6b 用于已释放)。访问这些字节意味着 use-after-free 或未初始化访问。
- Red Zoning:在对象尾部设置警戒区(Red Zone),检测 buffer overflow。
- Tracking:记录每次分配和释放的调用栈,帮助定位内存泄漏。
运行时开启调试:slub_debug=UZP(U=用户追踪,Z=Red Zone,P=Poison)。
6. 性能调优实践
6.1 高对象创建/销毁场景的优化
如果发现 /proc/slabinfo 中某个缓存的活跃对象与总对象比值很高(接近 1:1),说明该缓存的对象周转率极高。此时可调整以下参数:
batchcount:每次从 Node slab 移动多少对象到 CPU 本地。提高此值可减少锁争用。limit:CPU 本地空闲对象上限。对于高周转率的缓存,适当增加 limit 可减少回 Node 的频率。slab_min_objects:创建 slab 页时至少包含的对象数。增大可减少 partial 页碎片。
6.2 减少 slab 内存回写延迟
对于不需要频繁分配的缓存,可以通过 /sys/kernel/slab/<cache>/ 下的 tunables 减少内存占用。例如降低 cpu_partial 参数可减少 CPU 本地保留的 partial 页数量。
6.3 NUMA 感知策略
在多路服务器上,SLUB 默认优先在本地 NUMA 节点的 partial 页中分配。如果应用的工作集集中在特定节点,可通过 taskset 或 set_mempolicy() 将进程绑定到对应节点,最大化 slab 高速命中率。
7. 新版本内核中的进化
7.1 Linux 5.x 的 TYPESAFE_BY_RCU
引入 SLAB_TYPESAFE_BY_RCU 标志,允许对象在 RCU 读端临界区内被分配和初始化,然后在 RCU 宽限期后提交给读者。这减少了传统 RCU 模式下的分配等待开销。
7.2 Linux 6.x 的 SLUB 性能优化
最新内核(6.x 系列)持续优化 SLUB:
- LOCAL_NODE_DEFER:引入延迟 NUMA 分配策略,减少跨节点访问
- per-CPU partial 页扩展:每个 CPU 可保留更多 partial 页,减少 Node 级锁争用
- 更好地支持 KASAN/KFENCE:在调试模式下减少性能损失
8. 总结
Slab 分配器作为 Linux 内核内存管理的核心子系统,经历了从经典 SLAB 到现代化 SLUB 的演进。其设计精髓在于:通过对象缓存消除重复初始化开销,通过分层管理(CPU → Node → Buddy)实现高效的内存利用率与缓存亲和性。
对于内核开发者而言,理解 SLUB 的数据结构、分配路径和调试手段,是进行内核模块开发和性能调优的基础。无论是编写 kmem_cache_alloc() 上下文中的模块代码,还是诊断内存泄漏、use-after-free 等疑难问题,掌握 Slab 分配器都是必不可少的关键技能。
参考资源
- Understanding the Linux Virtual Memory Manager - Chapter 7: Slab Allocator
- Linux 源码:
mm/slab.h、mm/slub.c - 《Understanding the Linux Kernel, 3rd Edition》Chapter 8: Memory Management
- Jeff Bonwick, "The Slab Allocator: An Object-Caching Kernel Memory Allocator", USENIX 1994

发表评论 取消回复