内存分配优化:内核工程slab与vmalloc实战 - 第2节
2.1 对象缓存思想:从通用分配到专用缓存
伙伴系统(Buddy System)以页框(通常 4KB)为最小单位进行内存分配,对于内核中频繁创建和销毁的小对象(如 task_struct、inode、dentry),直接使用伙伴系统会产生两个严重问题:
- 内部碎片:一个几十字节的对象占用一整页(4096 字节),利用率极低。
- 分配/释放开销高:每次分配都要走伙伴系统的页框合并与分裂算法,性能无法满足高频路径。
slab 分配器的核心思想是 对象缓存(Object Caching):预先从伙伴系统申请若干连续页框,将其划分为多个相同大小的对象槽位,并维护空闲链表。分配时直接从缓存中取出一个对象,释放时归还到缓存,避免频繁与伙伴系统交互。
关键洞察: 对象缓存将“页框级分配”降级为“对象级分配”,分配时间从微秒级降至纳秒级,同时消除了内部碎片。
2.1.1 对象缓存的工作流程
- 缓存创建:调用
kmem_cache_create(),指定对象大小、对齐方式、构造函数等。 - 页框预取:slab 分配器从伙伴系统申请 2n 个连续页框,构成一个 slab。
- 对象划分:将 slab 的连续内存划分为等长对象槽位,每个槽位对应一个空闲对象。
- 分配:从空闲链表中摘取一个对象,返回给调用者;若当前 slab 已满,则分配新的 slab。
- 释放:对象归还到空闲链表;若整个 slab 全空闲,可释放回伙伴系统。
2.2 slab / slub / slob 三种实现对比
Linux 内核历史上先后出现了三种 slab 分配器实现:slab(原始实现)、slub(当前默认,针对大型系统优化)、slob(针对小型嵌入式系统优化)。三者的核心目标一致,但在数据结构、锁粒度、内存开销和可扩展性上存在显著差异。
| 特性 | slab(原始) | slub(默认) | slob(嵌入式) |
|---|---|---|---|
| 核心数据结构 | 每 CPU 缓存 + 共享 slab 链表 + 三色链表(满/部分/空) | 每 CPU 空闲指针 + 每个 slab 的 freepointer 链表 | 简单空闲链表 + 页框内偏移管理 |
| 元数据开销 | 高(每个 slab 有 kmem_slab 描述符) | 低(slab 描述符嵌入页框或外部) | 极低(几乎无额外描述符) |
| 锁粒度 | 全局 slab 链表锁 + 每 CPU 缓存锁 | 每 CPU 操作无锁(freelist 原子操作) | 全局锁(简单但并发差) |
| 分配/释放复杂度 | O(1) 平均,但链表操作较多 | O(1) 严格,freelist 头插头取 | O(n) 最坏情况(遍历空闲链表) |
| 内存碎片控制 | 好(着色 + 三色回收) | 好(简化着色,依赖伙伴系统) | 一般(简单合并,易产生外部碎片) |
| 适用场景 | 中小型系统,历史兼容 | 大型服务器、桌面、移动设备(默认) | 极低内存设备(< 64MB) |
| 内核配置选项 | CONFIG_SLAB |
CONFIG_SLUB |
CONFIG_SLOB |
选型建议: 对于大多数通用场景(服务器、桌面、Android),保持内核默认的 slub 即可。仅当目标设备内存小于 64MB 时考虑 slob;slab 主要用于需要细粒度着色调试或历史兼容的场合。
2.2.1 三种实现的数据结构对比
2.3 高速缓存(cache)与 kmem_cache 结构
在 slab 分配器中,高速缓存(cache) 是管理同一类对象的逻辑容器。每个 cache 对应一个 struct kmem_cache 实例,包含对象大小、对齐、slab 管理策略、每 CPU 缓存等全部元数据。
2.3.1 kmem_cache 核心字段
struct kmem_cache {
struct kmem_cache_cpu __percpu *cpu_slab; /* 每 CPU 缓存 */
unsigned long size; /* 对象大小(含对齐填充) */
slab_flags_t flags; /* 标志位(如 SLAB_HWCACHE_ALIGN) */
unsigned int object_size; /* 原始对象大小(不含填充) */
unsigned int offset; /* freepointer 在对象内的偏移 */
unsigned int cpu_partial; /* 每 CPU 批量分配阈值 */
struct kmem_cache_node *node[MAX_NUMNODES];/* NUMA 节点链表 */
int align; /* 对齐要求 */
const char *name; /* 缓存名称(如 "task_struct") */
int refcount; /* 引用计数 */
void (*ctor)(void *); /* 构造函数 */
struct list_head list; /* 全局 cache 链表 */
};
关键字段解读:
cpu_slab:每 CPU 的kmem_cache_cpu结构,包含当前正在使用的 slab 指针和空闲对象链表头。这是 slub 实现无锁分配的核心。size与object_size的差值:对齐填充和元数据预留空间,直接影响内存利用率。offset:在 slub 中,空闲对象的freepointer存储在对象内部的offset位置,复用对象本身的空间。cpu_partial:控制每 CPU 缓存中部分空闲 slab 的数量,平衡本地分配与全局回收。
2.3.2 高速缓存的层次结构
2.3.3 高速缓存的创建与销毁 API
| 函数 | 功能 | 关键参数 |
|---|---|---|
kmem_cache_create() |
创建新的高速缓存 | name, size, align, flags, ctor |
kmem_cache_destroy() |
销毁高速缓存(需确保所有对象已释放) | s (kmem_cache 指针) |
kmem_cache_alloc() |
从缓存分配一个对象 | s, gfpflags |
kmem_cache_free() |
将对象归还缓存 | s, x (对象指针) |
kmem_cache_shrink() |
收缩缓存,释放空闲 slab 回伙伴系统 | s |
常见陷阱:
- 使用
kmem_cache_alloc分配的对象必须用kmem_cache_free释放,不能混用kfree,否则会导致 slab 元数据损坏。 - 销毁缓存前必须确保所有对象已释放,否则会造成内存泄漏。
- 构造函数(ctor)在对象首次分配时调用,但 slub 在批量分配时可能跳过构造函数以提升性能,需注意初始化逻辑。
2.3.4 实战示例:创建并使用专用缓存
#include <linux/slab.h>
#include <linux/string.h>
struct my_object {
int id;
char name[32];
unsigned long flags;
};
static struct kmem_cache *my_cachep;
int init_my_cache(void)
{
/* 创建专用缓存:对象大小 40 字节,8 字节对齐,无构造函数 */
my_cachep = kmem_cache_create("my_object_cache",
sizeof(struct my_object),
8, 0, NULL);
if (!my_cachep)
return -ENOMEM;
return 0;
}
void cleanup_my_cache(void)
{
kmem_cache_destroy(my_cachep);
}
struct my_object *alloc_my_object(gfp_t gfp_flags)
{
struct my_object *obj;
obj = kmem_cache_alloc(my_cachep, gfp_flags);
if (obj) {
obj->id = 0;
obj->flags = 0;
memset(obj->name, 0, sizeof(obj->name));
}
return obj;
}
void free_my_object(struct my_object *obj)
{
if (obj)
kmem_cache_free(my_cachep, obj);
}
性能提示: 对于高频分配路径,建议使用
kmem_cache_alloc_bulk() 和 kmem_cache_free_bulk()(slub 特有),一次操作可分配/释放多个对象,分摊锁开销和 slab 遍历成本。网络驱动和块设备驱动中广泛使用此接口。