理解了 Buddy System(伙伴系统)和内存分配器,你就真正理解了为什么我们在 C++/Rust 里申请内存有时快、有时慢,以及由于碎片化导致的 OOM (Out of Memory) 是怎么回事。
我们分两部分讲:先讲内存分配器的角色,再讲Buddy System的具体操作。
第一部分:内存分配器 (Memory Allocator) 是干嘛的?
想象一下,操作系统(OS)是土地局,它拥有所有的物理内存(RAM)。 你的程序是开发商,你想盖房子(存数据)。
如果你每盖一个小厕所(比如 new int,4字节)都要跑一趟土地局去申请 4 字节的地,土地局和你的效率都会低到爆炸。
内存分配器(Allocator) 就是夹在中间的**“二房东” (或者说批发商转零售商**)。
它的核心工作流程:
- 批发:分配器一次性向操作系统(土地局)申请一大块地(比如 2MB 的 Huge Page)。这通常通过系统调用
mmap或sbrk实现。 - 零售:当你在代码里写
let s = Box::new(10)或malloc(10)时,分配器从它手里的那块大地上,切下来一小块给你。 - 回收与复用:当你
free或drop时,分配器把这块地收回来,标记为“空闲”,留着下次给别的变量用。注意:它通常不会立刻把地还给操作系统,因为还要用,留着更高效。
为什么需要它?
- 速度:系统调用(找 OS 要内存)很慢(上下文切换)。分配器在用户态切分内存极快。
- 管理:它要解决碎片化问题。
第二部分:Buddy System (伙伴系统)
Buddy System 是一种非常经典、优雅的内存管理算法。它主要用于 Linux 内核管理物理内存页(Page),但它的思想也被很多用户态分配器(如 jemalloc)借鉴用于管理大块内存。
它的核心哲学是:二进制切分(一分为二) 。
1. 核心规则
- 所有内存块的大小必须是 2 的幂次方 (2n):4KB, 8KB, 16KB, 32KB...
- 伙伴 (Buddy) :一块大内存切成两半后,这俩互为“伙伴”。
2. 分配过程 (Splitting) —— “蛋糕切切切”
假设内存池里只有一块 64KB 的大蛋糕。 现在你要申请 7KB 的内存。
-
对齐:7KB 不是 2 的幂,分配器会把它向上取整到最近的 8KB。
-
查找:
- 看有没有现成的 8KB?没有。
- 找大一号的 16KB?没有。
- ... 直到找到 64KB。
-
切分:
- 把 64KB 一刀两断 -> 变成两个 32KB。(互为伙伴)
- 拿其中一个 32KB,再一刀两断 -> 变成两个 16KB。
- 拿其中一个 16KB,再一刀两断 -> 变成两个 8KB。
-
交付:
- 把其中一个 8KB 给你用。
- 剩下的那些(32KB, 16KB, 8KB)挂在空闲链表上等待别人用。
3. 回收过程 (Coalescing) —— “兄弟团聚”
这就是 Buddy System 最牛的地方:极速合并。
当你用完归还那 8KB 时:
- 分配器立刻计算它的“伙伴”是谁(通过地址运算,非常快)。
- 检查伙伴:如果伙伴也是空闲的,合并!变成 16KB。
- 继续向上:检查这 16KB 的伙伴在不在?如果在,再合并!变成 32KB。
- 连锁反应:如果一路畅通,它们最终会变回最初的那块 64KB 大蛋糕。
为什么叫 Buddy? 因为它们生来就是一对。内存地址只有最后一位不同。 比如地址
0000和1000(二进制) 是伙伴。合并后变成0000(跨度变大)。
第三部分:Buddy System 的优缺点
没有完美的算法,Buddy System 也是一种权衡。
优点
- 合并极快:因为地址是对齐的,通过位运算(XOR)就能算出伙伴地址,不需要遍历链表。
- 减少外碎片 (External Fragmentation) :通过不断的合并,尽量保证有大块内存可用,不会出现“由于全是小洞洞,导致放不下一个大箱子”的情况。
缺点:内碎片 (Internal Fragmentation)
这是 Buddy System 最大的痛。
- 场景:你只要 33KB。
- 处理:Buddy 说:“对不起,我只能切 2 的幂。下一个档位是 64KB。”
- 结果:给了你 64KB。你只用了 33KB,剩下的 31KB 在这块内存内部被浪费了,别人也用不了。
这就是为什么现代分配器(如 Rust 默认的
jemalloc或 glibc 的ptmalloc)通常采用混合策略:
- 大内存:用 Buddy System 思想。
- 小内存(比如几十字节的对象):用 Slab Allocator。
补充:Slab Allocator (Buddy 的好搭档)
为了解决 Buddy 的“内碎片”浪费问题,Slab 被发明了出来。它专门处理小对象。
- 原理:向 Buddy 申请一大块地(比如 4KB)。
- 细分:把这 4KB 切成无数个固定大小的小格子(比如全是 32 字节的格子)。
- 分配:你要 32 字节?直接给你个格子。
- 效果:几乎没有浪费(除了最后一点点零头)。
总结
-
内存分配器是你的管家,帮你向操作系统批发内存,再零售给你,让你不用每次都去排队(系统调用)。
-
Buddy System 是一种管理大块内存的算法。
- 核心是 2 的幂次方 和 二分法。
- 分配时像切蛋糕,不断一分为二。
- 回收时像贪吃蛇,兄弟相遇立刻融合成更大的块。
-
主要问题是内碎片(要33给64)。
-
现代内存管理通常是 Buddy (管大页) + Slab (管小对象) 的组合拳。
你在 Rust 里写的 Box::new 背后的分配器,大概率就在做这些复杂的切割和合并工作,只为了让你感觉不到内存管理的存在。