Linux内核内存管理月度总结:伙伴系统到slab的核心机制回顾
Linux内核内存管理月度总结伙伴系统到slab的核心机制回顾一、为什么重读内核内存管理7月份我花了三周时间重新梳理Linux内核内存管理模块。起因很有趣团队在做性能优化时遇到了一个诡异的内存碎片问题。排查到最后发现根因是伙伴系统buddy system的order分配行为。那一刻我意识到很多玄学问题的答案都写在教科书里。所以我在7月系统地回顾了从伙伴系统到slab分配器的完整链路。这篇文章是我一个月的学习笔记和验证实验的整理。核心目标不是罗列知识而是建立物理内存→内核分配器→用户态的完整心智模型。二、伙伴系统页面级的分配艺术伙伴系统是内核最底层的物理页分配器。它的核心思想是把物理内存按2的幂次order分组管理。每一组叫做一个free_area从order 04KB到order 104MB。核心数据结构// 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]中查找空闲块。如果没有向order1申请然后分裂。分配后将另一半放入低阶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); // 合并为order1的块 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; }四、实践中的典型问题与诊断问题一内存碎片导致高阶分配失败症状系统有大量空闲内存但order3的分配持续失败。诊断工具# 查看各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_kbytessysctl -w vm.min_free_kbytes65536问题二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%的内存分配问题。