0. 前言

buddy 系统 中分配内存时是以 page 为单位,在实际运用中很多内存需求是以字节为单位的,若分配一个页面,则会浪费很多的内存空间。早期 Linux 内核实现了以 字节为大小的内存块分配机制,这个机制非常类似于 buddy 系统,这个简单的机制虽然减少了内存浪费,但是并不高效。

早期的伙伴(buddy)系统分配内存“粗犷”到每次至少给你一页(通常是 4 KB),哪怕只要几十个字节,也得浪费掉 99.9%!这带来了两个大麻烦:

  1. 内存利用率低
    小块需求得整页给——99.9% 的空间都闲着,看着都心疼;

  2. 分配效率低且访问慢
    要走长长一串伙伴算法流程,遇到不够还得压缩或回收,分分钟卡住;更糟的是,每次至少拿一个页框,数据不在高速缓存里,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_structfs_structinode)各自建立一个 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)区域是否被篡改

    • 验证 bufctlfreelist 链表结构完整性

  • 生效条件:仅当内核编译时启用了 CONFIG_DEBUG_SLAB

  • 场景:调试阶段使用,用于尽早捕获内存损坏、双重释放等 slab 层面的错误;务必不要在生产环境下开启(性能开销极大)。


1.2.2 SLAB_RED_ZONE (红区保护)

  • 宏定义0x00000400U

  • 作用:在每个对象前后分别插入“红区”(red-zone)填充字节,用来检测越界读写。若访问落入红区,内核会立刻报错并定位。

  • 生效条件:仅当启用 CONFIG_DEBUG_SLAB

  • 场景:定位 buffer overflow、off-by-one 等边界错误;红区大小受 kmem_cachered_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_DMA0x00004000U

    • SLAB_CACHE_DMA320x00008000U

  • 作用:强制分配的 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

  • 场景:用于高度关键的内存池——如果没有这部分内存,系统就不应该继续运行;比如早期网络栈的关键控制结构等。

中文解释

变量

备注

Red-zone 检测

SLAB_RED_ZONE

slub_debug=Z

Poison(内存填充)

SLAB_POISON

slub_debug=P

一致性检查

SLAB_CONSISTENCY_CHECKS

slub_debug=F

记录调用者

SLAB_STORE_USER

slub_debug=U

Panic on fail

SLAB_PANIC

slub_debug=F

备注:关于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 (或每个线程组)上维护的本地缓存结构,用于:

  1. 快速分配/释放路径:绝大多数 alloc/free 调用都在本地完成,避免跨 CPU 的锁竞争和远程内存访问。

  2. 批量操作管理:在本地累积一定数量的对象后,一次性从全局(partial slab 链表)中 refill 或 flush,降低全局同步开销。

  3. 事务一致性:通过 tid 唯一标识一次 refill/flush 操作,保证多核场景下的安全与高效。

字段名

含义

freelist

指向本地缓存中下一个可用对象的指针数组;alloc 时直接 pop,free 时直接 push。

tid

每次从全局 partial slab refill 或将满页 slab flush 回 partial 时,都会递增该 ID,用于检测并发冲突。

page

当前正在使用的 slab 页:当 freelist 用尽,要 refill 时,就在此取新的页;当 freelist 超出上限,要 flush,也在此引用。

partial

(仅在 CONFIG_SLUB_CPU_PARTIAL 下)存放那些已“冻结”但尚未被全局接管的 slab 页,优化 batch 管理。

lock

保护上述字段的自旋锁;由于是 per-CPU,冲突概率极低,主要用于 guard 一次完整的 refill/flush 事务。

stat[]

(仅在 CONFIG_SLUB_STATS 下)记录本地 alloc hit/miss、free hit/miss、refill/flush 次数等,用于调试和性能分析。

使用场景简介

  • 快速分配(kmem_cache_alloc

    • 调用路径先尝试从本 CPU 的 freelist pop 一个对象:

      if (cpu->freelist)
          return pop(cpu->freelist);

    • 如果 freelist 为空,进入一次 refill 事务

      1. 拿到 lock,读 tid 做事务标记

      2. 从 global partial slab 链表或新建 slab 中批量(batchcount)抓取对象到 freelist

      3. 释放 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 的次数

    • 通过这些数据,可动态调整 batchcountlimitcpu_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_limitnext_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 链表。

字段

作用

何时使用

slab_list

SLAB 模式下,把这个 page 挂到 slabs_partial/slabs_full/slabs_free 链表时用的双向链表头

SLAB

next

SLAB 模式下,部分分配 slab 的单链表指针;用在 array_cache 等快速路径的 partial 队列里

SLAB 专用

pages

SLAB 模式下,该 slab(可能跨多页)剩余的可用页数

SLAB

pobjects

SLAB 模式下,对该 slab 中空闲对象的粗略计数

SLAB

slab_cache

指向该 slab 属于哪个 kmem_cache

SLAB/SLUB

freelist

链向 slab 中下一个可用对象(对象地址即为返回给用户的指针)

SLAB/SLUB/SLOB

s_mem

SLAB 专用:slab 内对象区域的起始地址(第一个对象)

SLAB

counters

SLUB 专用:压缩存储 inuseobjectsfrozen 三个值的整数字段

SLUB

inuseobjectsfrozen

SLUB 专用的位域视图,用于分别存储当前已分配对象数、总对象数及冻结标志

SLUB

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 的数据结构中数据覆盖