Linux 内核内存管理从伙伴系统到 Slab 分配器的分层设计一、内存管理的工程挑战碎片化与分配效率的矛盾Linux 内核需要管理从几 KB 到几 TB 的内存分配请求这些请求的粒度差异巨大进程的页表需要整页4KB分配而文件系统的 inode 只需几十字节。如果所有分配都按页对齐小对象的内存浪费高达 99%。如果所有分配都按字节对齐外部碎片化会导致大块连续内存无法分配。Linux 的解决方案是分层设计伙伴系统Buddy System管理物理页帧解决外部碎片化Slab 分配器在伙伴系统之上管理小对象解决内部碎片化。两层协作各司其职。二、内存管理的分层架构flowchart TB APP[应用层 kmalloc/vmalloc] -- SLAB[Slab 分配器 小对象] APP -- BUDDY[伙伴系统 页帧分配] SLAB -- BUDDY BUDDY -- ZONE[内存区域 ZONE_DMA/NORMAL/HIGHMEM] ZONE -- PHYS[物理内存] subgraph Slab 层 SLAB CACHE[Slab Cache 对象池] end subgraph 伙伴系统层 BUDDY FREE_AREA[空闲区域链表 2^n 页] end三、伙伴系统与 Slab 的核心实现/* 伙伴系统核心逻辑 */ /* 空闲区域每个 order 维护一个链表 */ struct free_area { struct list_head free_list; /* 空闲页块链表 */ unsigned long nr_free; /* 空闲页块数量 */ }; /* 伙伴系统分配从 order 链表中取出 2^order 个连续页 */ static struct page *alloc_pages(unsigned int order) { struct free_area *area; unsigned int current_order; /* 从请求的 order 开始向上查找空闲块 */ for (current_order order; current_order MAX_ORDER; current_order) { area zone-free_area[current_order]; if (!list_empty(area-free_list)) goto found; } return NULL; /* 没有足够大的连续页块 */ found: /* 从链表中取出一个空闲块 */ struct page *page list_entry(area-free_list.next, struct page, lru); list_del(page-lru); area-nr_free--; /* 如果取出的块比请求的大逐级分裂 */ while (current_order order) { current_order--; area zone-free_area[current_order]; /* 将后半部分挂到低一级的链表上 */ struct page *buddy page (1 current_order); list_add(buddy-lru, area-free_list); area-nr_free; } return page; } /* 伙伴系统释放合并相邻的空闲块 */ static void free_pages(struct page *page, unsigned int order) { unsigned long page_idx page_to_pfn(page); /* 尝试与伙伴合并直到无法合并或达到最大 order */ while (order MAX_ORDER - 1) { unsigned long buddy_idx page_idx ^ (1 order); struct page *buddy pfn_to_page(buddy_idx); /* 检查伙伴是否空闲且大小相同 */ if (!page_is_buddy(buddy, order)) break; /* 从链表中移除伙伴合并 */ list_del(buddy-lru); zone-free_area[order].nr_free--; /* 合并后页索引取较小值 */ page_idx min(page_idx, buddy_idx); order; } /* 将合并后的块挂到对应 order 的链表 */ list_add(pfn_to_page(page_idx)-lru, zone-free_area[order].free_list); zone-free_area[order].nr_free; } /* Slab 分配器核心逻辑 */ /* Slab Cache管理同一类型的小对象 */ struct kmem_cache { const char *name; /* Cache 名称 */ unsigned int object_size; /* 对象大小 */ unsigned int objs_per_slab; /* 每个 Slab 中的对象数 */ struct list_head slabs_full; /* 已满 Slab 链表 */ struct list_head slabs_partial; /* 部分 Slab 链表 */ struct list_head slabs_free; /* 空 Slab 链表 */ }; /* Slab 分配从 partial 链表取对象 */ static void *slab_alloc(struct kmem_cache *cachep) { struct slab *slabp; /* 优先从 partial 链表分配 */ if (!list_empty(cachep-slabs_partial)) { slabp list_entry(cachep-slabs_partial.next, struct slab, list); } else if (!list_empty(cachep-slabs_free)) { /* partial 为空从 free 链表取 */ slabp list_entry(cachep-slabs_free.next, struct slab, list); list_move(slabp-list, cachep-slabs_partial); } else { /* 需要新建 Slab向伙伴系统申请一页 */ slabp new_slab(cachep); if (!slabp) return NULL; list_add(slabp-list, cachep-slabs_partial); } /* 从 Slab 中取出一个空闲对象 */ void *objp slabp-freelist; slabp-freelist *(void **)objp; /* freelist 是隐式链表 */ slabp-inuse; /* Slab 满了移到 full 链表 */ if (slabp-inuse cachep-objs_per_slab) list_move(slabp-list, cachep-slabs_full); return objp; } /* Slab 释放将对象放回 freelist */ static void slab_free(struct kmem_cache *cachep, void *objp) { struct slab *slabp virt_to_slab(objp); /* 将对象插入 freelist 头部 */ *(void **)objp slabp-freelist; slabp-freelist objp; slabp-inuse--; /* 根据使用率调整 Slab 所在链表 */ if (slabp-inuse 0) { list_move(slabp-list, cachep-slabs_free); } else if (slabp-inuse cachep-objs_per_slab - 1) { list_move(slabp-list, cachep-slabs_partial); } }四、内存管理的 Trade-offs 分析伙伴系统的内部碎片请求 3 页时分配 4 页2^2浪费 25%。这是页对齐的代价。缓解方案是 Slab 分配器在小对象层面复用浪费的空间。Slab 的对象对齐开销每个 Slab 需要维护 freelist 和元数据这些开销对小对象如 16 字节的 dentry占比显著。Linux 使用隐式 freelist在空闲对象内部存储 next 指针减少元数据开销。NUMA 架构的复杂性多 CPU 系统中每个 CPU 有本地内存节点跨节点访问延迟高。伙伴系统和 Slab 都需要按 NUMA 节点分区增加了管理复杂度。内存压缩的开销当外部碎片化严重时内核需要做内存压缩memory compaction——移动已分配的页以合并空闲块。压缩本身消耗 CPU需要在碎片率和压缩频率之间权衡。五、总结Linux 内存管理的分层设计通过伙伴系统解决外部碎片化、Slab 分配器解决内部碎片化。伙伴系统按 2 的幂次管理页帧分裂和合并操作保证 O(log n) 的分配效率。Slab 分配器在伙伴系统之上管理小对象通过对象池复用减少分配开销。落地时需要关注内部碎片率、Slab 元数据开销、NUMA 分区和内存压缩策略。理解这两层机制是排查 Linux 内存问题的基础。
Linux 内核内存管理:从伙伴系统到 Slab 分配器的分层设计
Linux 内核内存管理从伙伴系统到 Slab 分配器的分层设计一、内存管理的工程挑战碎片化与分配效率的矛盾Linux 内核需要管理从几 KB 到几 TB 的内存分配请求这些请求的粒度差异巨大进程的页表需要整页4KB分配而文件系统的 inode 只需几十字节。如果所有分配都按页对齐小对象的内存浪费高达 99%。如果所有分配都按字节对齐外部碎片化会导致大块连续内存无法分配。Linux 的解决方案是分层设计伙伴系统Buddy System管理物理页帧解决外部碎片化Slab 分配器在伙伴系统之上管理小对象解决内部碎片化。两层协作各司其职。二、内存管理的分层架构flowchart TB APP[应用层 kmalloc/vmalloc] -- SLAB[Slab 分配器 小对象] APP -- BUDDY[伙伴系统 页帧分配] SLAB -- BUDDY BUDDY -- ZONE[内存区域 ZONE_DMA/NORMAL/HIGHMEM] ZONE -- PHYS[物理内存] subgraph Slab 层 SLAB CACHE[Slab Cache 对象池] end subgraph 伙伴系统层 BUDDY FREE_AREA[空闲区域链表 2^n 页] end三、伙伴系统与 Slab 的核心实现/* 伙伴系统核心逻辑 */ /* 空闲区域每个 order 维护一个链表 */ struct free_area { struct list_head free_list; /* 空闲页块链表 */ unsigned long nr_free; /* 空闲页块数量 */ }; /* 伙伴系统分配从 order 链表中取出 2^order 个连续页 */ static struct page *alloc_pages(unsigned int order) { struct free_area *area; unsigned int current_order; /* 从请求的 order 开始向上查找空闲块 */ for (current_order order; current_order MAX_ORDER; current_order) { area zone-free_area[current_order]; if (!list_empty(area-free_list)) goto found; } return NULL; /* 没有足够大的连续页块 */ found: /* 从链表中取出一个空闲块 */ struct page *page list_entry(area-free_list.next, struct page, lru); list_del(page-lru); area-nr_free--; /* 如果取出的块比请求的大逐级分裂 */ while (current_order order) { current_order--; area zone-free_area[current_order]; /* 将后半部分挂到低一级的链表上 */ struct page *buddy page (1 current_order); list_add(buddy-lru, area-free_list); area-nr_free; } return page; } /* 伙伴系统释放合并相邻的空闲块 */ static void free_pages(struct page *page, unsigned int order) { unsigned long page_idx page_to_pfn(page); /* 尝试与伙伴合并直到无法合并或达到最大 order */ while (order MAX_ORDER - 1) { unsigned long buddy_idx page_idx ^ (1 order); struct page *buddy pfn_to_page(buddy_idx); /* 检查伙伴是否空闲且大小相同 */ if (!page_is_buddy(buddy, order)) break; /* 从链表中移除伙伴合并 */ list_del(buddy-lru); zone-free_area[order].nr_free--; /* 合并后页索引取较小值 */ page_idx min(page_idx, buddy_idx); order; } /* 将合并后的块挂到对应 order 的链表 */ list_add(pfn_to_page(page_idx)-lru, zone-free_area[order].free_list); zone-free_area[order].nr_free; } /* Slab 分配器核心逻辑 */ /* Slab Cache管理同一类型的小对象 */ struct kmem_cache { const char *name; /* Cache 名称 */ unsigned int object_size; /* 对象大小 */ unsigned int objs_per_slab; /* 每个 Slab 中的对象数 */ struct list_head slabs_full; /* 已满 Slab 链表 */ struct list_head slabs_partial; /* 部分 Slab 链表 */ struct list_head slabs_free; /* 空 Slab 链表 */ }; /* Slab 分配从 partial 链表取对象 */ static void *slab_alloc(struct kmem_cache *cachep) { struct slab *slabp; /* 优先从 partial 链表分配 */ if (!list_empty(cachep-slabs_partial)) { slabp list_entry(cachep-slabs_partial.next, struct slab, list); } else if (!list_empty(cachep-slabs_free)) { /* partial 为空从 free 链表取 */ slabp list_entry(cachep-slabs_free.next, struct slab, list); list_move(slabp-list, cachep-slabs_partial); } else { /* 需要新建 Slab向伙伴系统申请一页 */ slabp new_slab(cachep); if (!slabp) return NULL; list_add(slabp-list, cachep-slabs_partial); } /* 从 Slab 中取出一个空闲对象 */ void *objp slabp-freelist; slabp-freelist *(void **)objp; /* freelist 是隐式链表 */ slabp-inuse; /* Slab 满了移到 full 链表 */ if (slabp-inuse cachep-objs_per_slab) list_move(slabp-list, cachep-slabs_full); return objp; } /* Slab 释放将对象放回 freelist */ static void slab_free(struct kmem_cache *cachep, void *objp) { struct slab *slabp virt_to_slab(objp); /* 将对象插入 freelist 头部 */ *(void **)objp slabp-freelist; slabp-freelist objp; slabp-inuse--; /* 根据使用率调整 Slab 所在链表 */ if (slabp-inuse 0) { list_move(slabp-list, cachep-slabs_free); } else if (slabp-inuse cachep-objs_per_slab - 1) { list_move(slabp-list, cachep-slabs_partial); } }四、内存管理的 Trade-offs 分析伙伴系统的内部碎片请求 3 页时分配 4 页2^2浪费 25%。这是页对齐的代价。缓解方案是 Slab 分配器在小对象层面复用浪费的空间。Slab 的对象对齐开销每个 Slab 需要维护 freelist 和元数据这些开销对小对象如 16 字节的 dentry占比显著。Linux 使用隐式 freelist在空闲对象内部存储 next 指针减少元数据开销。NUMA 架构的复杂性多 CPU 系统中每个 CPU 有本地内存节点跨节点访问延迟高。伙伴系统和 Slab 都需要按 NUMA 节点分区增加了管理复杂度。内存压缩的开销当外部碎片化严重时内核需要做内存压缩memory compaction——移动已分配的页以合并空闲块。压缩本身消耗 CPU需要在碎片率和压缩频率之间权衡。五、总结Linux 内存管理的分层设计通过伙伴系统解决外部碎片化、Slab 分配器解决内部碎片化。伙伴系统按 2 的幂次管理页帧分裂和合并操作保证 O(log n) 的分配效率。Slab 分配器在伙伴系统之上管理小对象通过对象池复用减少分配开销。落地时需要关注内部碎片率、Slab 元数据开销、NUMA 分区和内存压缩策略。理解这两层机制是排查 Linux 内存问题的基础。