引言
在内核编程中,内存分配是一个高频操作。与用户态的 malloc/free 不同,内核内存分配面临着独特的约束:不能睡眠(在原子上下文中)、不能触发缺页异常(无用户地址空间后备)、需要处理 GFP 标志控制分配行为,以及对性能和碎片化的极致要求。为了解决这些问题,Linux 内核构建了一套三层内存分配体系:Buddy System(页分配器)→ SLAB/SLUB/SLOB(对象缓存分配器)→ kmalloc/vmalloc(通用分配接口)。
本文将深入剖析这套体系的第三层——SLAB/SLUB/SLOB 对象分配器,从设计哲学到实现细节,从 fast path 到 slow path,从调试工具到性能调优,给出完整的实战指南。
一、为什么需要对象分配器
1.1 Buddy System 的局限
Buddy System 以页(通常 4KB)为最小分配单位。当内核需要分配一个 task_struct(约 6KB)或一个 inode(约 600 字节)时,直接通过 buddy system 分配会造成严重的内部碎片——每个对象只占一页的一小部分,剩余空间白白浪费。
更关键的是,内核中大量对象需要频繁创建和销毁(如网络数据包 sk_buff、文件描述符、定时器)。每次构造/析构都涉及复杂的初始化(锁分配、链表头初始化、引用计数归零等),纯页分配方案下重复的初始化开销将极大拖慢系统性能。
1.2 对象分配器的核心需求
- 细粒度分配:分配小于一页的任意大小对象,消除内部碎片
- 对象复用:释放的对象不立即归还内存,而是放入空闲链表供下次复用,避免重复的初始化和析构
- 缓存友好:同一类型的对象大小和布局一致,利于 CPU cache 命中
- per-CPU 缓存:每个 CPU 维护本地空闲对象池,消除多核竞争,实现无锁快速分配
- NUMA 感知:优先从本地 NUMA 节点分配,降低跨节点访问延迟
二、SLAB 分配器的设计哲学
2.1 核心概念:Slab 与 Cache
SLAB 分配器由 Jeff Bonwick 于 1994 年为 Solaris 设计,Linux 内核从 2.1.23 版本开始引入。其核心数据结构包括:
- kmem_cache:一种对象类型的"缓存"描述符(如
task_struct_cachep、inode_cachep)。它记录了对象的对齐方式、构造/析构函数、每 slab 对象数等元信息 - Slab:一个或多个连续物理页的集合,被整齐划分为若干等大对象。一个 slab 有三种状态:full(全部分配)、partial(部分分配)、empty(完全空闲)
2.2 Slab 的内部布局
一个 slab 的内存布局可分为两部分:
- 对象区:紧密排列的 N 个等大小对象槽位
- 空闲链表(free list):在对象区的头部或尾部嵌入一个单向链表,记录哪些槽位是空闲的。空闲对象的前若干字节直接存储下一个空闲对象的指针——这是一种巧妙的"零开销"链表设计
这种嵌入式空闲链表设计意味着:空闲对象的内存空间本身被借用来存储指针,不需要额外的元数据内存。
2.3 Slab 着色(Slab Coloring)
由于 slab 中所有对象的起始偏移相对于页面起始是固定的,同一类型 slab 中的对象在 CPU cache 中的映射位置几乎一致。这会导致cache line 冲突——多个对象竞争同一 cache set 造成频繁的 cache eviction。
着色(coloring)的解决方案是:在不同 slab 中引入不同偏移量(color offset),使对象的 cache line 映射微微错开。kmem_cache 中维护的 colour_off 字段即是每次 slab 创建时的偏移增量。
2.4 SLAB 的 slab 管理
每个 kmem_cache 维护三个链表来管理其 slab:
- slabs_full:全部对象已被分配的 slab
- slabs_partial:部分对象已分配、仍有空闲槽位的 slab(优先从此链表分配)
- slabs_empty:完全空闲的 slab(内存紧张时归还给 buddy system)
分配时的查找顺序:partial → 新建 slab。释放时的状态迁移:partial → full(若全部分配完)或 empty(若全部分配)。
三、SLUB 分配器:Linux 的默认选择
3.1 SLUB 取代 SLAB 的原因
SLAB 虽然设计精巧,但代码复杂度随功能增长急剧膨胀。SLUB(Unqueued SLAB)由 Christoph Lameter 于 2007 年推出,目标是大幅简化实现同时保持性能。主要改进包括:
- 移除每 CPU/每节点的复杂 queued 管理
- 简化 slab 链表:每 CPU 只有一个 active slab
offset:空闲链表指针在对象中的偏移size:对象实际大小(含对齐)object_size:用户请求的原始大小(不含元数据)inuse:对象中用户数据的字节数min_partial:节点 partial 链表的最少保留 slab 数- 检查
s->cpu_slab->freelist(per-CPU 空闲链表)是否非空 - 若非空,直接弹出链表头对象,无需任何锁或原子操作
- 返回对象
- 从当前 CPU 的 active slab 中获取新对象(如果 active slab 还有空闲槽位)
- If active slab 已满,从 NUMA 节点的 partial 链表 摘下一个 slab 作为新 active slab
- If partial 也为空,调用 buddy system 分配新的 slab
- 新 slab 被加入 active 和 node 的 partial 管理
partial:该节点上的 partial slab 链表nr_slabs、total_objects:统计信息- 优点:代码量极小(约 300 行),内存开销极小,适合嵌入入式场景
- 缺点:严重碎片化(外部碎片 + 内部碎片),性能差(O(n) 搜索),无 per-CPU 缓存,无 NUMA 支持
GFP_KERNEL:标准标志,允许睡眠、允许 I/O 回写、允许直接或压缩回收(适用于大多数可睡眠上下文)GFP_ATOMIC:原子要求, 禁止睡眠和回收, 从紧急预留内存中分配(中断上下文、持有自旋锁时使用)GFP_DMA/GFP_DMA32:限定分配区域为 ISA DMA 兼容的低端内存GFP_NOFS:禁止文件系统调用(持有文件系统锁时使用)GFP_NOIO:禁止 I/O 操作(块设备层使用)__GFP_HIGH/__GFP_MEMALLOC:允许访问紧急预留内存(内存回收路径使用)- 大内存分配请求(超过连续物理页可用大小)
- 只需要虚拟连续性(如加载内核模块、DMA scatter-gather 映射、ioremap)
- 不用于需要物理连续性的硬件 DMA 操作
kzalloc(size, flags):分配并零填充(等价于kmalloc + memset但优化为一次性标记)krealloc(ptr, new_size, flags):重新分配(尝试原地扩展或分配新区域并拷贝)kfree(ptr):释放( 传入 NULL 安全,不会睡眠kcalloc(n, size, flags):分 配 n 个大小为 size 的元素并 零填充,有溢出检查
Linux 2.6.23 起 SLUB 成为默认分配器。
3.2 SLUB 的核心数据结构
SLUB 的设计比 SLAB 更精简。struct kmem_cache 中的关键字段:
3.3 SLUB 的 per-CPU 热路径
SLUB 的快速分配路径(fast path)极其简洁:
这意味着在缓存充足的情况下,一次 SLUB 分配仅需几条指令——性能接近寄存器操作。
3.4 SLUB 的慢速路径(slow path)
当 per-CPU freelist 为空时,进入 slow path:
3.5 NUMA 感知
SLUB 在 NUMA 系统中为每个 NUMA 节点维护一个 kmem_cache_node 结构,包含:
分配时优先从本地节点的 partial 链表获取 slab,失败时才 fallback 到其他节点或分配新 slab。
四、SLOB 分配器:嵌入式设备的极简方案
SLOB(Simple List Of Blocks)是三种分配器中最简单的,专为内存极受限的嵌入式系统设计。
4.1 设计思路
SLOB 将所有空闲内存块组织成一个简单的 的空闲链表,使用 最佳适配(best-fit)算法查找最合适的空闲块。每个空闲块头部存储块大小和指向下一个空闲块的指针。
4.2 优缺点
4.3 选择时机
CONFIG_SLOB 在 CONFIG_EXPERT 开启时才可见。典型场景:内存 < 16MB 的嵌入式设备,对内存精确度要求极致而对性能不敏感。
五、kmalloc 与 vmalloc
5.1 kmalloc:物理连续的内存分配
kmalloc 基于 SLUB 分配器,返回物理连续的虚拟内存区域。对于大尺寸请求(如 > 2 页),回退到 buddy system 的 alloc_pages。
关键GFP标志:
kmalloc 的分配大小有上限(通常 4MB~8MB 取决于配置),且分配时间不确定(因为允许回收和压缩)。
5.2 vmalloc:虚拟连续、物理不连续的内存
vmalloc 分配的内存虚拟地址连续但物理页不连续。它通过修改页表将离散的物理页映射到连续的虚拟地址区域。
适用场景:
重要区别:vmalloc 比 kmalloc 慢(需修改页表、TLB flush),且每次访问可能触发更多 cache miss(物理页不连续)。能用 kmalloc 就不要用 vmalloc。
5.3 辅助函数
六、专用缓存 vs 通用缓存
6.1 通用缓存:kmalloc 的 size-class 体系
kmalloc 并不直接为每次请求创建新 cache,而是使用一组预建的通用缓存(kmalloc_caches 数组)。这些缓存覆盖从 8 字节到 8K+ 字节的多个 size class:
在 x86_64 上,典型的 size class 序列为:8, 16, 24, 32, 48, 64, 96, 128, 192, 256, 384, 512, 768, 1024, 1536, 2048, 3072, 4096, 6144, 8192...
调用 kmalloc(300, GFP_KERNEL) 时,分配器找到第一个 ≥ 300 字节的 size class(512 字节)对应的 cache 并从其分配。
6.2 专用缓存:kmem_cache_create
当内核需要对某类频繁创建/销毁的特定结构体做内存分配时,会创建专用缓存:
// 典型的专用缓存创建
struct kmem_cache *task_struct_cachep =
kmem_cache_create("task_struct",
arch_task_struct_size, // 对象大小
SLAB_PANIC | SLAB_ACCOUNT, // flags
SLAB_HWCACHE_ALIGN | ...); // 构造/析构函数等
创建后通过 kmem_cache_alloc(cachep, flags) 和 kmem_cache_free(cachep, obj) 使用。
6.3 为什么需要专用缓存
- 消除内部碎片:通用缓存的 size class 存在间距(如 256 → 384 之间有 128 字节空隙),专用缓存精确匹配结构体大小
- 构造/析构回调:释放到 slab 时自动调用析构函数清理复杂状态(如注销/proc 条目)
- 统计与调试:每个 cache 有独立的 /proc/slabinfo 条目和 tracepoint
- Red zone 与 usercopy:专用缓存可启用更多的安全调试特性
七、性能优化与调试
7.1 /proc/slabinfo:分配器状态全景
/proc/slabinfo 列出了所有活动缓存的统计信息:
# name <active_objs> <num_objs> <objsize> <objperslab> <pagesperslab> : tunables <limit> <batchcount> <sharedfactor> : slabdata <active_slabs> <num_slabs> <sharedavail>
关键字段解读:
- active_objs / num_objs:已分配总数 / 总对象数(含空闲)
- objsize:对象实际大小(含元数据和对齐)
- active_slabs / num_slabs:在用 slab 数 / 总 slab 数
- num_objs - active_objs ≈ 0:所有 slab 都是 full,内存紧张
7.2 slabtop:实时监控
slabtop 命令提供类似 top 的实时 slab 监控视图,按活跃对象数排序,快速定位异常分配。
7.3 SLUB_DEBUG:Use-After-Free 和越界检测
开启 CONFIG_DEBUG_SLUB 后,SLUB 启用多种调试机制:
- Red Zoning:在每个对象周围插入特殊魔数(
0x1b 0xab 0xab ...),检测写越界 - Poisoning:释放时用特定染色字节(如
0x5a)填充对象内存,使 UAF 即刻瘫痪 - Tracking:记录每次分配/释放的调用栈,可在
/sys/kernel/debug/slab/<cache>/alloc_traces和free_traces中查看
7.4 KASAN + SLUB:动态内存错误检测
KASAN(Kernel Address SANitizer)基于编译器插桩(-fsanitize=address)实现,与 SLUB 紧密集成:
- 为每个对象添加 shadow memory(1/8 标记比例)
- 分配时标记为
valid,释放后标记为poisoned - 每次内存加载/存储时检测影子内存,OOB 和 UAF 立即触发
BUG() - 结合 SLUB 的 quarantine 延迟释放机制,让 UAF 逃逸时间更长,更容易捕获
7.5 性能调优参数
通过 sysfs 调优 SLUB 行为:
/sys/kernel/slab/<cache>/order:每个 slab 的页数(2^order),增大可减少 partial 链表遍历/sys/kernel/slab/<cache>/min_partial:节点保留的最少 partial slab 数,防止 slab 被完全回收/sys/kernel/slab/<cache>/cpu_partial:per-CPU partial 列表中的最大对象数
八、实战:内核模块中的内存分配模式
模式 1:小对象高频分配
// 推荐:kmalloc 走 SLUB fast path
void *buf = kmalloc(64, GFP_KERNEL);
if (!buf) return -ENOMEM;
// 使用 buf...
kfree(buf);
// 更好:如果结构体有大量 create/destroy,用专用 cache
struct kmem_cache *my_cache = kmem_cache_create("my_struct",
sizeof(struct my_struct), 0, SLAB_HWCACHE_ALIGN, NULL);
struct my_struct *obj = kmem_cache_alloc(my_cache, GFP_KERNEL);
kmem_cache_free(my_cache, obj);
模式 2:大块分配(接近页大小)
// kmalloc 可以处理到数 MB,但如果物理连续页不足会失败
void *big = kmalloc(256 * 1024, GFP_KERNEL); // 尝试 SLUB → fallback buddy
// 仅需虚拟连续时用 vmalloc
void *big_v = vmalloc(1024 * 1024); // 物理不连续
vfree(big_v);
模式 3:中断上下文原子分配
// 错误:GFP_KERNEL 可能睡眠导致死锁
// void *buf = kmalloc(size, GFP_KERNEL); // 中断上下文中禁止!
// 正确:使用 GFP_ATOMIC,永不睡眠
void *buf = kmalloc(size, GFP_ATOMIC);
if (!buf) {
// 原子上下文内存分配失败是严重事件
return -ENOMEM;
}
模式 4:DMA 分配
// 硬件 DMA 需要物理连续且落在设备可寻址范围
dma_addr_t dma_handle;
void *buf = dma_alloc_coherent(dev, size, &dma_handle, GFP_KERNEL);
// 使用 buf 和 dma_handle 设置 DMA 描述符...
dma_free_coherent(dev, size, buf, dma_handle);
九、源码剖析:SLUB 分配器的关键函数路径
9.1 kmem_cache_alloc → __kmem_cache_alloc
虽然 SLUB 的核心实现在 mm/slub.c 中,但分配入口从 kmem_cache_alloc 宏展开后:
// include/linux/slub_def.h 中的 inline fast path
static __always_inline void *slab_alloc(struct kmem_cache *s, gfp_t gfpflags, unsigned long addr)
{
// 尝试 per-CPU freelist 快速分配
void **object;
struct kmem_cache_cpu *c = raw_cpu_ptr(s->cpu_slab);
redo:
object = c->freelist;
if (unlikely(!object || !object[-1]))
goto slowpath; // per-CPU 无空闲 → slow path
object[-1] = 0; // 清除 Debug 标记
c->freelist = object[0]; // 链表头弹出
c->page->inuse++; // 增加在用计数
return object;
slowpath:
return slab_alloc_slow(s, gfpflags, addr);
}
9.2 new_slab:创建新 slab
static struct page *new_slab(struct kmem_cache *s, gfp_t gfpflags)
{
struct page *page = alloc_pages_node(...) // 从 buddy system 获取连续页
// 初始化 page 的 slab 元数据
page->freelist = start; // 空闲链表起始
page->inuse = 0; // 在用对象数
page->objects = s->objects_per_slab;
page->slab_cache = s;
return page;
}
9.3 deactivate_slab 与 partial 链表管理
当 per-CPU slab 被用完或需要重新平衡时,deactivate_slab 将 slab 从 per-CPU active 中移除:
- 若 slab 中仍有空闲对象(partial)→ 加入
kmem_cache_node->partial链表 - 若 slab 中无空闲对象(full)→ 加入 node 的 full 链表(仅 SLAB/SLUB debug 模式;普通模式下 full slab 脱离 slab 缓存管理)
十、演进趋势:SLUB 的未来方向
现代 Linux 内核(5.x+)中 SLUB 仍在持续演进:
10.1 BPF memcg 整合
Linux 5.14 起,SLUB 支持 memcg(cgroup memory controller)对 SLUB 缓存的精确计费和限制。kmem_cache_charge() 将内核内存分配纳入 cgroup 审计。
10.2随机化分配(SLAB_RANDOM)
slab 内部的对象偏移引入随机化,减少 slab 缓存侧信道攻击面(针对 speculative execution 攻击的缓解)。
10.3 Folio与 SLAB 协同
Linux 引入 Folio(复合页)概念,将透明大页(THP)和文件页统一管理。SLUB 在分配超大对象时可与 folio 协作以减少 TLB miss。
10.4 Tight Memory Reclaim
未来内核计划引入更积极的"温和回收"策略,在空闲 slab 到达空白时将部分物理页异步返回给 buddy system,但保留 cache 状态以加速后续重新分配。
总结
Linux 内核的对象分配体系是一个精巧的多层结构:
- Buddy System 提供页框(page frame)级别的物理内存分配
- SLUB(默认) 在其上构建 per-NUMA-node + per-CPU 的空闲对象缓存,实现无锁快速分配
- kmalloc/vmalloc 提供统一的应用层接口,封装了GFP标志语义
- 专用缓存(kmem_cache_create)为高频特定结构体提供定制化分配
理解这套机制不仅有助于编写高性能内核代码,更能帮助诊断内存泄漏、越界访问等复杂问题。关键要点总结:
- 小对象、高频分配 → kmalloc(走 SLUB cache,优先 fast path)
- 大对象、仅需虚拟连续 → vmalloc(修改页表,性能较低)
- 特定结构体、大量 create/destroy → kmem_cache_create + kmem_cache_alloc/free
- 中断/原子上下文 → GFP_ATOMIC,禁止 GFP_KERNEL
- DMA 硬件操作 → dma_alloc_coherent(物理连续 + 设备可寻址)
- 调试内存错误 → 开启 SLUB_DEBUG + KASAN + red zone + poisoning
- 性能分析 → /proc/slabinfo、slabtop、tracepoint
内存分配是系统稳定与性能的基石,深入理解 SLUB/SLAB 是每个内核开发者的必修课。

发表评论 取消回复