K01 —— 物理页分配后的对象管理:SLAB/SLUB 分配器
金句:把固定大小的对象放在固定大小的页上,是内存分配器设计的核心。
Buddy System 之后为什么还需要 SLAB
Buddy System 管理的是 4KB、8KB、16KB… 4MB 的物理页框。但内核需要分配更小的对象——Task Struct(几十字节)、inode(几百字节)、dentry(几百字节)。
单一对象分配的问题(直接用 buddy):
假设需要分配 100 字节对象:
- buddy 最小单位 4KB
- 分配 4KB,浪费 3900 字节
- 1000 个对象 → 浪费 3.9MB
解决方案:SLAB 分配器
- 在 buddy 分配的页上,建立对象池(object cache)
- 每个对象大小固定(2^n 字节)
- 每个 SLAB 包含多个对象(填满整个 4KB 页)
- 分配和释放都是 O(1)
4KB / 100 bytes = 40 对象/页
1000 个对象 → 只需要 25 页,而不是 1000 页SLAB 的数据结构
SLAB 的结构:
struct slab {
struct list_head list; // 链表节点(连接 free/partial/full)
unsigned long colouroff; // 颜色偏移(缓存行对齐)
void *s_mem; // 第一个对象的地址
unsigned int inuse; // 已分配对象数
kmem_bufctl_t free; // 空闲对象链表头
};
每个 SLAB 管理一个或多个 4KB 页:
- 包含若干相同大小的对象
- 空闲对象通过 free 链表串联
- 已满的 SLAB 不参与分配
SLAB 状态:
- free:所有对象空闲(不在 CPU cache 中分配)
- partial:部分对象已分配
- full:所有对象已分配kmem_cache 结构(每个对象类型的缓存):
struct kmem_cache {
const char *name; // 缓存名称(如 "task_struct")
size_t object_size; // 对象大小(如 576 bytes)
size_t size; // 对齐后大小(如 640 bytes)
size_t align; // 对齐要求(如 64 bytes)
void (*ctor)(void *); // 构造函数(可选)
struct kmem_cache_cpu __percpu *cpu_slab; // per-CPU 缓存
struct list_head *node; // NUMA 节点链表(每个节点有多个 SLAB)
struct kmem_cache_order_objects size;
struct kmem_cache_order_objects order;
};
关键设计:
- 每个 CPU 有自己的 SLAB(无锁分配)
- 每个 NUMA 节点有自己的 SLAB 链表
- 分配优先从本地 CPU 的 SLAB 取SLUB 分配器(现代 Linux 默认)
SLUB = SLAB with Minimal Optimization
- 简化了 SLAB 的实现
- 移除了 per-SLAB 的 free 链表
- 使用 page->freelist 直接指向空闲对象
SLUB 的优势:
1. 更少内存开销(不需要额外元数据)
2. 更好的缓存友好性
3. 更好的 NUMA 局部性
SLUB page 结构(嵌入在 struct page 中):
struct page {
union {
struct {
unsigned long flags; // 页面状态
atomic_t _refcount; // 引用计数
unsigned long:25; // 空闲对象数
unsigned long inuse:16;
void *freelist; // 第一个空闲对象
};
};
// ...
};
freelist 指向每个对象中的下一个空闲对象:
freelist[0] → 对象 A
freelist[A] → 对象 B
freelist[B] → 对象 C
freelist[C] = NULL(结束)SLUB 分配流程:
__slab_alloc(slab, flags):
// 1. 从 current CPU 的 slab 取
c = get_cpu_ptr(cachep->cpu_slab);
page = c->page;
if (!page) // CPU slab 为空
goto new_slab;
// 2. 从 page->freelist 取对象
object = page->freelist;
page->freelist = object[next]; // 移动到下一个空闲对象
page->inuse++;
return object;
new_slab:
// 3. 从 buddy 分配新页面
page = alloc_pages(flags, order);
// 4. 初始化 freelist
page->freelist = objects; // 构建空闲链表
page->inuse = 0;
// 5. 更新 cpu_slab
c->page = page;
return object;per-CPU 缓存和锁优化
SLUB 最大的优化:per-CPU 本地缓存
每个 CPU 维护自己的"当前 SLAB":
- 分配时不需要获取锁(无竞争)
- O(1) 时间
- 避免了跨 CPU 的锁竞争
per-CPU slab 结构:
struct kmem_cache_cpu {
void **freelist; // 指向本地 SLAB 的空闲对象
unsigned long tid; // 事务 ID(用于调试)
struct page *page; // 当前 SLAB(大部分对象空闲)
struct page *partial; // 部分空闲的 SLAB(本地)
};
CPU0 的分配:
if (cpu_cache.frelist != NULL) {
object = cpu_cache.freelist;
cpu_cache.freelist = *(void**)object;
return object; // 无锁,O(1)
}
跨 NUMA 节点的优化:
- NUMA node 的 SLAB 优先分配给本地 CPU
- 减少跨节点内存访问kmalloc:通用对象分配
kmalloc 是 SLUB 之上的通用分配接口:
kmalloc(size, flags):
- size:分配大小(1B ~ 8KB)
- 返回:虚拟地址(对象起始地址)
kmalloc 的内部:
- 把 size 映射到对应的 kmem_cache
- kmalloc-32, kmalloc-64, kmalloc-128, ... kmalloc-8192
- 每个 size 类别有一个独立的 kmem_cache
大小类别(Power of 2):
32 / 64 / 128 / 256 / 512 / 1024 / 2048 / 4096 / 8192 bytes
实际分配示例:
kmalloc(100, GFP_KERNEL)
→ 找到最接近的 size = 128
→ 从 kmalloc-128 cache 分配
→ 实际分配 128 bytes
slabinfo 查看缓存:
$ slabtop
OBJS ACTIVE USE SIZE OBJECTS SLABS
12345 12000 97% 64 24567 1234 kmalloc-64
5678 5600 98% 128 8123 456 kmalloc-128总结
- Buddy vs SLAB:Buddy 分配 4KB+ 的块,SLAB 把大页切成小对象池
- SLAB 结构:slab = 1~多个页 + 对象数组 + free 链表 + 状态(free/partial/full)
- SLUB(现代):page->freelist 直接指向空闲对象(无额外元数据),page->inuse 计数
- per-CPU 缓存:每个 CPU 有自己的 current slab,分配无锁 O(1)
- kmalloc:SLUB 之上的通用接口,size → cache(power of 2),GFP_KERNEL 标志
下篇预告(K02):虚拟地址到物理地址的翻译具体怎么做的?多级页表、页表缓存(TLB)、以及 page fault 的完整处理流程。
关注公众号「AI不着急」,回复”资料”获取内存学习路线图。
评论