0. 前言
buddy 系统 中分配内存时是以 page 为单位,在实际运用中很多内存需求是以字节为单位的,若分配一个页面,则会浪费很多的内存空间。早期 Linux 内核实现了以 字节为大小的内存块分配机制,这个机制非常类似于 buddy 系统,这个简单的机制虽然减少了内存浪费,但是并不高效。
早期的伙伴(buddy)系统分配内存“粗犷”到每次至少给你一页(通常是 4 KB),哪怕只要几十个字节,也得浪费掉 99.9%!这带来了两个大麻烦:
内存利用率低
小块需求得整页给——99.9% 的空间都闲着,看着都心疼;分配效率低且访问慢
要走长长一串伙伴算法流程,遇到不够还得压缩或回收,分分钟卡住;更糟的是,每次至少拿一个页框,数据不在高速缓存里,cache miss 频发。
真正迎来转机的是 Solaris 2.4:Jeff Bonwick 在论文《The Slab Allocator: An Object-Caching Kernel Memory Allocator》中提出了 slab 分配器。它的核心思路超简单——“有旧的先用旧的”:
按对象类型造仓库
提前为每种常见内核对象(比如struct fs_struct
)准备专属缓存;释放后不还伙伴
对象释放时,先放回自己类型的仓库,下次再来相同大小的,直接复用;命中率蹭蹭涨
旧对象往往还驻留在 CPU cache,复用时命中率高,访问速度飞起。
不过原版 slab 为了管理各种构造/析构、对齐等 metadata,代码量和内存开销都不小。Linux 后来又演化出两位“兄弟”:
SLUB:用“页框组”管理对象、借用
struct page
空闲字段做账,元数据少 50%,常见场景下性能提升 5%–10%;SLOB:极简版,只保留最基本的链表与首次适配算法,专门跑在超紧凑的嵌入式设备上,但在速度和碎片控制上稍逊。
接下来,我们聚焦 SLUB,从初始化到分配、释放,一步步揭开它靠批量操作、无锁路径和每 CPU 本地 cache 实现高效与低碎片的秘诀。
1. 核心数据结构
1.1 struct kmem_cache
作用
代表一种“对象类型”的缓存池。内核启动时或动态创建时,会为常见的内核对象(如 task_struct
、fs_struct
、inode
)各自建立一个 kmem_cache
,用它来管理该类型对象的分配与回收。
关键成员
///每个slab描述符,都有一个struct kmem_cache数据结构来抽象描述
struct kmem_cache {
struct array_cache __percpu *cpu_cache; ///本地缓存池,每个CPU一个
/* 1) Cache tunables. Protected by slab_mutex */
///当本地的对象缓存池为空时,需要从共享缓存池或者slans_partial/slabs_free中,
///获取batchcount个对象到本地缓存池
unsigned int batchcount;
///当本地缓存池空闲数目大于limit时,需要释放一些对象,就是batchcount个,便于内核回收或销毁
unsigned int limit;
unsigned int shared; ///用于多核系统
unsigned int size; ///对象的长度,align对齐
struct reciprocal_value reciprocal_buffer_size;
/* 2) touched by every alloc & free from the backend */
///对象分配掩码
slab_flags_t flags; /* constant flags */
///一个slab最多可以有多少个对象
unsigned int num; /* # of objs per slab */
/* 3) cache_grow/shrink */
/* order of pgs per slab (2^n) */
///一个slab占用多个2的order次方物理页面
unsigned int gfporder;
/* force GFP flags, e.g. GFP_DMA */
gfp_t allocflags;
///一个slab分配器有多少个不同的高速缓存行,用于着色
size_t colour; /* cache colouring range */
///一个着色区长度,和L1高速缓存行大小相同
unsigned int colour_off; /* colour offset */
///用于OFF_SLAB模式的slab分配器,使用额外的内存来保存slab管理区域
struct kmem_cache *freelist_cache;
///每个对象要占用1字节来存放freelists
unsigned int freelist_size;
/* constructor func */
void (*ctor)(void *obj);
/* 4) cache creation/removal */
///slab描述符名字
const char *name;
///链表节点,用于把slab描述符添加到全局链表slab_caches中
struct list_head list;
///本描述符引用计数
int refcount;
///对象实际大小
int object_size;
///对齐长度
int align;
/* 5) statistics */
#ifdef CONFIG_DEBUG_SLAB
unsigned long num_active; /* 当前正被使用(allocated but not freed)的对象总数 */
unsigned long num_allocations;/* 累计分配(kmem_cache_alloc)次数 */
unsigned long high_mark; /* 历史最高的 num_active 值(峰值并发使用量) */
unsigned long grown; /* slab “增长”次数:从 partial/slabs_empty 中取空 slab 或新建 slab 的总次数 */
unsigned long reaped; /* slab “收割”次数:将完全空闲的 slab 返还给伙伴系统的总次数 */
unsigned long errors; /* 运行时遇到的错误次数(如分配失败、参数校验失败等) */
unsigned long max_freeable; /* 在任意时刻,整 cache 中可一次性返还给伙伴系统的最大页数 */
unsigned long node_allocs; /* NUMA 环境下,从本节点分配 slab 对象的次数 */
unsigned long node_frees; /* NUMA 环境下,向本节点返还 slab 对象的次数 */
unsigned long node_overflow; /* NUMA 本地缓存溢出的次数(本地 node cache 空间不足需借用或返还) */
atomic_t allochit; /* 分配时直接命中 per-CPU cache 的次数(无需访问 partial/full) */
atomic_t allocmiss; /* 分配时未命中 per-CPU cache,需要去 partial/full 或新建 slab 的次数 */
atomic_t freehit; /* 释放时直接回填 per-CPU cache 的次数 */
atomic_t freemiss; /* 释放时未能回填 per-CPU cache,需要 flush 回 partial/full 的次数 */
/*
* If debugging is enabled, then the allocator can add additional
* fields and/or padding to every object. 'size' contains the total
* object size including these internal fields, while 'obj_offset'
* and 'object_size' contain the offset to the user object and its
* size.
*/
int obj_offset; /* 对象在 slab 内的偏移(bytes),仅在调试模式下有效:
slab 区块前后可能插入 red-zone、poison 区等额外字段,
用户实际对象的起始地址 = slab_start + obj_offset */
#endif /* CONFIG_DEBUG_SLAB */
#ifdef CONFIG_KASAN
struct kasan_cache kasan_info; // 这个在后续kasan关于slab的原理部分会介绍
#endif
#ifdef CONFIG_SLAB_FREELIST_RANDOM
unsigned int *random_seq;
#endif
unsigned int useroffset; /* Usercopy region offset */
unsigned int usersize; /* Usercopy region size */
///slab节点,在NUMA系统中,每个节点有一个kmem_cache_node数据结构
///kmem_cache_node,维护的对象是slab,
///array_cache维护的对象是slab的obj
struct kmem_cache_node *node[MAX_NUMNODES];
};
1.2 slab_flags_t flags
该 slab 特有的标志,用于slab 分配、释放过程中,这个很重要。
/*
* Flags to pass to kmem_cache_create().
* The ones marked DEBUG are only valid if CONFIG_DEBUG_SLAB is set.
*/
/* DEBUG: Perform (expensive) checks on alloc/free */
#define SLAB_CONSISTENCY_CHECKS ((slab_flags_t __force)0x00000100U)
/* DEBUG: Red zone objs in a cache */
#define SLAB_RED_ZONE ((slab_flags_t __force)0x00000400U)
/* DEBUG: Poison objects */
#define SLAB_POISON ((slab_flags_t __force)0x00000800U)
/* Align objs on cache lines */
#define SLAB_HWCACHE_ALIGN ((slab_flags_t __force)0x00002000U)
/* Use GFP_DMA memory */
#define SLAB_CACHE_DMA ((slab_flags_t __force)0x00004000U)
/* Use GFP_DMA32 memory */
#define SLAB_CACHE_DMA32 ((slab_flags_t __force)0x00008000U)
/* DEBUG: Store the last owner for bug hunting */
#define SLAB_STORE_USER ((slab_flags_t __force)0x00010000U)
/* Panic if kmem_cache_create() fails */
#define SLAB_PANIC ((slab_flags_t __force)0x00040000U)
1.2.1 SLAB_CONSISTENCY_CHECKS (调试检查)
宏定义:
0x00000100U
作用:在每次分配 (
alloc
) 和释放 (free
) 时,都执行额外的完整一致性检查,包括:确认对象未重复释放
校验红区(red-zone)与毒化(poison)区域是否被篡改
验证
bufctl
或freelist
链表结构完整性
生效条件:仅当内核编译时启用了
CONFIG_DEBUG_SLAB
场景:调试阶段使用,用于尽早捕获内存损坏、双重释放等 slab 层面的错误;务必不要在生产环境下开启(性能开销极大)。
1.2.2 SLAB_RED_ZONE
(红区保护)
宏定义:
0x00000400U
作用:在每个对象前后分别插入“红区”(red-zone)填充字节,用来检测越界读写。若访问落入红区,内核会立刻报错并定位。
生效条件:仅当启用
CONFIG_DEBUG_SLAB
场景:定位 buffer overflow、off-by-one 等边界错误;红区大小受
kmem_cache
的red_left
/red_right
参数控制。
1.2.3 SLAB_POISON
(释放毒化)
宏定义:
0x00000800U
作用:在对象释放时,用固定模式(例如
0xdeadbeef
)填充对象内存,以便后续对已释放内存的访问更易被发现。生效条件:仅当启用
CONFIG_DEBUG_SLAB
场景:调试 use-after-free 错误;配合
SLAB_RED_ZONE
能更全面地捕捉越界与已释放访问。
1.2.4 SLAB_HWCACHE_ALIGN
(硬件缓存行对齐)
宏定义:
0x00002000U
作用:强制将每个对象按 CPU 缓存行大小 (通常 64 B)对齐,避免跨行访问导致的“伪共享”(false sharing)和缓存行争用。
场景:多 CPU 并发访问相邻对象时,能有效降低缓存一致性开销,提升性能;适用于高并发场景下频繁访问小对象的 cache。
1.2.5 SLAB_CACHE_DMA
/ SLAB_CACHE_DMA32
(DMA 可访问内存)
宏定义:
SLAB_CACHE_DMA
—0x00004000U
SLAB_CACHE_DMA32
—0x00008000U
作用:强制分配的 slab 所在的物理内存来自可供 DMA 引擎访问的区域:
SLAB_CACHE_DMA
:低于 16 MB 的 DMA 区域(旧架构)SLAB_CACHE_DMA32
:低于 4 GB 的 32 bit DMA 区域
场景:当缓存要用于 DMA 传输的缓冲区时,必须使用这些标志确保物理地址可达,否则 DMA 操作会失败。
1.2.6 SLAB_STORE_USER
(记录调用者)
宏定义:
0x00010000U
作用:在每个对象的元数据中保存最后一次
alloc
/free
的调用者(通常是保存返回地址),以便在调试时打印出内存泄漏或非法释放的调用堆栈。生效条件:仅当启用
CONFIG_DEBUG_SLAB
场景:定位内存泄漏、非法释放(free)场景;通过
slabinfo
或日志可以看到对象归属。
1.2.7 SLAB_PANIC
(分配失败时崩溃)
宏定义:
0x00040000U
作用:如果
kmem_cache_create()
或后续任何对该 cache 的分配操作失败,直接触发内核 panic,而不是返回NULL
。场景:用于高度关键的内存池——如果没有这部分内存,系统就不应该继续运行;比如早期网络栈的关键控制结构等。
备注:关于slub_debug=xxx,后面会讲!
1.3 struct kmem_cache_cpu
struct kmem_cache_cpu {
void **freelist; /* Pointer to next available object */
unsigned long tid; /* Globally unique transaction id */
struct page *page; /* The slab from which we are allocating */
#ifdef CONFIG_SLUB_CPU_PARTIAL
struct page *partial; /* Partially allocated frozen slabs */
#endif
local_lock_t lock; /* Protects the fields above */
#ifdef CONFIG_SLUB_STATS
unsigned stat[NR_SLUB_STAT_ITEMS];
#endif
};
kmem_cache_cpu
是 SLUB 在 每个 CPU (或每个线程组)上维护的本地缓存结构,用于:
快速分配/释放路径:绝大多数
alloc
/free
调用都在本地完成,避免跨 CPU 的锁竞争和远程内存访问。批量操作管理:在本地累积一定数量的对象后,一次性从全局(partial slab 链表)中 refill 或 flush,降低全局同步开销。
事务一致性:通过
tid
唯一标识一次 refill/flush 操作,保证多核场景下的安全与高效。
使用场景简介:
快速分配(
kmem_cache_alloc
)调用路径先尝试从本 CPU 的
freelist
pop 一个对象:if (cpu->freelist) return pop(cpu->freelist);
如果
freelist
为空,进入一次 refill 事务:拿到
lock
,读tid
做事务标记从 global
partial
slab 链表或新建 slab 中批量(batchcount
)抓取对象到freelist
释放
lock
,返回第一个对象,其余留作后续快速分配。
快速释放(
kmem_cache_free
)首先尝试向本地
freelist
push:if (cpu->freelist_size < limit) push(cpu->freelist, obj);
若本地已满,则进行一次 flush 事务:将当前
page
(或partial
)中的一批对象返还给 global partial slab,更新tid
,再把当前释放的对象放入新一轮的本地freelist
。
统计与调优
在开启
CONFIG_SLUB_STATS
时,stat[]
会累积记录:SLUB_STAT_ALLOC_FAST
: alloc 命中本地freelist
的次数SLUB_STAT_ALLOC_SLOW
: alloc 走 refill 的次数SLUB_STAT_FREE_FAST
: free 命中本地 cache 的次数SLUB_STAT_FREE_SLOW
: free 走 flush 的次数
通过这些数据,可动态调整
batchcount
、limit
、cpu_partial
等参数,以获得更好的延迟/吞吐比。
多核并发安全
因为每个 CPU 拥有独立的
kmem_cache_cpu
,绝大多数操作都不需要全局锁,只在 refill/flush 时才短暂持有本地lock
,大幅降低并发竞争。tid
用来检测在长事务中是否有外部并发修改,确保 refill/flush 原子性。
1.4 struct kmem_cache_node
struct kmem_cache_node {
spinlock_t list_lock;
#ifdef CONFIG_SLAB
///所有slab中有部分obj是空闲的
struct list_head slabs_partial; /* partial list first, better asm code */
///slab中没有空闲obj
struct list_head slabs_full;
///slab中全部是空闲obj
struct list_head slabs_free;
///本节点slab总数
unsigned long total_slabs; /* length of all slab lists */
///本节点空闲slab个数
unsigned long free_slabs; /* length of free slab list only */
///三个链表中空闲对象总和
unsigned long free_objects;
///所有slab分配器上容许空闲对象最大数目
unsigned int free_limit;
unsigned int colour_next; /* Per-node cache coloring */
///本节点所有CPU共享的本地高速缓存
struct array_cache *shared; /* shared per node */
///其他节点所有CPU共享的本地高速缓存
struct alien_cache **alien; /* on other nodes */
unsigned long next_reap; /* updated without locking */
int free_touched; /* updated without locking */
#endif
#ifdef CONFIG_SLUB
unsigned long nr_partial; /* 本节点上 partial slab 的总数 */
struct list_head partial; /* 本节点的 partial slab 链表 */
#ifdef CONFIG_SLUB_DEBUG
atomic_long_t nr_slabs; /* 本节点所有 slab 的总数(用于统计/调试) */
atomic_long_t total_objects; /* 本节点所有 slab 中总对象数(调试用) */
struct list_head full; /* 本节点的 full slab(仅在 debug 下维护) */
#endif
#endif
};
kmem_cache_node
专门负责 按 NUMA 节点(或单节点系统中即全局)管理 slab 的分配与回收。它维护了本节点上属于同一个 kmem_cache
的 slab 链表,以及一些统计和阈值,用来:
局部优先:优先在本地节点上分配与释放,减少跨节点访问延迟。
负载平衡:当本地没有可用 slab 时,可向其他节点 “窃取”(steal)或从伙伴系统申请新 slab。
碎片控制:通过
free_limit
、next_reap
等参数,定期回收过多的空闲 slab,避免长期占用内存。
1.5 struct page中用于slub的部分
struct page {
…
union {
…
struct { /* slab, slob and slub 共用 */
union {
struct list_head slab_list;
struct { /* Partial pages (SLAB 专用) */
struct page *next; /* 指向下一个部分分配 slab 的页 */
#ifdef CONFIG_64BIT
int pages; /* 该 slab 还有多少页未分配(SLAB) */
int pobjects; /* 估算的空闲对象总数(SLAB) */
#else
short int pages;
short int pobjects;
#endif
};
};
struct kmem_cache *slab_cache; /* 所属的 kmem_cache 指针(SLOB 除外) */
void *freelist; /* slab 内下一个可用对象的链头(SLAB/SLUB/SLOB) */
union {
void *s_mem; /* slab 中第一个对象的起始地址(SLAB) */
unsigned long counters; /* SLUB 模式下的 inuse/objects/frozen 位域压缩存储 */
struct { /* SLUB 模式下的位域解读 */
unsigned inuse:16; /* 当前已分配(in-use)的对象数 */
unsigned objects:15; /* slab 总共能容纳的对象数 */
unsigned frozen:1; /* 标记该 slab 是否处于“冻结”状态 */
};
};
};
…
};
…
} _struct_page_alignment;
在 Linux 内核的页描述符 struct page
中,SLAB/SLUB/SLOB 都复用了一段相同的空间来记录与 slab 相关的信息:
链接到 cache:将一个或一组页(slab)挂到某个
kmem_cache
下;管理空闲对象:通过链表或指针,快速找到该 slab 中下一个可用对象;
记录使用状态:跟踪 slab 中正在使用/空闲对象的数量,以便决定何时将 slab 返回给伙伴系统或转入 full/partial/free 链表。
2. 关系图
(PS:图片来之于网络,侵删)
2.1 slub object空间布局
(PS:图片来之于网络,侵删)
首先需要注意,每个object 中 棕色 部分就是存放下一个 free object 地址的地方。图上有两处,分别对应内置和外置 (只能有一处有效)。
如果是内置,在object 头提供 8 个字节空间存放,此时s->offset 为0;
如果是外置,则如上面代码所示,在s->align 之后另外提供8个字节空间存放,此时的s->offset 为经过对齐之后的size;(最终object 的size 需要加上这8个字节)
当然,这个是slab 创建时候临时形成的object 单向链表,当某 object 被申请使用时,object 将从 slab中取出,此时这部分存放下一个obj地址的空间将会被实际object 的数据结构中数据覆盖