Linux内核Slab分配器深度实战:从SLAB到SLUB全链路剖析与性能调优
引言
在Linux内核中,内存分配是操作系统最核心的功能之一。伙伴系统(Buddy System)以页(4KB)为单位管理物理内存,但内核中大量场景需要远小于4KB的对象分配——如task_struct、inode、dentry、文件描述符等。若直接由伙伴系统分配整页,内存浪费将极其严重。Slab分配器应运而生,它以对象(Object)为单位缓存、管理和复用内核结构体,极大提升了小对象分配的效率和内存利用率。
自2.6.23内核起,SLUB取代SLAB成为默认分配器;而在嵌入式等资源受限场景中,SLOB依然有一席之地。本文将深入剖析三种变体的实现原理、数据结构、分配/释放全链路、调试手段和性能调优策略。
1. Slab分配器设计思想
1.1 核心原理
Slab分配器的核心思想是对象缓存与复用:将相同类型的结构体预先分配好并保存在一个高速缓存(cache)中,每次分配时直接从cache取出已初始化的对象,释放时归还而非销毁,避免频繁的构造/析构开销。
工作流程:
- 为特定类型的对象创建kmem_cache,指定对象大小、对齐方式和构造/析构函数
- kmem_cache向伙伴系统申请一个或多个page作为slab,将slab内存按对象大小切分为slots
- 分配时从partial/full slabs链表中取出空闲slot,返回对象指针
- 释放时标记slot为空闲,若slab全空则有机会归还给伙伴系统
1.2 Slab的三种状态
- Full slab:所有slot都已分配,无空闲对象
- Partial slab:部分分配、部分空闲,优先从这里分配
- Empty slab:所有slot均空闲,在系统内存紧张时会被回收
2. SLAB分配器实现剖析
2.1 核心数据结构
struct kmem_cache {
struct kmem_cache_node **node; // NUMA节点数组
struct array_cache __percpu *cpu_cache; // per-CPU批处理缓存
unsigned long flags;
unsigned int object_size; // 实际对象大小
unsigned int size; // 对齐后大小
unsigned int align;
const char *name;
void (*ctor)(void *obj); // 构造函数
struct list_head list; // 全局cache链表
unsigned long num; // 每slab对象数
gfp_t alloc_gfp; // 分配标志
};
struct kmem_cache_node {
struct list_head slabs_full;
struct list_head slabs_partial;
struct list_head slabs_free;
unsigned long free_objects; // 空闲对象计数
unsigned int free_limit;
};
struct slab {
union {
struct {
struct list_head slab_list; // 链入full/partial/free
void *s_mem; // slab中第一个对象地址
unsigned int inuse; // 已使用对象数
unsigned int free; // 第一个空闲对象索引
};
struct kmem_cache *slab_cache; // SLUB复用此union
};
};
2.2 SLAB对象布局(kmem_cache slab obj)
经典SLAB中,每个slab的管理结构(struct slab)位于slab内存区域的末尾,开头是freelist数组(记录空闲对象索引),中间是对象内存区域。coloring机制通过在slab开头添加不同大小的填充,缓解Cache Line冲突。
3. SLUB分配器——现代默认方案
3.1 设计改进
SLUB(Unqueued Slab Allocator)针对SLAB的复杂元数据进行了简化:
- 将slab管理结构直接嵌入page结构体,无需外部冗余存储
- 每个CPU维护单节点缓存,摒弃了SLAB的多队列设计
- per-CPU partial slab支持无锁批量操作
- freelist使用链表而非索引数组,CPU cache-friendly
3.2 SLUB核心数据结构
// page结构体中与slab相关的字段(SLUB嵌入)
struct page {
...
union {
struct {
unsigned long flags;
...
struct {
union {
struct list_head lru;
struct {
union {
struct page *next; // partial链表指针
};
int pages; // compound order
int pobjects; // partial中空闲对象数
};
};
struct kmem_cache *slab_cache; // 指向所属cache
void *freelist; // 指向第一个空闲对象
...
};
};
};
};
struct kmem_cache {
struct kmem_cache_cpu *cpu_slab; // per-CPU
struct kmem_cache_node *node[MAX_NUMNODES];
unsigned long flags;
unsigned int offset; // 空闲指针偏移(Red Zone检测用)
unsigned int object_size;
unsigned int inuse;
unsigned int size;
unsigned int align;
unsigned long min_partial; // node中最小partial数
unsigned int cpu_partial; // per-CPU partial批处理数
const char *name;
struct list_head list;
...
};
3.3 SLUB分配全链路
kmem_cache_alloc(cache, flags)
└─ __kmem_cache_alloc(cache, flags)
│
├─ CPU空闲对象(freelist)存在?
│ ├─ 是 → 直接pop,快速路径(无锁,2-3条指令)
│ └─ 否 → new_slab()
│ ├─ partial slab存在? → 提升为CPU slab
│ └─ 无 → 向伙伴系统申请新page
│ └─ page_address()初始化对象布局
│ └─ 构建freelist(空闲对象链表)
│
└─ 返回对象指针
3.4 SLUB释放全链路
kmem_cache_free(cache, obj)
└─ __slab_free(cache, obj)
│
├─ 对象属于当前CPU slab?
│ ├─ 是 → push到CPU freelist,快速路径
│ └─ 否 → 判断对象所在slab状态
│ ├─ 原slab full → 移入partial
│ └─ 原slab partial → 若超过free_limit则归还
│
└─ (若部分空闲数超过cpu_partial阈值,补充给node partial链表)
4. SLOB——超轻量嵌入式方案
SLOB(Simple List Of Blocks)是为内存受限系统设计的极简分配器,匹配kmalloc请求到最佳空闲块(最佳适应算法),代码量极少(约600行),但存在严重外部碎片问题,不推荐用于通用系统。
5. 三种分配器对比
| 特性 | SLAB | SLUB | SLOB |
|---|---|---|---|
| 引入版本 | Linux 2.1 | Linux 2.6.23 | Linux 2.6 |
| 默认内核 | Solaris风格 | Linux2.6.23 | 嵌入式 |
| 元数据结构 | 独立struct slab | 嵌入page结构 | 页内位图 |
| NUMA支持 | 多节点队列 | node cpu | 无 |
| 调试能力 | slub_debug(P/F/U/Z/R/A) | 有限 | |
| 内存开销 | 较高 | 低 | 最低 |
| 性能 | 良好 | 最优 | 一般 |
| 适用场景 | 服务器 | 桌面/服务器/云 | 嵌入式 |
6. Slab调试与监控
6.1 /proc/slabinfo
$ cat /proc/slabinfo
slabinfo - version: 2.1
# name

发表评论 取消回复