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 内存问题的基础。
