2. 伙伴系统与slab分配器
伙伴系统(Buddy System)与 Slab 分配器是 Linux 内核内存管理的两大基石。伙伴系统负责物理页框的分配与回收,以 2 的幂次阶(order)为单位管理连续物理内存;Slab 分配器(包括其改进版 SLUB)则构建在伙伴系统之上,专门用于内核对象(如 task_struct、inode)的缓存与复用,避免频繁的页分配开销。本章深入剖析两者的核心原理、数据结构与关键路径。
2.1 伙伴系统原理
伙伴系统将物理内存划分为多个 free_area,每个 free_area 管理一种特定阶(order)的空闲页块。阶(order)定义为 2^order 个连续物理页框。系统启动时,所有可用内存被合并到最高阶的 free_area 中;分配时,若当前阶无空闲块,则向高阶分裂;释放时,检查伙伴块是否空闲,若空闲则合并回高阶。
每个 free_area 包含一个链表(free_list)和位图(map),用于快速查找和合并伙伴。
2.1.1 伙伴系统分配与释放流程
| 操作 | 步骤 | 说明 |
|---|---|---|
| 分配 | 1. 从请求阶 order 的 free_area 开始查找 | 若链表非空,直接分配 |
| 2. 若当前阶为空,向 order+1 阶查找 | 递归向上查找 | |
| 3. 找到高阶块后,分裂为两个伙伴块 | 一个返回给调用者,另一个插入低阶 free_area | |
| 释放 | 1. 计算伙伴块的物理地址 | 通过页框号异或 2^order 得到 |
| 2. 检查伙伴块是否在 free_list 中 | 通过位图或 PG_buddy 标志判断 | |
| 3. 若伙伴空闲,合并并递归向上检查 | 合并后插入 order+1 的 free_area |
2.1.2 伙伴系统的碎片问题
伙伴系统虽然高效,但存在 外部碎片 问题:频繁分配和释放不同阶的块会导致内存碎片化,即使总空闲内存足够,也可能无法满足高阶分配请求。内核通过 内存规整(compaction) 和 虚拟可移动区域(ZONE_MOVABLE) 缓解此问题。
2.2 页面分配与回收关键路径
页面分配的核心函数是 alloc_pages(gfp_mask, order),它通过 快速路径 和 慢速路径 两级机制保证分配成功率。
2.2.1 快速路径与慢速路径
| 路径 | 触发条件 | 操作 | 可能阻塞? |
|---|---|---|---|
| 快速路径 | 当前 zone 的 free_area 有足够空闲块 | 直接从伙伴系统分配,不唤醒 kswapd | 否 |
| 慢速路径 | 快速路径失败,或 gfp_mask 含 __GFP_KSWAPD_RECLAIM | 唤醒 kswapd 异步回收;若仍失败,则同步直接回收(direct reclaim) | 是(可能等待 I/O) |
| OOM 触发 | 慢速路径回收后仍无法满足分配 | 调用 out_of_memory() 选择 victim 进程并杀死 | 是(进程被杀死) |
// 快速路径核心逻辑(简化)
static inline struct page *rmqueue(struct zone *zone, unsigned int order) {
struct free_area *area = &zone->free_area[order];
struct page *page = list_first_entry_or_null(&area->free_list, struct page, lru);
if (page) {
list_del(&page->lru);
set_page_private(page, 0);
return page;
}
// 若当前阶为空,尝试从高阶分裂
return __rmqueue_smallest(zone, order);
}
2.2.2 页面回收机制
页面回收由 kswapd 内核线程和 直接回收(direct reclaim) 共同完成。kswapd 在后台异步扫描 LRU 链表,回收匿名页和文件页;直接回收则在分配路径中同步执行。
- LRU 链表:分为活跃匿名页、不活跃匿名页、活跃文件页、不活跃文件页。
- 回收策略:优先回收不活跃页;若内存压力大,则扫描活跃页并降级。
- Swap:匿名页回收时写入交换分区;文件页回收时若为脏页则写回磁盘。
2.3 Slab/SLUB 分配器机制
Slab 分配器(以及其改进版 SLUB)用于管理内核中频繁创建和销毁的小对象(如 struct file、struct inode)。它通过预分配对象缓存,避免每次分配都调用伙伴系统,从而降低开销并减少碎片。
2.3.1 Slab 分配器核心概念
| 概念 | 说明 |
|---|---|
| kmem_cache | 每个对象类型对应一个缓存,包含多个 slab |
| slab | 一个或多个连续物理页框,划分为多个对象槽位 |
| 对象(object) | 内核数据结构实例,如 task_struct |
| 着色(coloring) | 通过偏移调整对象起始地址,优化 CPU 缓存行对齐 |
2.3.2 SLUB 分配器改进
SLUB(Unqueued Slab Allocator)是 Slab 的现代替代,自 Linux 2.6.22 起成为默认分配器。主要改进:
- 去除 per-CPU 队列:Slab 使用 per-CPU 对象池,SLUB 改用 per-CPU 空闲指针,减少锁竞争。
- 简化 slab 管理:SLUB 将 slab 元数据存储在页框的
struct page中,不再需要独立的 slab 描述符。 - 批量操作:支持批量分配和释放,提升吞吐量。
// SLUB 分配核心:从 per-CPU 空闲指针获取对象
static __always_inline void *slab_alloc_node(struct kmem_cache *s, gfp_t gfpflags, int node) {
struct kmem_cache_cpu *c = per_cpu_ptr(s->cpu_slab, raw_smp_processor_id());
void *object = c->freelist;
if (unlikely(!object)) {
// 当前 CPU 空闲列表为空,从 slab 中获取新批
object = __slab_alloc(s, gfpflags, node, c);
} else {
// 直接分配
c->freelist = get_freepointer(s, object);
}
return object;
}
2.3.3 Slab 与 SLUB 对比
| 特性 | Slab | SLUB |
|---|---|---|
| 元数据存储 | 独立 slab 描述符 | 嵌入 struct page |
| per-CPU 队列 | 有,需加锁 | 无,使用 freelist 指针 |
| 批量分配 | 不支持 | 支持 |
| 内存开销 | 较高(描述符 + 着色) | 较低 |
| 调试能力 | 较弱 | 支持红区(redzone)和毒化(poison) |
2.4 故障排查要点
cat /proc/buddyinfo 观察各 zone 的 free_area 分布。若高阶(order>3)块长期为 0,说明碎片严重,可触发内存规整:echo 1 > /proc/sys/vm/compact_memory。
slabtop 或 cat /proc/slabinfo 观察缓存对象数量是否持续增长。若某个缓存(如 kmalloc-128)的 active_objs 不断上升且不回落,可能存在内存泄漏。进一步使用 kmemleak 工具扫描未释放对象。
伙伴系统与 Slab 分配器共同构成了内核内存分配的两级缓存体系:伙伴系统管理物理页框的宏观分配,Slab/SLUB 则在内核对象层面实现高效复用。理解这两者的协同工作方式,是诊断 OOM 和内存泄漏问题的前提。