Linux内核内存管理月度总结:伙伴系统到slab的核心机制回顾
Linux内核内存管理月度总结:伙伴系统到slab的核心机制回顾
一、为什么重读内核内存管理
7月份我花了三周时间重新梳理Linux内核内存管理模块。
起因很有趣:团队在做性能优化时遇到了一个诡异的内存碎片问题。
排查到最后发现根因是伙伴系统(buddy system)的order分配行为。
那一刻我意识到,很多"玄学问题"的答案都写在教科书里。
所以我在7月系统地回顾了从伙伴系统到slab分配器的完整链路。
这篇文章是我一个月的学习笔记和验证实验的整理。
核心目标不是罗列知识,而是建立"物理内存→内核分配器→用户态"的完整心智模型。
二、伙伴系统:页面级的分配艺术
伙伴系统是内核最底层的物理页分配器。
它的核心思想是把物理内存按2的幂次(order)分组管理。
每一组叫做一个free_area,从order 0(4KB)到order 10(4MB)。
核心数据结构:
// 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];
// ...
spinlock_t lock; /* 保护并发访问 */
};
伙伴系统的分配逻辑可以简化为:
- 计算请求大小对应的order。
- 在对应free_area[order]中查找空闲块。
- 如果没有,向order+1申请,然后分裂。
- 分配后将另一半放入低阶free_area。
释放时执行反向操作(合并):
// mm/page_alloc.c (简化)
static inline void __free_one_page(struct page *page,
unsigned long pfn, struct zone *zone,
unsigned int order)
{
// 检查伙伴是否也空闲
while (order < MAX_ORDER - 1) {
buddy_pfn = __find_buddy_pfn(pfn, order);
buddy = page + (buddy_pfn - pfn);
if (!page_is_buddy(buddy, order))
break;
// 从当前free_list删除伙伴
del_page_from_free_list(buddy, zone, order);
// 合并为order+1的块
combined_pfn = pfn & buddy_pfn;
page = page + (combined_pfn - pfn);
pfn = combined_pfn;
order++;
}
add_to_free_list(page, zone, order);
}
碎片化类型与对策:
伙伴系统面临三种碎片:
| 碎片类型 | 说明 | 内核对策 |
|---|---|---|
| 外部碎片 | 空闲页不连续 | 内存规整(compaction) |
| 内部碎片 | 分配大于需求 | slab对象级细分 |
| 迁移碎片 | 不可移动页阻断 | migrate_type分级 |
迁移类型(MIGRATE_TYPES)是反碎片的核心机制:
enum migratetype {
MIGRATE_UNMOVABLE, // 内核核心数据结构
MIGRATE_MOVABLE, // 用户态页面(可换出)
MIGRATE_RECLAIMABLE, // slab等可回收内存
MIGRATE_PCPTYPES, // per-CPU页框
MIGRATE_HIGHATOMIC, // 高阶原子分配
MIGRATE_CMA, // 连续内存分配器
MIGRATE_ISOLATE, // 隔离中的页面
};
这个设计保证了:即使总空闲内存充足,
也不会因为可移动页散布而阻塞不可移动内存的分配。
三、Slab分配器:小而美的对象缓存
伙伴系统的最小分配单位是4KB(一页)。
但内核中大量的分配请求远小于一页。
比如:inode结构体约600字节,dentry约200字节。
如果每次都用伙伴系统分配一页,内部碎片会达到80%以上。
Slab分配器就是为了解决这个问题而生的。
它的核心思想是"对象缓存"(object caching):
// 创建专用缓存
struct kmem_cache *my_cache = kmem_cache_create(
"my_struct",
sizeof(struct my_struct), // 对象大小
0, // 对齐
SLAB_HWCACHE_ALIGN, // 硬件缓存行对齐
NULL // 构造函数
);
// 分配和释放
struct my_struct *obj = kmem_cache_alloc(my_cache, GFP_KERNEL);
kmem_cache_free(my_cache, obj);
Slab分配器内部有三个层次:
第一层:kmem_cache — 缓存描述符
每个kmem_cache对应一种固定大小的对象类型。
内核启动时会创建两类缓存:
- 专用缓存:为特定类型创建(如inode_cache、dentry_cache)。
- 通用缓存:kmalloc使用的预定义大小缓存(8, 16, 32, ..., 8192字节)。
第二层:slab — 页面的管理者
每个slab管理一个或多个连续物理页。
slab有三种状态:
- full:所有对象都在使用中。
- partial:部分对象在使用中。
- empty:所有对象都是空闲的。
struct slab {
struct list_head slab_list; // 链接到缓存的partial/full/empty链表
unsigned long freelist[N]; // 空闲对象位图
void *s_mem; // 第一个对象的地址
unsigned int inuse; // 使用中的对象数
unsigned int free; // 空闲对象数
};
第三层:per-CPU缓存 — 无锁快速路径
SLUB(当前默认实现)在每个CPU上维护一个本地对象池:
struct kmem_cache_cpu {
void **freelist; // 空闲对象链表
struct page *page; // 当前活跃的slab页
int node; // NUMA节点
unsigned int offset;// 对象在页内的偏移
};
分配路径非常简洁:
// mm/slub.c (简化)
static void *slab_alloc_node(struct kmem_cache *s, gfp_t gfpflags, int node)
{
struct kmem_cache_cpu *c = raw_cpu_ptr(s->cpu_slab);
// 快速路径:从per-CPU freelist取
void *object = c->freelist;
if (likely(object)) {
c->freelist = get_freepointer(s, object);
return object;
}
// 慢速路径:需要从partial/新页面获取
object = __slab_alloc(s, gfpflags, node, c);
return object;
}
四、实践中的典型问题与诊断
问题一:内存碎片导致高阶分配失败
症状:系统有大量空闲内存,但order>=3的分配持续失败。
诊断工具:
# 查看各order的空闲块统计
cat /proc/buddyinfo
# 输出示例:
# Node 0, zone Normal 1024 512 256 128 64 32 16 8 4 2 1
# order0 ... order10
如果高阶区(order3+)的数字很小:
- 触发内存规整:
echo 1 > /proc/sys/vm/compact_memory - 或增加vm.min_free_kbytes:
sysctl -w vm.min_free_kbytes=65536
问题二:Slab缓存膨胀
# 查看slab缓存统计
cat /proc/slabinfo
# 重点关注"active_objs / num_objs"比值
# 比值 << 1 表示大量空闲对象浪费内存
清理slab缓存:
echo 2 > /proc/sys/vm/drop_caches # 回收dentry和inode
五、总结
核心技术提炼:
- 伙伴系统的本质:以2的幂次组织页面,通过分裂/合并实现高效物理页分配。
关键参数:MAX_ORDER决定最大连续分配大小。 - MIGRATE_TYPES的反碎片策略:将页面按可迁移性分类,避免不可移动页阻断大块连续分配。
实战中通过/proc/pagetypeinfo检查迁移类型分布。 - Slab的三层架构:kmem_cache(类型) → slab(页面) → per-CPU freelist(无锁)。
SLUB是当前默认实现,用freelist指针链取代位图。 - 分配路径的两阶段:快速路径走per-CPU freelist(无锁),慢速路径走partial slab→新页分配。
性能瓶颈通常在伙伴系统层的zone->lock争用。 - 诊断工具链:/proc/buddyinfo(碎片)、/proc/slabinfo(缓存膨胀)、/proc/pagetypeinfo(迁移类型分布)。
三者结合可以定位90%的内存分配问题。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐


所有评论(0)