3. 物理内存管理:伙伴系统算法、slab分配器、内存碎片化问题

物理内存管理,说白了就是操作系统怎么把有限的物理内存条,高效地分配给各个进程使用。这可不是简单的“谁要就给谁”。内核里有两套核心机制在协同工作:伙伴系统和slab分配器。一个管大块内存的分配与回收,另一个管内核自身频繁创建的小对象。搞懂它们,你才算真正入了内存管理的门。

核心脉络: 伙伴系统解决“大块连续内存”的分配问题;slab分配器解决“内核小对象”的复用问题;而内存碎片化,则是这两套机制在长期运行后,必然会遇到的“顽疾”。

3.1 伙伴系统算法:物理内存的“大管家”

伙伴系统负责管理物理内存的“大块”分配。它把物理内存按照2的幂次方(4KB、8KB、16KB……一直到4MB甚至更大)划分成一个个“块”。每个块都有一个“伙伴”——也就是大小相同、地址相邻的另一个块。

分配逻辑很简单: 你要一个2^3=8KB的块,系统就从8KB的空闲链表里找。如果没有,就从16KB的链表里拆一个出来,一半给你,另一半(它的伙伴)挂回8KB的链表。如果16KB也没有,就继续向上拆,直到找到合适的。

释放逻辑是逆过程: 你释放一个8KB的块,系统会检查它的“伙伴”是否也是空闲的。如果是,就把两个8KB合并成一个16KB的块,挂回16KB的链表。然后继续检查这个16KB的伙伴……直到无法合并为止。

我早年在一台运行了半年的数据库服务器上,发现内存分配越来越慢。用/proc/buddyinfo一看,好家伙,2^0(4KB)的块几乎被耗尽,而2^4(64KB)以上的大块却堆积如山。这就是典型的“外碎片化”——大块内存被拆成了无数小块,但无法合并回去。

避坑指南: 我曾经在排查内存泄漏时,只盯着进程的RSS看,忽略了伙伴系统的状态。后来发现,虽然进程RSS不高,但/proc/buddyinfo显示大量不可移动的页面阻塞了内存合并。记住,/proc/buddyinfo是诊断物理内存碎片的第一现场。

3.2 slab分配器:内核对象的“专属管家”

伙伴系统分配的最小单位是4KB(一页)。但内核里大量需要的是几十字节、几百字节的小对象,比如进程描述符task_struct、文件对象file、网络缓冲区sk_buff。如果每次都找伙伴系统要一页,那浪费就太大了。

slab分配器就是干这个的。它从伙伴系统申请一整页或多页内存,然后把它切分成多个大小相同的“对象缓存”。每个缓存专门管理一种内核对象。

slab分配器的核心优势:

  • 对象复用: 对象释放后,不会真的归还给伙伴系统,而是留在slab缓存里,下次分配直接取用,避免了频繁的页分配和释放。
  • 硬件缓存友好: 同类型的对象在物理上连续存放,能提高CPU缓存的命中率。
  • 着色(Coloring): 通过偏移对象在slab中的起始位置,让不同slab中的对象错开,减少CPU缓存行冲突。

你可以通过slabtop命令实时查看slab缓存的使用情况。我个人习惯在系统上线前,先跑一轮压力测试,然后用slabtop -s c按缓存大小排序,看看哪些内核对象的缓存增长异常。

注意: slab分配器虽然高效,但也会导致“内部碎片”。比如一个对象实际需要60字节,但slab缓存可能按64字节对齐,那4字节就是内部碎片。不过相比伙伴系统的4KB粒度,这点浪费完全可以接受。

3.3 内存碎片化:两大“顽疾”

内存碎片化分为两种:外部碎片和内部碎片。伙伴系统主要面临外部碎片,slab分配器主要面临内部碎片。

碎片类型 产生机制 影响范围 典型场景
外部碎片 频繁分配和释放不同大小的块,导致空闲内存被分割成不连续的小块 伙伴系统 长时间运行的服务器,大块内存分配失败
内部碎片 分配的内存块大于实际请求的大小 slab分配器、伙伴系统 slab缓存对齐、伙伴系统按2的幂次分配

你想想看,外部碎片最头疼的地方在哪?就是明明总空闲内存还有很多,但就是分配不出一个连续的、足够大的块。比如你要分配一个2MB的连续DMA缓冲区,但物理内存被拆成了无数个4KB的小碎片,那就只能干瞪眼。

内核的应对策略:

  • 内存规整(Compaction): 内核会尝试把可移动的页面迁移到一起,腾出连续的大块空间。你可以通过/proc/sys/vm/compact_memory手动触发。
  • 可移动页面(Movable Pages): 内核将页面分为“可移动”、“可回收”、“不可移动”等类型。规整时,只移动“可移动”的页面,避免影响内核核心数据结构。
  • 虚拟可移动内存(ZONE_MOVABLE): 专门划出一块区域,里面的页面都是可移动的,用于用户态进程的匿名页和页缓存。

我曾经遇到一个案例:一台Nginx服务器运行了两个月后,突然无法分配新的网络缓冲区,导致大量连接超时。排查后发现,/proc/buddyinfo中所有order大于3的块都是0,而/proc/pagetypeinfo显示“不可移动”的页面占据了大量内存。最终通过echo 1 > /proc/sys/vm/compact_memory触发内存规整,才恢复了正常。

实用技巧: 如果你怀疑系统存在内存碎片问题,可以执行以下命令快速诊断:
cat /proc/buddyinfo —— 查看各order的空闲块数量。
cat /proc/pagetypeinfo —— 查看各迁移类型的页面分布。
cat /proc/slabinfo —— 查看slab缓存的使用详情。

3.4 知识结构图:物理内存管理全景

下面这张图,把伙伴系统、slab分配器和内存碎片化的关系串起来了。你可以把它当作本章的“思维导图”。

物理内存管理全景图 物理内存管理全景图 物理内存 (Physical Memory) 伙伴系统 (Buddy System) slab分配器 (Slab Allocator) 核心机制 按2的幂次分配/合并 伙伴合并、内存规整 核心机制 对象缓存、复用、着色 从伙伴系统申请大块后切分 外部碎片 (External Fragmentation) 内部碎片 (Internal Fragmentation) 应对策略:内存规整 / 可移动页面 / ZONE_MOVABLE / 调整slab缓存大小 依赖

嗯,这张图把整个脉络理清楚了。伙伴系统是地基,slab分配器是上层建筑,碎片化是两者共同面临的挑战。理解了这个层次关系,你再去分析内存问题,思路就会清晰很多。

一句话总结: 伙伴系统管“大块”的分配与合并,slab分配器管“小块”的缓存与复用。内存碎片化是长期运行的必然结果,内核通过内存规整和页面迁移来对抗它。诊断工具就是/proc/buddyinfo、/proc/pagetypeinfo和slabtop。