2. 物理内存管理:伙伴系统算法、slab分配器、内存碎片化问题
物理内存是计算机系统最核心的硬件资源之一。操作系统内核必须高效、公平地管理这片有限的空间,同时满足不同大小、不同生命周期、不同对齐要求的内存分配请求。本章深入剖析Linux内核中两大物理内存管理支柱——伙伴系统(Buddy System)与Slab分配器,并重点讨论工程实践中棘手的内存碎片化问题及其缓解策略。
工程视角: 在用户态通过
mmap 映射匿名页或文件页时,底层最终都会落到物理页的分配上。理解伙伴系统和Slab分配器,有助于解释为什么频繁的 mmap/munmap 可能导致系统性能抖动,以及如何通过调整内核参数来优化。
2.1 伙伴系统算法
伙伴系统是Linux内核管理物理页帧(Page Frame)的基础算法。它专门解决“分配连续物理页”的问题,并以2的幂次阶(order)为单位进行管理,以平衡分配效率与外部碎片。
2.1.1 核心数据结构
每个内存节点(NUMA Node)的 struct zone 中,包含一个 free_area 数组,用于维护不同阶的空闲链表:
// 简化自 include/linux/mmzone.h
struct free_area {
struct list_head free_list[MIGRATE_TYPES]; // 按迁移类型分组
unsigned long nr_free; // 该阶空闲页块数量
};
struct zone {
// ...
struct free_area free_area[MAX_ORDER]; // MAX_ORDER 通常为 11 (0~10阶)
unsigned long _watermark[NR_WMARK];
// ...
};
- 阶(Order):0阶对应1页(4KB),1阶对应2页(8KB),n阶对应 2^n 页。
- MAX_ORDER:通常为11,即最大可分配 2^10 = 1024 页(4MB)的连续物理内存。
- 迁移类型(MIGRATE_TYPES):将空闲页按可移动性分类(不可移动、可回收、可移动、预留等),以减少碎片。
2.1.2 分配与释放流程
| 操作 | 步骤 | 说明 |
|---|---|---|
分配 alloc_pages(order) |
1. 从请求阶的空闲链表查找 2. 若失败,向高阶逐级查找 3. 找到后,将高阶块分裂为两个伙伴 4. 将低阶块返回,高阶伙伴插入对应链表 |
最坏情况需遍历到 MAX_ORDER-1 |
释放 __free_pages(page, order) |
1. 检查释放块的伙伴是否空闲 2. 若伙伴空闲且同阶,合并为高阶块 3. 递归向上合并,直到无法合并为止 4. 将最终块插入对应空闲链表 |
合并操作可显著缓解外部碎片 |
关键指标: 伙伴系统的分配时间复杂度为 O(MAX_ORDER),释放时可能触发递归合并,但整体仍为常数级。然而,频繁的分配/释放会导致高阶空闲块被不断分裂,产生大量低阶碎片。
2.2 Slab分配器
伙伴系统以页为最小单位,但内核中大量对象(如 task_struct、inode、dentry)仅需几十到几百字节。直接使用伙伴系统分配会产生巨大的内部碎片。Slab分配器应运而生,它在伙伴系统提供的连续页之上,实现针对特定大小对象的高速缓存(Cache)。
2.2.1 架构层次
- Cache(高速缓存):每个Cache管理一种固定大小的对象(如 32B、64B、128B 或特定结构体)。
- Slab( slabs):一个Slab由一个或多个连续物理页组成,被划分为多个对象槽位。
- 对象(Object):Cache中分配的最小单元,每个对象可以处于“已分配”或“空闲”状态。
| 组件 | 作用 | 典型大小 |
|---|---|---|
struct kmem_cache |
描述一个对象缓存池,包含 slab 链表、构造函数、对齐属性 | 数百字节 |
struct slab |
描述一个具体的 slab 页块,管理对象位图 | 几十字节 |
| 对象(Object) | 实际分配的内存单元,可能包含 freelist 指针 |
8B ~ 8KB |
2.2.2 分配与释放路径
// 分配对象
void *kmem_cache_alloc(struct kmem_cache *cachep, gfp_t flags) {
struct slab *slabp;
unsigned long objp;
// 1. 从 per-CPU 的本地缓存(cpu_cache)快速分配
// 2. 若本地缓存为空,从 slab 的 partial 链表获取
// 3. 若 partial 为空,从 buddy 分配新 slab 页
// 4. 返回对象地址
return objp;
}
// 释放对象
void kmem_cache_free(struct kmem_cache *cachep, void *objp) {
// 1. 将对象放回 per-CPU 缓存
// 2. 若 per-CPU 缓存已满,批量回收到 slab 的 partial 链表
// 3. 若 slab 所有对象空闲,释放回伙伴系统
}
性能陷阱: Slab分配器的 per-CPU 缓存(
struct array_cache)是性能关键。如果频繁跨 CPU 释放对象(如一个 CPU 分配,另一个 CPU 释放),会导致缓存行 bouncing 和锁竞争。在 NUMA 系统中,应尽量使用 kmem_cache_alloc_node() 在本地节点分配。
2.3 内存碎片化问题
内存碎片化是长期运行系统中导致高阶分配失败(如 order-4 以上)的主要元凶。即使总空闲内存充足,也可能无法分配连续的物理页块。
2.3.1 碎片类型
| 类型 | 定义 | 产生原因 | 影响 |
|---|---|---|---|
| 内部碎片 | 分配单元内部未使用的空间 | 伙伴系统按 2^n 页分配;Slab 对象对齐 | 内存利用率下降,但通常可控 |
| 外部碎片 | 空闲页块不连续,无法满足高阶分配 | 长期分配/释放导致空闲页散布 | 高阶分配失败,触发内存压缩或 OOM |
2.3.2 碎片化缓解策略
- 迁移类型分组(MIGRATE_TYPES):将不可移动页(如内核分配)与可移动页(如用户态匿名页)隔离,避免不可移动页阻塞连续空间。
- 内存压缩(Memory Compaction):通过页面迁移(page migration)将可移动页集中到区域一端,腾出连续大块。
- 虚拟可移动区域(ZONE_MOVABLE):专门用于可移动页的分配,便于在线内存热插拔和碎片整理。
- THP(透明大页):尽量分配 2MB 大页,减少页表项数量,但需注意大页分配失败时的回退策略。
工程实践建议:
- 监控
/proc/buddyinfo观察各阶空闲页数量,若高阶(order≥4)长期为0,说明碎片严重。 - 在长时间运行的服务中,定期触发
echo 1 > /proc/sys/vm/compact_memory进行内存压缩。 - 对于大页需求,使用
mmap时指定MAP_HUGETLB并配合hugetlbfs,绕过伙伴系统直接使用预留大页池。
2.4 本章小结
- 伙伴系统以 2^n 页为单位管理物理内存,通过分裂与合并平衡分配灵活性与碎片控制,是物理页分配的基础。
- Slab分配器在伙伴系统之上构建对象缓存,通过 per-CPU 缓存和 slab 链表实现高效的小对象分配,避免内部碎片。
- 内存碎片化是长期运行系统的核心挑战,通过迁移类型分组、内存压缩、ZONE_MOVABLE 等机制缓解。
- 在
mmap开发中,理解底层物理内存管理有助于解释大页映射、内存压力、分配延迟等行为,是性能调优的前提。