操作系统内核中最高效的对象缓存与内存分配方案,从 Linux 到 FreeBSD 的核心基石
在操作系统内核中,有大量频繁创建和销毁的小对象:进程描述符(task_struct)、文件描述符、网络 socket、dentry 缓存等。如果每次都直接调用页分配器(Buddy System)获取内存,会带来三个严重问题:
Buddy System 以页(4KB)为最小分配单位。一个 task_struct 可能只有 1.7KB,却要占用整整一页,浪费超过 50% 的空间。内核中这类小对象成千上万,浪费极其惊人。
每次分配都要搜索空闲页框、修改页表、更新标记位;释放时又要做合并、回收。这些操作涉及多个自旋锁和中断控制,开销巨大。
同一类对象(如 task_struct)被反复分配时,每次都要执行构造函数来初始化数据结构。如果对象频繁创建销毁,重复初始化就是纯粹的浪费。
Slab 分配器采用三级层次结构,自顶向下依次为:
每个 Slab(由连续物理页组成)始终处于以下三种状态之一:
根据对象类型找到对应的 kmem_cache。例如分配 task_struct 就找 task_struct_cache。每个 Cache 保存了对象大小、对齐要求、构造/析构函数等元信息。
遍历 partial 链表,从第一个 partial slab 中取出一个空闲对象(通过空闲对象指针数组 O(1) 定位)。取出后更新 slab 的空闲计数。
如果 partial 链没有可用的 slab,则从 empty 链取出一个 slab 并升级为 partial。
所有链表都无可用 slab 时,调用伙伴系统分配新页框,创建新的 slab,初始化其中的对象(执行构造函数),然后分配。
返回指向该对象的指针。由于对象已被构造函数初始化(或从缓存复用),调用者可以直接使用,无需再次初始化。
通过对象的物理地址计算出它所在的 slab(利用页描述符 page 中的 slab 指针,或通过地址对齐计算)。
如果该 Cache 配置了析构函数,先执行析构函数清理对象中的资源(如释放内部指针、复位标志位)。
将对象加入 slab 内部的空闲对象链表(或位图标记为空闲),更新 slab 的 inuse 计数。
如果 slab 变成全部空闲,从 partial 链移到 empty 链。系统在内存紧张时可以回收 empty slab 的页框还给伙伴系统。
下面是一个简化的 Slab 分配器模拟器。你可以亲自体验对象的分配和释放过程。
理解 Slab 的核心在于掌握三个数据结构之间的关系:
// Linux 内核中的核心结构体(简化版) struct kmem_cache { const char *name; // Cache 名称,如 "task_struct" unsigned int object_size; // 单个对象的大小 unsigned int size; // 含对齐填充后的实际大小 unsigned int order; // 从伙伴系统申请的页框阶数 unsigned int num; // 每个 Slab 可容纳的对象数 void (*ctor)(void *); // 对象构造函数 void (*dtor)(void *); // 对象析构函数 struct list_head partial; // Partial Slab 双向链表 struct list_head full; // Full Slab 双向链表 struct list_head empty; // Empty Slab 双向链表 unsigned int gfporder; // 分配标志 }; struct slab { struct list_head list; // 链表节点(挂在 cache 的某个链表上) unsigned int inuse; // 已使用的对象数量 unsigned int free; // 空闲对象数量 void *s_mem; // 指向第一个对象的指针 void **freelist; // 空闲对象指针数组 };
为了提高 CPU 缓存命中率,Slab 分配器引入了着色(coloring)机制:
| 特性 | Buddy System | Slab 分配器 | kmalloc |
|---|---|---|---|
| 最小分配单位 | 1 页 (4KB) | 单个对象 | 字节级(按 2^n 分类) |
| 适用对象大小 | 页级(大块) | 固定大小的内核对象 | 任意小尺寸 |
| 内部碎片 | 严重(小对象) | 极小 | 较小 |
| 分配速度 | 较慢 | 极快(O(1)) | 快 |
| 对象初始化 | 每次重新初始化 | 构造函数 + 缓存复用 | 不清零(可设 GFP_ZERO) |
| 硬件缓存友好 | 一般 | 优秀(着色机制) | 一般 |
Linux 内核中实际上有三种 Slab 分配器的实现:
// 1. 创建一个新的 Slab Cache struct kmem_cache *task_struct_cachep; task_struct_cachep = kmem_cache_create( "task_struct", // Cache 名称 sizeof(struct task_struct), // 对象大小 ARCH_MIN_TASKALIGN, // 对齐要求 SLAB_PANIC | SLAB_NOTRACK, // 标志位 NULL // 构造函数(可选) ); // 2. 从 Cache 中分配一个对象 struct task_struct *task; task = kmem_cache_alloc(task_struct_cachep, GFP_KERNEL); // 3. 使用完毕后释放 kmem_cache_free(task_struct_cachep, task); // 4. 通用分配(kmalloc 底层使用 Slab) void *buf = kmalloc(256, GFP_KERNEL); kfree(buf);
// 在 Linux 系统中,可以通过以下方式查看 // 方式 1:查看 /proc/slabinfo $ cat /proc/slabinfo slabinfo - version: 2.1 # name <active_objs> <num_objs> <objsize> ... task_struct 512 640 1792 ... 1 8 : tunables ... // 方式 2:使用 slabtop 命令(实时监控) $ slabtop // 方式 3:查看 /sys/kernel/slab/(SLUB 特有) $ ls /sys/kernel/slab/task_struct/ aliases align object_size order partial ...
恭喜你学完了 Slab 分配机制的核心知识!
你可以回到上方操作交互式演示加深理解,或在内核中用 slabtop 实际观察。