引言
Linux 内核内存管理子系统是整个操作系统最底层的基石之一。在用户态程序中,我们习惯于直接调用 malloc/free 来完成内存的动态分配,但在内核态,事情远比这复杂——内核不能直接使用用户态的内存分配器,必须有一套专门为内核设计的、更加高效且可靠的内存管理机制。SLAB(由 Jeff Bonwick 最初为 Solaris 设计)及其改进版本 SLUB 就是这一需求的产物。
本文将从 SLAB 分配器的设计哲学入手,深入剖析其核心数据结构、工作流程、SLUB 的改进之处,并结合实际的调试手段与最佳实践,帮助读者全面理解 Linux 内核内存分配机制。
一、为什么需要 SLAB 分配器?
1.1 物理页分配的局限性
Linux 内核的物理内存管理以页(通常 4KB)为最小单位。伙伴系统(Buddy System)负责在物理页层面进行分配与回收,但它只能以 2 的幂次页数为粒度(1页、2页、4页、8页...)。当内核需要分配一个 task_struct(约 1.7KB)或者一个 inode 对象(约 600B)时,如果使用伙伴系统直接分配整页,会造成严重的内部碎片浪费。
1.2 内核对象的特征
内核中具有大量频繁创建和销毁的同类型对象,如:
- task_struct:进程描述符,进程创建/销毁时频繁操作
- dentry:目录项缓存,文件操作频繁
- inode:索引节点,文件系统操作频繁
- file:文件对象,open/close 频繁
- sk_buff:网络数据包,网络收发极其频繁
- 各种锁对象:mutex、rw_semaphore、spinlock 等
这些对象有两个共同特征:大小固定(同类对象大小相同)和生命周期短(频繁创建销毁)。SLAB 分配器正是针对这种场景设计的——它在页(伙伴系统分配的大块内存)的基础上进一步细分为小对象,同时利用着色(coloring)来优化缓存利用率,避免了反复初始化/销毁对象的开销。
1.3 SLAB 的设计目标
Jeff Bonwick 在 1994 年的论文《The Slab Allocator: An Object-Caching Kernel Memory Allocator》中提出了 SLAB 的四大设计目标:
- 快速分配与释放:通过空闲链表和缓存设计实现 O(1) 时间复杂度的操作
- 极低碎片:对象大小精准匹配,无内部碎片;通过着色减少外部冲突
- 硬件缓存友好:着色机制使对象均匀分布在缓存行上,减少缓存冲突未命中
- 可检查与可调试:支持毒化(poisoning)、红区(red zone)、所有权追踪等调试手段
二、SLAB 核心数据结构
2.1 三层结构总览
SLAB 分配器采用三层结构,从高到低分别是:
┌─────────────────────────────────────────────┐
│ kmem_cache (cache 层) │
│ - 每种对象类型一个 cache │
│ - 管理三个 slab 链表 │
├─────────────────────────────────────────────┤
│ slab 层 │
│ - 每个 slab 由一个或多个连续页组成 │
│ - 包含空闲对象链表 │
├─────────────────────────────────────────────┤
│ object 层 │
│ - 实际分配给用户的同尺寸小对象 │
└─────────────────────────────────────────────┘
2.2 kmem_cache 结构体
struct kmem_cache 是 SLAB 分配器最重要的数据结构,每种被缓存的对象类型对应一个 kmem_cache 实例。以下是精简后的关键字段:
struct kmem_cache {
struct kmem_cache_cpu *cpu_slab; // Per-CPU 快速缓存(每CPU一个)
unsigned long flags; // 缓存标志(如 SLAB_POISON)
unsigned int object_size; // 单个实际对象大小
unsigned int size; // 对齐后大小(可能含元数据)
unsigned int offset; // 空闲链表指针偏移
unsigned int cache_color; // 当前着色偏移
unsigned int colour_off; // 颜色步长(通常为缓存行大小)
// 三个 slab 管理链表
struct list_head slabs_full; // 已用满的 slab
struct list_head slabs_partial; // 部分空闲的 slab
struct list_head slabs_free; // 完全空闲的 slab
unsigned int num; // 每个 slab 包含的对象数量
gfp_t alloc_gfp; // 分配页面时的 GFP 标志
const char *name; // 缓存名称(如 "task_struct")
// 构造函数
void (*ctor)(void *obj);
};
关键字段说明:
- cpu_slab:每个 CPU 的快速分配路径(不要等待指针),这是 SLAB 高性能的关键之一
- 三个 slab 链表:按使用率对 slab 进行分类管理,分配时优先从 partial 链表取对象
- colour_off / cache_color:着色相关,用于使得不同 slab 中的对象映射到不同的硬件缓存行
2.3 kmem_cache_cpu:Per-CPU 加速
这是 SLAB 分配器最直接的性能优化手段:
struct kmem_cache_cpu {
void **freelist; // 指向下一个可用对象的指针(空闲链表头)
unsigned long tid; // 全局事务 ID(用于同步)
struct page *page; // 当前使用的 slab 页
struct page *partial; // Per-CPU partial slab 链表
};
分配优先级策略:
- 先尝试从
cpu_slab->freelist(Per-CPU 空闲对象链表)分配 —— 无锁、极快 - 若 freelist 为空,从
cpu_slab->partial(Per-CPU partial slab)补充 freelist - 若 partial 也为空,从
kmem_cache->slabs_partial(节点级 partial 链表)获取一个 slab - 如果 partial 链表也空,则从伙伴系统获取新的页组成新 slab
2.4 slab 结构与空闲链表
每个 slab 由一个或多个连续的物理页组成(通常为 1-4),这些页的 struct page 中特定字段指向 slab 管理结构:
struct slab {
union {
struct list_head slab_list; // 链接在 kmem_cache 的 slabs_* 链表中
struct {
void *freelist; // 空闲对象链表头
unsigned inuse; // 已使用对象计数
};
};
unsigned int s_mem; // 第一个对象的起始地址
};
空闲对象通过隐式链表连接——在每个空闲对象的起始位置嵌入一个 void* 指针,指向下一个空闲对象(由 kmem_cache->offset 字段控制位置):
slab 页面内存布局:
┌──────────────────────────────────────────┐
│ object 0 │ object 1 │ ... │ object N-1 │
│ │ [ptr→2] │ │ │
└──────────────────────────────────────────┘
freelist → 1 → 2 → NULL
每个空闲对象的起始 8 字节存储 next 指针
三、分配与回收流程深度解析
3.1 kmem_cache_alloc 完整路径
当一个内核组件调用 kmem_cache_alloc(gfp_mask) 时,完整的流程如下:
kmem_cache_alloc(cachep, flags)
│
├─ 1. 关中断 + 抢占(进入 Per-CPU 临界区)
│
├─ 2. 获取当前 CPU 的 kmem_cache_cpu
│
├─ 3. 检查 freelist 是否有空闲对象
│ ├─ 是:直接 pop 一个对象,goto done
│ │ freelist = object[0] // 读 next 指针
│ │ return object
│ │
│ └─ 否:→ 步骤 4
│
├─ 4. 检查 cpu_slab->partial(Per-CPU partial)
│ ├─ 有:将 page 切换为当前 page,跳转到步骤 3
│ └─ 无:→ 步骤 5
│
├─ 5. 从 node slab_partial 链表取一个 slab(需要加锁)
│ ├─ 有:摘除 slab,加入 cpu_slab,跳转到步骤 3
│ └─ 无:→ 步骤 6
│
├─ 6. 从伙伴系统分配新页面,创建新 slab
│ ├─ alloc_pages(cachep->gfpflags, cachep->order)
│ ├─ 按对象大小切割初始化
│ ├─ 构建 freelist 链表
│ └─ 如果指定了 ctor,对每个对象调用构造函数
│
└─ 7. 开中断 + 抢占,返回对象
3.2 kmem_cache_free 完整路径
kmem_cache_free(cachep, objp)
│
├─ 1. 通过 virt_to_page(objp) 找到对应 page
│ 验证该 page 有效(属于某个 slab)
│
├─ 2. 关中断
│
├─ 3. 将对象插入 Per-CPU freelist 头部
│ *(void**)objp = cachep->cpu_slab->freelist
│ cachep->cpu_slab->freelist = objp
│
├─ 4. 更新 inuse 计数
│ inuse--
│
├─ 5. 判断 slab 状态变更
│ ├─ inuse==0(变空): 移入 slabs_free 链表
│ │ → 系统内存紧张时归还页面给伙伴系统
│ ├─ inuse==num(刚放满): 从 partial 移入 slabs_full
│ └─ 其他: 保持 partial 链表
│
└─ 6. 开中断
3.3 自动扩容与收缩机制
SLAB 分配器内置了基于内存压力的自动调整机制:
- 扩容:当 partial 链表为空且 CPU 空闲链表也为空时,从伙伴系统分配新页
- 收缩:当系统内存紧张时(通过
shrinker机制),释放 slabs_free 链表中的所有 slab 页面 - 平衡:部分内核线程(如 kswapd)会间接触发缓存收缩
四、着色(Cache Coloring)机制
什么是着色?
由于硬件缓存是直接映射或组相联映射的,如果每个 slab 的起始位置都从页面起始开始切割对象,那么不同 slab 中相同索引的对象会映射到相同的缓存行,导致缓存冲突未命中(conflict miss)。
着色通过在不同 slab 起始处加入不同的偏移量(color * colour_off),使得对象起点位置分散开来。colour_off 通常等于一个缓存行大小(如 64 字节),color 是一个循环递增的计数器。
无着色时:
┌─Slab A──┐ ┌─Slab B──┐ ┌─Slab C──┐
│obj0@0x0 │ │obj0@0x0 │ │obj0@0x0 │ ← 都映射到同一缓存行!
│obj1@0x20│ │obj1@0x20│ │obj1@0x20│
└─────────┘ └─────────┘ └─────────┘
有着色时(colour_off = 64):
┌─Slab A──┐ ┌─Slab B──┐ ┌─Slab C──┐
│△64 │obj0 │ │△128│obj0│ │△192│obj0│ ← 分散到了不同的缓存行
│obj1@0x40 │ │obj1@0x60 │ │obj1@0x80 │
└─────────┘ └─────────┘ └─────────┘
五、SLUB:默认的现代分配器
5.1 SLUB 的设计动机
随着系统规模增大(多核 NUMA 架构),SLAB 的元数据开销和锁竞争成为了瓶颈:
- SLAB 每个 slab 都需要独立的
struct slab结构和kmem_bufctl_t控制数组 - 每个 CPU 的 partial 链表管理增加了内存消耗
- 复杂的链表操作使得代码维护困难
Vegard Nossum 在 Linux 2.6.23 引入 SLUB 作为默认分配器,大幅简化了设计。
5.2 SLUB 的关键简化
SLUB 采用了以下核心设计改进:
- 取消独立的 slab 管理结构:直接利用
struct page的 lru、freelist 等字段复用为 SLUB 管理 - 无 Per-CPU partial 链表:SLUB 将 partial slab 全部挂载在 NUMA node 链表上,由 node 锁保护
- 简化空闲链表:使用 page->freelist → object1 → object2 → ... 的一级链表
- 合并 kmem_cache_cpu 和 page 结构:减少指针跳转,降低缓存未命中
5.3 SLUB 数据布局
SLUB 中的 struct page 关键字段复用:
┌──────────────────────────────┐
│ struct page │
│ .freelist → 空闲对象链表头 │
│ .inuse → 已使用对象数 │
│ .objects → 总对象数 │
│ .frozen → 是否冻结(PerCPU)│
│ .next/prev → node partial 链表│
└──────────────────────────────┘
空闲对象的 next 指针存储在对象自身内存中:
第一个 8 字节 = 指向下一个空闲对象的指针
5.4 SLUB vs SLAB 性能对比
| 对比维度 | SLAB | SLUB |
|---|---|---|
| 元数据开销 | 每个 slab 需额外 struct slab + bufctl 数组 | 0 额外元数据,复用 page 结构 |
| Per-CPU partial | 支持,消耗额外内存 | 不支持,简化管理 |
| 大对象页支持 | 需要特殊处理 | 透明巨页(THP)原生支持 |
| NUMA 支持 | 较复杂较贵 | 本地 node 分配 + fallback |
| 调试功能 | 需额外模块支持 | 内置更完善(redzoning、poisoning) |
存在格式问题,已修复
六、kmalloc 系列函数
6.1 kmalloc 与 SLAB 的关系
kmalloc() 是内核通用内存分配接口,它并非直接与伙伴系统交互,而是通过预定义的 kmalloc_caches 数组(由 SLUB 管理的一个小型 cache 集群)来实现:
// mm/slab_common.c
struct kmem_cache *kmalloc_caches[KMALLOC_SHIFT_HIGH + 1] __ro_after_init;
// 划分为两个区间:
// 1. kmalloc-8, kmalloc-16, kmalloc-32, ..., kmalloc-8K (普通区,KMALLOC_NORMAL)
// 2. kmalloc-rcl-8, ..., kmalloc-rcl-8K (备用紧急区,KMALLOC_RECLAIM)
分配时根据请求大小向上取整到最近的 cache 档位,然后通过 SLUB 分配。这意味着 kmalloc 分配的内存不大于 8KB(KMALLOC_MAX_SIZE),更大的分配需要使用 vmalloc() 或 kmalloc_large()(高版本支持更大的kmalloc)。
6.2 kmalloc GFP 标志详解
| 标志 | 含义 | 典型场景 |
|---|---|---|
| GFP_KERNEL | 标准内核分配,允许睡眠 | 大多数内核代码 |
| GFP_ATOMIC | 原子分配,不允许睡眠 | 中断处理、持有 spinlock |
| GFP_NOIO | 不会触发 I/O 操作 | 块 I/O 层内分配 |
| GFP_NOFS | 不会触发文件系统操作 | 文件系统代码内分配 |
| GFP_HIGHUSER | 分配用户空间高端内存 | 用户缓冲区映射 |
| GFP_ZERO | 分配后清零 | 需要零初始化场景 |
6.3 kmalloc 最佳实践
// ✅ 推荐:使用 kzalloc 获取零初始化内存
struct task_info *info = kzalloc(sizeof(*info), GFP_KERNEL);
// ✅ 推荐:使用数组分配宏
struct item *arr = kmalloc_array(count, sizeof(*arr), GFP_KERNEL);
// ✅ 推荐:size_t 防止整数溢出
buf = kmalloc(sizeof(*buf) + extra_len, GFP_KERNEL);
// ❌ 错误:使用硬编码大小(忽略结构体变化)
info = kmalloc(256, GFP_KERNEL);
// ❌ 错误:遗漏 GFP 标志
info = kmalloc(sizeof(*info));
七、调试与故障排查
7.1 SLUB 调试工具
SLUB 以 SLAB 更强的调试能力自启,主要通过以下方式开启:
// 编译时开启:CONFIG_DEBUG_SLUB
// 启动参数添加:slub_debug=FZP
// F - fault injection(故障注入)
// Z - red zoning(红区标记)
// P - poisoning(毒化填充)
// 运行时对特定缓存开启:
echo 1 > /sys/kernel/debug/slab/task_struct/validate
cat /sys/kernel/debug/slab/slabinfo
cat /sys/kernel/slab/task_struct/alloc_from_overflow // 查看分配溢出
7.2 常见的 SLUB 错误场景
// 【错误1】Use-After-Free
// SLUB 毒化模式使用 0x6b (FREE_FILL_PATTERN) 填充已释放对象
// 如果后续代码访问已释放对象,调试信息中包含 poison 值即可定位
// 【错误2】Out-of-Bounds 读写
// SLAB 在对象边界设置 redzone(通常为 0xbb),
// 覆盖 redzone 会触发 BUG_ON()
// 【错误3】Double-Free
// SLUB 空闲链表会检测重复插入同一对象
// 输出类似 "object was already free" 的警告
7.3 slabinfo 输出解读
root@linux:~# cat /proc/slabinfo
task_struct 234 240 7360 12 8 : tunables ...
↑ ↑
active total
objs objs
// 参数含义:
// name: 缓存名称
// active_objs: 当前已使用对象数
// num_objs: 总对象数(包括空闲的)
// objsize: 每个对象大小(字节)
// objperslab: 每 slab 对象数
// pageperslab: 每个 slab 占用页数
7.4 ftrace 追踪 slab 事件
# 开启 slab 事件追踪
echo 1 > /sys/kernel/debug/tracing/events/kmem/kmalloc/enable
echo 1 > /sys/kernel/debug/tracing/events/kmem/kmem_cache_alloc/enable
echo 1 > /sys/kernel/debug/tracing/events/kmem/kfree/enable
echo 1 > /sys/kernel/debug/tracing/events/kmem/kmem_cache_free/enable
# 实时查看:
cat /sys/kernel/debug/tracing/trace_pipe
# 输出示例:
// ls-1234 [001] 456.789: kmalloc: call_site=0xffff88800a1b3c40
// ptr=0xffff88800b2a1000 bytes_req=256 bytes_alloc=256
// gfp_flags=GFP_KERNEL
八、性能优化实战
8.1 缓存行对齐优化
对于频繁访问的结构体,建议使用 ____cacheline_aligned 宏确保其起始地址落在缓存行边界上,避免 False Sharing(错误共享):
// 计数器结构——确保每个 CPU 的计数器独占一行
struct percpu_counter {
raw_spinlock_t lock;
s64 count;
} ____cacheline_aligned_in_smp;
// 高频访问的锁对象
struct my_device {
spinlock_t lock;
char padding1[64 - sizeof(spinlock_t)];
atomic_t counter;
char padding2[64 - sizeof(atomic_t)];
};
8.2 NUMA 感知分配
在大规模 NUMA 系统中,访问远端内存节点会造成大幅性能下降(通常 2-3 倍延迟)。内核通过以下方式优化:
- 首选 node 分配:SLUB 默认从请求 CPU 所在的 node 分配内存(node 级 freelist + partial 链表)
- NUMA 平衡:通过
mbind()或set_mempolicy()设置用户进程的内存绑定策略 - 手动指定:64 位进程可通过
alloc_pages_node(nid, gfp, order)指定 node
8.3 使用 percpu 变量避免间接分配
对于每个 CPU 频繁访问的计数器或状态,直接定义 per-CPU 变量可以避免从 SLUB 缓存分配:
// 定义并声明
DEFINE_PER_CPU(struct cpu_stats, cpu_stats);
// 分配(初始化时)
struct cpu_stats *stats;
for_each_possible_cpu(cpu) {
stats = per_cpu_ptr(&cpu_stats, cpu);
memset(stats, 0, sizeof(*stats));
}
// 访问(抢占安全但不需要加锁)
this_cpu_inc(cpu_stats.packet_count);
// 或
get_cpu_var(cpu_stats).packet_count++;
put_cpu_var(cpu_stats);
8.4 使用 slab 的 RCU 释放
对于 RCU 保护的对象,释放需要延迟到 grace period 结束,避免读者还在访问就被回收:
// 错误:直接 kfree
// kfree(my_rcu_protected_obj); // 可能导致 use-after-free!
// 正确:使用 kfree_rcu 或 call_rcu
kfree_rcu(my_rcu_protected_obj, rcu_head);
// 等价于:
call_rcu(&my_rcu_protected_obj->rcu_head, my_release_function);
九、实际案例分析
9.1 案例一:高并发下 SLUB 碎片优化
某网络服务器(8 核、16GB 内存)在大量并发连接/断开时性能下降,查看 slabinfo 发现 kmalloc-256 缓存了大量 partial slab:
kmalloc-256 45200 98304 256 16 1 : tunables ...
^^^^^ ↑ 很多 active 远小于 total
分析原因:连接突发期间分配大量 256 字节带宽对象,突发结束后只释放了部分,但所有 slab 都残留少量 inuse 对象而无法归还,造成内存浪费。
解决方案:
// 1. 通过 /sys 接口手动收缩(仅归还完全空闲的 slab)
echo 1 > /sys/kernel/slab/kmalloc-256/shrink
// 2. 优化应用层:使用对象池预分配,减少临时分配
// 3. 考虑开启 CONFIG_SLUB_TINY(极简 SLUB),对于内存紧张的系统更合适
9.2 案例二:内核模块内存泄漏排查
自定义内核模块加载后内存持续增长,使用 kmemleak 追踪:
// 启用 kmemleak:启动参数 kmemleak=on 或 CONFIG_DEBUG_KMEMLEAK
// 或运行时:echo scan > /sys/kernel/debug/kmemleak
// 查看泄漏报告:
cat /sys/kernel/debug/kmemleak
// 输出包含未释放对象的分配调用栈
// 模块中的典型泄漏模式:
static int __init mymod_init(void) {
my_obj = kmalloc(sizeof(*my_obj), GFP_KERNEL);
// 忘记在 exit 中释放
return 0;
}
// 修复:
static void __exit mymod_exit(void) {
kfree(my_obj);
}
十、总结与展望
SLAB 分配器自 1994 年诞生以来,经历了 SLAB → SLUB + SLOB 的演进,目前 SLUB 是绝大多数 Linux 系统的默认分配器。理解其原理对于内核开发、驱动开发、高性能系统调优都有重要意义:
- 设计哲学:针对固定大小、高频创建销毁的小对象场景,通过缓存设计实现零锁分配和极低碎片
- 核心机制:Per-CPU 快速路径 + 二级缓存 + 三级链表的层次化设计
- 性能关键:着色优化缓存行利用率、Per-CPU 消除锁竞争、分层缓存减少延迟
- 调试能力: poison/red zone/fault injection 等机制使内存错误可定位
对于未来的发展方向,Linux 内核社区正在探索 SLUB 的进一步精简,以及在新型硬件(持久内存、CXL 内存)时代的适配优化。内存管理作为操作系统最底层的子系统,始终是性能调优和稳定性的关键所在。

发表评论 取消回复