Linux 内核 SLAB/SLUB/SLOB 分配器深度实战:从原理架构到生产调优
内核内存分配器是 Linux 系统性能与稳定性的基石。SLAB、SLUB、SLOB 三种分配器各有其设计哲学与适用场景,本文从数据结构源码级剖析其实现机制,并给出生产环境中 OOM、碎片化、性能瓶颈的完整排查与调优方案。
一、为什么需要专用的内核内存分配器
用户态的 malloc() 基于 brk()/mmap() 系统调用获取整页内存(通常 4KB),而内核中大量对象远小于一页——task_struct 约 1.7KB、inode 约 600B、dentry 约 192B。若直接以页为单位分配,内存利用率将极其低下。此外,内核对分配延迟有严格约束,频繁的页分配/释放还会加剧 TLB 抖动。
内核内存分配器的核心设计目标:
- 减少内部碎片:将同类型对象聚集在同一个 SLAB 页框中,最大化空间利用率
- 降低分配延迟:利用对象缓存(empty/partial/full 三链表)实现 O(1) 级别的快速分配
- 对象复用与缓存着色:保留已初始化对象,避免重复构造/析构开销;通过 cache coloring 优化 CPU 缓存行利用
- NUMA 感知:在 NUMA 系统上优先从本地节点分配,降低跨节点延迟
- 硬件缓存对齐:确保对象起始地址按 cache line 对齐,避免 false sharing
二、SLAB 分配器:经典设计的完整剖析
2.1 历史背景与核心数据结构
SLAB 由 Sun 的 Jeff Bonwick 于 1994 年为 Solaris 设计,Linux 2.1/2.2 中由 Ingo Molnár 引入并成为默认分配器。其核心思想是对象存储池化:相同类型和大小的内存在逻辑上被组织为 kmem_cache,每个 cache 管理多个 slab(物理页框组)。
SLAB 的核心数据结构关系:
// include/linux/slab.h (简化)
struct kmem_cache {
struct array_cache __percpu *cpu_cache; // per-CPU 快速缓存(SLAB 特有)
unsigned int limit; // cpu_cache 中最大对象数
unsigned int colour; // cache colouring 范围
unsigned int colour_off; // 颜色偏移增量
struct kmem_cache *slabp_cache; // 用于分配 kmem_cache_node 的 cache
unsigned int slab_size; // 单个 slab 总大小
unsigned int object_size; // 用户请求的对象大小
unsigned int align; // 对齐要求
unsigned long flags; // SLAB_* 标志位
unsigned int num; // 每 slab 中对象数量
const char *name; // cache 名称(/proc/slabinfo 可见)
struct list_head list; // 全局 cache 链表
struct kmem_cache_node **node; // NUMA 节点数组
struct array_cache __percpu *array[1]; // flexible array of per-cpu caches
};
struct kmem_cache_node {
struct list_head slabs_partial; // 部分使用 slab 链表
struct list_head slabs_full; // 已满 slab 链表
struct list_head slabs_free; // 空闲 slab 链表
unsigned long free_objects; // 节点中空闲对象总数
unsigned int free_limit; // 空闲对象上限
spinlock_t list_lock; // 节点链表锁
};
struct slab {
union {
struct list_head list; // 链入 slabs_partial/full/free
struct slab_rcu slab_rcu; // RCU 释放路径
};
void *s_mem; // slab 中第一个对象地址
unsigned inuse; // 已使用对象计数器
unsigned free; // 下一个空闲对象索引
unsigned short nodeid; // 所属 NUMA 节点
};
2.2 SLAB 描述符的内存布局
SLAB 有两种布局模式,由 slab 大小与页框容量的比例决定:
- 常规布局 (Normal Layout):对象较小,slab 结构体(
struct slab)放在 slab 页框外部(即使用单独的内存存放元数据),对象在页框内部紧密排列。 - 大型布局 / Off-slab:对象较大(> 页框容量的 1/8),slab 元数据嵌入页框末尾,
s_mem指向页框起始,末尾空间存放kmem_bufctl_t数组。
kmem_bufctl_t 是一个 unsigned integer 数组,用作空闲对象链表。每个 entry 存储下一个空闲对象的索引,最后一个空闲对象标记为 BUFCTL_END (0xffff)。这种设计将 freelist 与对象本身分离,避免了传统嵌入式链表导致对象被修改的问题。
2.3 Cache Coloring 机制
Cache coloring(缓存着色)是 SLAB 中一个精巧的设计:不同 slab 中第一个对象的起始地址在页内有不同的偏移量(colour_off 的倍数),目的是让不同 slab 中同序号的对象映射到 CPU 缓存的不同 set 中,降低 cache line 冲突。
// mm/slab.c: cache_Colour 计算
static void cache_Colour(struct kmem_cache *cachep, struct slab *slabp, unsigned long colour)
{
slabp->s_mem = page_address(slabp->page) + colour * cachep->colour_off;
}
colour_off 通常等于 L1 cache line 大小(32B 或 64B)。当一个 cache 的 colour 范围耗尽时,colour 回绕到 0。实践表明,在频繁分配释放小块对象的场景下,cache coloring 可以带来 5%-15% 的指令缓存命中率提升。
2.4 SLAB 分配流程
SLAB 的 kmem_cache_alloc() 走三级路径(从快到慢):
kmem_cache_alloc(cachep, flags)
│
├── Level 1: cpu_cache (array_cache)
│ └── avail > 0 ? entry[--avail] : cache_alloc_refill()
│
├── Level 2: node->slabs_partial
│ └── 取 first_partial slab → obj = slab→s_mem + (free * size)
│ slab→inuse++; slab→free = bufctl[free];
│ if slab满: 从 partial 摘下,挂入 full
│
├── Level 3: node->slabs_free 或新分配
│ └── alloc_slabmgmt() 分配一个新 slab
│ alloc_slabmgmt() → alloc_pages_node()
│ slab→s_mem = page_address(page)
│ 初始化 bufctl freelist
│ node→slabs_partial 加入
第一级 CPU 缓存无需加锁,是纯 per-CPU 操作;第二级和第三级需要获取 kmem_cache_node.list_lock。这种设计使得单线程分配几乎无竞争。
2.5 SLAB 释放流程
释放时按逆向路径归还:
kmem_cache_free(cachep, objp)
│
├── 计算 slabp = virt_to_slab(objp)
├── cpu_cache.avail < limit ? cpu_cache->avail[cpu] = objp : cache_flusharray()
│
├── slab→inuse--
├── if slab→inuse == 0:
│ └── 从 partial 摘下 → slabs_free 或 free_slab() 归还页框
│ 若 node→free_objects > free_limit → 强制释放 free slabs
│
└── if slab→inuse == num-1 (刚由 full 变 partial):
└── 从 full 摘下 → slabs_partial 加入
关键细节:释放后检查是否需要让出空闲 slab 给全局伙伴系统——这保证了多 cache 之间的内存平衡。
2.6 SLAB 对象构造与析构
SLAB 支持 SLAB_CTOR_CONSTRUCTOR 标志位,缓存一个对象的构造函数指针(cachep->ctor)。当 slab 中对象首次被分配前调用 ctor 初始化,释放时不立即析构,而是保持构造状态。这种对象复用策略减少了重复的初始化开销,对task_struct、inode、signal_struct 等高频构造/析构的内核对象尤其有效。
然而,这也带来了一个潜在问题:若构造函数持有外部资源(如锁、引用计数),在对象被借用时可能导致状态泄漏。因此只有"纯数据初始化"的构造函数才应放在 ctor 中。
三、SLUB 分配器:现代 Linux 的选择
3.1 为什么需要替代 SLAB
SLAB 虽然经典,但随着硬件发展暴露出以下问题:
- 元数据膨胀严重:每个 slab page 对应一个
struct slab+ 完整的kmem_bufctl_t[]数组,加上 per-CPU 的array_cache,小对象场景下元数据开销接近有效载荷的 3%-5%。 - 扩展性瓶颈:早期 SLAB 的全局链表
cache_cache和多层shrink导致大规模系统(>128 核)下锁竞争严重。 - 代码复杂性过高:
slab.c巅峰期超过 5000 行,大量条件分支难以维护,漏洞频发。 - Off-slab 模型反直觉:大型 slab 的元数据嵌入方式使得地址计算复杂,调试困难。
SLUB 由 Christoph Lameter 于 2007 年合入 Linux 2.6.23,目标是简化数据结构、提升扩展性、保持 SLAB 兼容接口,现已成为现代发行版(RHEL 6+、Ubuntu 12.04+、Android)的默认分配器。
3.2 SLUB 数据结构:极简主义
SLUB 最核心的设计变化是元数据内嵌到 page 结构体:
// include/linux/mm_types.h (简化)
struct page {
...
union {
struct {
void *freelist; // 指向 page 中第一个空闲对象
union {
unsigned inuse:16; // slab 中已使用对象数
struct {
unsigned objects:15;
unsigned frozen:1; // 是否在 per-CPU 部分缓存中(frozen=1 时不回 budy)
};
};
};
struct {
struct page *next; // 在 node 的 partial 链表中的下一个 page
int pages; // 剩余 page 数(compound page 尾部)
int pobjects; // 空闲对象数(用于 partial 排序)
};
};
struct kmem_cache *slab_cache; // 指向所属 cache
...
};
关键设计决策:SLUB 复用 struct page 中的 union 字段存放 slab 元数据,完全消除了独立的 struct slab 结构。每个物理 page 的 struct page(64 字节)即为其 slab 描述符,甚至连 kmem_bufctl_t 数组都被嵌入式空闲链表替代(每个空闲对象的第一个 8 字节被用作 next 指针)。
// mm/slub.c
struct kmem_cache {
struct kmem_cache_cpu __percpu *cpu_slab; // per-CPU 快速路径
unsigned long flags;
unsigned int min_objects; // 每 slab 最小对象数(计算值)
unsigned int size; // 对象实际大小(含对齐)
unsigned int object_size; // 用户请求大小
struct reciprocal_value reciprocal_buffer_size; // 用于 reciprocal division 优化
unsigned int offset; // 空闲链表指针在对象内的偏移
unsigned int cpu_partial; // cpu slab partial 链表最大页数
...
struct kmem_cache_node *node[MAX_NUMNODES]; // NUMA 节点数组
const char *name;
};
struct kmem_cache_cpu {
void **freelist; // per-CPU freelist(指向下一个空闲对象)
unsigned long tid; // 全局唯一事务 ID(解决 ABA 问题)
struct page *page; // per-CPU 主 slab
struct page *partial; // per-CPU partial slab 链表
#ifdef CONFIG_SLUB_CPU_PARTIAL
static_key stateful_call; // 调试用
#endif
};
struct kmem_cache_node {
spinlock_t list_lock;
#ifdef CONFIG_SLUB
unsigned long nr_partial; // partial slab 数量
struct list_head partial; // partial slab 链表
#endif
};
SLUB 将 freelist 直接嵌入空闲对象内部——分配时返回 freelist 指向地址,并将 *freelist 作为下一个空闲指针读取。释放时,将 obj 的起始 8 字节写入 freelist,然后 freelist = obj。这种侵入式链表彻底消除了外部元数据数组。
3.4 SLUB 分配路径(New SLUB Path)
___slab_alloc(node, gfpflags, addr, s)
│
├── fast_path:
│ └── cpu_slab->freelist != NULL
│ && (!node || !in_interrupt() || !(gfpflags & __GFP_THISNODE))
│ && !irqs_disabled()
│ └── 直接返回 freelist,freelist = *freelist
│ tid 递增,O(1) 无锁操作
│
├── slow_path:
│ ├── new_slab:
│ │ ├── get_partial(node)
│ │ │ └── node->partial 链表取一页
│ │ ├── new_slab_objects():
│ │ │ └── new_slab() 从 buddy 获取一页或 compound page
│ │ │ → 初始化 page->freelist、page->objects
│ │ │ → 根据 colour 偏移(slub 简化为 randomized offset)
│ │ │ → 返回第一个对象
│ │ └── cpu_slab->page = new_page
│ └── redo:
│ └── freelist 不为空,正常分配
SLUB 的快速路径极其精简,仅两条指针解引用操作(读取 freelist、修改 freelist),无任何函数调用。这也是其相对于 SLAB array_cache 路径的优势——SLAB 还需要遍历 avail 数组和可能的 cache_alloc_refill() 调用。
3.5 释放路径与 Frozen Page 优化
__slab_free(s, page, x, addr)
│
├── 将 x 插入 cpu_slab->freelist 头部
├── page→inuse--
│
├── 若 page 为空但未 frozen:
│ └── put_cpu_partial(): 将 page 从 cpu_slab 摘下,挂入 cpu_slab->partial
│
├── 若 page→inuse==0 且 partial 链表已满:
│ └── unfreeze_partials(): 将 page 转移至 node->partial 或释放给 buddy
│ 若 node→nr_partial < min_partial → 保留在 node 的 partial 中
│ min_partial 由 kmem_cache 创建时根据 size 自动计算
SLUB 的 frozen 标志是一个巧妙的优化:当 page 被固定在某 CPU 的 cpu_slab 上时(page->frozen=1),即使空闲也不回收入 buddy 系统,保证该 CPU 下次分配无需再从 node 取页,避免缓存行 bouncing。
3.6 SLUB 对象布局化调试(slabinfo)
SLUB 提供丰富且低开销的调试机制:
- Red zoning:在对象边界写入
0x12345678魔数,溢出时触发 BUG() - Poisoning 释放时填充
0x5a,分配时填充0xa5(不同阶段),检测 use-after-free 与 uninitialized read - Tracking:记录每个对象的分配/释放调用栈,
/proc/slabinfo中可查看 active_num vs num_active 判断泄漏 - Cpu partial/node partial 平衡:通过
/sys/kernel/slab/<cache>/cpu_partial和min_partial控制回廊策略
四、SLOB 分配器:极致精简方案
4.1 适用场景
SLOB (Simple List Of Blocks) 是为内存极度受限的嵌入式系统设计的分配器。它没有对象缓存机制,本质上维护一个简单的空闲块链表,分配时遍历链表找满足大小要求的块(first-fit)。三个分配器的核心对比:
| 特性 | SLAB | SLUB | SLOB |
|---|---|---|---|
| 代码复杂度 | ~5000 行 | ~1500 行 | ~400 行 |
| 元数据结构 | struct slab + bufctl[] | struct page 复用 | 无独立元数据,块头嵌入 |
| per-CPU 缓存 | array_cache[] | cpu_slab + frozen | 无 |
| NUMA 支持 | 完善 | 完善(最佳) | 基本(单一 node) |
| 碎片化控制 | cache coloring | CPU partial 链表 | 无 |
| 最坏外部碎片 | 低(对象等大小) | 低(对象等大小) | 高(best-fit 失败时) |
| 默认启用场景 | 历史兼容 | 桌面/服务器/移动端 | 嵌入式(CONFIG_EMBEDDED) |
4.2 SLOB 实现机制
// mm/slob.c
static void *slob_alloc(size_t size, gfp_t gfp, int align)
{
struct slob_list *prev, *cur, **p;
slobidx_t offset;
list_for_each_entry(sp, slob_list, list) {
// 遍历空闲链表寻找满足大小的块
cur = sp->freelist;
while (cur) {
if (cur->units >= SLOB_UNITS(size)) {
// 找到合适的块,分割
offset = cur->units - SLOB_UNITS(size);
if (offset >= SLOB_UNITS(SLAB_SIZE)) {
// 分割块,剩余部分链回 freelist
split_block(cur, size);
}
return cur; // 返回分配块指针
}
cur = cur->next;
}
}
// 无可用块,调用 __alloc_pages()
return slob_new_page(gfp);
}
SLOB 中每个内存块有 8 字节的 slob_t 头(包含 units 计数和块指针),剩余空间供用户使用。由于是 first-fit 而非 buddy-system,长期运行后会产生严重的外部碎片——典型场景下 SLOB 系统的空闲内存总量可能足够,但找不到连续满足要求的块。
五、三种分配器的切换与运行时选择
5.1 编译期选择
通过 Kconfig 选定默认分配器:
5.2 运行时动态创建与销毁 cache
// 创建专用 cache(对象池化)
struct kmem_cache *kmem_cache_create(const char *name, unsigned int size,
unsigned int align, slab_flags_t flags,
void (*ctor)(void *));
// 分配/释放
void *obj = kmem_cache_alloc(cache, GFP_KERNEL);
kmem_cache_free(cache, obj);
// 销毁(确保无活跃对象)
kmem_cache_destroy(cache);
// 或全局的 kmalloc 系列(使用预置大小 cache: 8, 16, 32, ... 128K)
void *p = kmalloc(size, GFP_KERNEL);
kfree(p);
kmalloc() 并非独立实现,而是通过一套预创建的 kmalloc_caches[] 数组(SLAB/SLUB/SLOB 共享相同接口)实现。每个 cache 覆盖几何分布的大小级(8/16/32/64/96/128/192/256/512/1024/2048/4096/8192 等),请求时选择能满足的最小 cache,内部碎片浪费不超过请求大小的 12.5%。
六、生产环境性能诊断与调优
6.1 /proc/slabinfo 与 slabtop 详解
$ cat /proc/slabinfo
slabinfo - version: 2.1
# name <active_objs> <num_objs> <objsize> <objperslab> <pagesperslab> : tunables <limit> <batchcount> <sharedfactor> : slabdata <active_slabs> <num_slabs> <sharedavail>
dentry 2154524 2218833 192 21 1 : tunables 0 0 0 : slabdata 105658 105658 0
inode_cache 543210 567890 648 5 2 : tunables 0 0 0 : slabdata 113578 113578 0
task_struct 89234 102400 1696 4 2 : tunables 0 0 0 : slabdata 25600 25600 0
vm_area_struct 456789 500000 208 19 1 : tunables 0 0 0 : slabdata 26316 26316 0
kmalloc-32 1234567 2000000 32 128 1 : tunables 0 0 0 : slabdata 15625 15625 0
关键字段解读: active_objs < num_objs 表示有对象处于空闲态。若两者差值巨大(如 kmalloc-32 差 765,433),说明存在大量对象未释放,可能存在内存泄漏。objsize 是分配器实际分配大小(含对齐),objperslab 是每个 slab 页框的对象密度。
slabtop 实时观察:
$ slabtop -s c # 按Cache size排序
Active / Total Objects (% used) : 2345678 / 3456789 (67.8%)
Active / Total Slabs (% used) : 45678 / 56789 (80.4%)
Active / Total Caches (% used) : 180 / 220 (81.8%)
Active / Total Size (used) : 456.78 MB / 678.90 MB (67.3%)
Minimum / Average / Maximum Object : 0.01K / 0.20K / 8.00K
OBJS ACTIVE USE OBJ SIZE SLABS OBJ/SLAB CACHE SIZE NAME
2200000 1500000 68% 0.03K 17188 128 67.0MB kmalloc-32
500000 400000 80% 0.64K 100000 5 40.0MB inode_cache
200000 180000 90% 0.20K 10240 19 8.0MB vm_area_struct
6.2 SLUB 碎片化排查
内存碎片化是 slab 分配器的常见问题,SLUB 通过 CPU partial 链表缓解但并非消除。排查命令:
$ sudo cat /sys/kernel/slab/kmalloc-1024/alloc_from_cpu_partial
45690 # CPU partial 列表中成功分配的次数(越高越好)
$ sudo cat /sys/kernel/slab/kmalloc-1024/cpu_partial
2 # 每个 CPU 的 partial slab 页数上限
$ sudo cat /sys/kernel/slab/kmalloc-1024/min_partial
5 # 节点保留 partial slab 的最小值
# 排查 node partial 链表长度
$ sudo cat /sys/kernel/slab/kmalloc-1024/node_partial
若 alloc_from_cpu_partial 保持为 0 且 alloc_calls 频繁上升,说明 slab_cache 长时间处于高负载且无有效 partial 缓冲,应考虑调高 cpu_partial 值。
6.3 对象泄漏排查实战
当 active_objs 持续增长且不释放时,第一步是确认是否为真正的泄漏:
# 1. 开启 SLUB 泄漏追踪
echo 1 > /sys/kernel/slab/<cache_name>/trace
# 2. 触发一次内存回收后查看泄漏报告
echo 1 > /sys/kernel/slab/<cache_name>/.leak追踪
cat /sys/kernel/slab/dentry/.leak追踪
# 3. 或使用 systemtap 精确追踪分配/释放不匹配点
$ stap -e 'probe kernel.function("kmem_cache_alloc") {
if ($s->name == "dentry")
printf("%s %d kmem_cache_alloc %s\n", execname(), pid(), $s->name)
}'
# 4. 利用 slabinfo 的 /proc/slabinfo 对比两次采样
$ cat /proc/slabinfo > /tmp/slab1.txt
$ sleep 60
$ cat /proc/slabinfo > /tmp/slab2.txt
$ diff /tmp/slab1.txt /tmp/slab2.txt | grep active_objs
6.4 高负载下 SLUB 参数调优
针对高频分配场景(如 NFV 数据面、Web 前端服务器、容器宿主机),推荐调节参数:
# 放大 per-CPU partial 缓存,减少 slub 全局链表竞争
echo 50 > /sys/kernel/slab/kmalloc-*/cpu_partial
# 提高 min_partial,保证节点层有更多 spare slab
echo 20 > /sys/kernel/slab/dentry/min_partial
# 关闭 slub merge(排查问题时定位具体 cache)
echo 0 > /sys/kernel/slab/kmalloc-*/cache_dma
# 降低 slaborder 减少单 slab 大小,减少大页浪费
echo 0 > /sys/kernel/slab/kmalloc-1024/order
注意:order(slaborder)增加可以提升 buddy 利用率(更大连续页),但会增加分配延迟;对延迟敏感场景保持默认值即可。
6.5 OOM 与 slab 回收的边界
当系统内存紧张时,内核通过 shrink_slab() 通知各 cache 释放空闲对象。SLUB 的回收策略比 SLAB 更激进——它直接释放 empty slab page 回 buddy 系统,且优先释放 node partial 而非 cpu partial(cpu partial 访问需要锁)。
OOM 排查要点:
$ dmesg | grep -i "slub\|oom"
[284734.294] SLUB: Unable to allocate memory on node 0 (gfp=0x20)
[284734.294] cache: kmalloc-256, object size 256, buffer total: 4096, kernel addr: 00000000fe32f7e2
[284734.294] node 0: slabs: 2000, partial: 50, free: 20000 (cpu 0: 0/0 cpu 1: 0/0 cpu 2: 0/0 ...)
OOM 发生时,node 0: slabs: 2000, partial: 5 表示 2000 个 slab 中 partial 仅 5 个,说明 cache 处于高负载且 buddy 系统内存不足。这种情况需通过 slab_order 或 slab ratio 调整。
七、分配器选型决策与最新演进
7.1 决策树
- 嵌入式设备(<64MB RAM) → SLOB(编译进内核,零元数据开销)
- 通用服务器/桌面/移动(≥256MB RAM) → SLUB(默认选择)
- 旧系统或特殊 CPU(SPARC/M68K/Alpha) → SLAB(兼容驱动)
- 实时性要求极高且对象等大小 → 直接使用
kmem_cache_create()创建专用 cache + SLUB 后端
7.2 SLUB 的最新演进方向
Linux 6.x 内核中 SLUB 持续优化,值得关注的新特性:
- Tiny RCU 与可睡眠 RCU:SLUB 的释放路径已适配可抢占上下文,RT 内核受益
- Memoryfolios:针对 folio(多页复合对象)的 slab 分层次布局,减少大页 slab 的元数据开销
- 回收侧扩展性增强:6.5+ 内核引入 per-CPU 统计与无锁 node 遍历,减少 shrink 路径的锁竞争
八、总结
SLAB 开创了对象池化分配思想,SLUB 继承了这一哲学并通过元数据内嵌和 per-CPU 优化将其推向极致,SLOB 则服务于极致轻量场景。三者共享 kmem_cache_alloc()/kmem_cache_free() 接口,向上层内核子系统(文件系统、网络栈、驱动)提供统一的内存抽象。
掌握三种分配器的内部结构、分配路径与参数调节手段,是每一位 Linux 内核开发与运维工程师的必修课。在实际生产中,应养成通过 /proc/slabinfo 和 slabtop 定期巡检的习惯,对 active_objs 异常增长保持警惕,以构建稳定高效的内核内存环境。
参考资料
- Jeff Bonwick, "The Slab Allocator: An Object-Caching Kernel Memory Allocator", USENIX 1994
- Christoph Lameter, "SLUB: The unqueued slab allocator", Linux Symposium 2007
- Linux 内核源码:mm/slab.c、mm/slub.c、mm/slob.c
Documentation/vm/slub.rst

发表评论 取消回复