内存管理器

4 阅读5分钟

理解了 Buddy System(伙伴系统)和内存分配器,你就真正理解了为什么我们在 C++/Rust 里申请内存有时快、有时慢,以及由于碎片化导致的 OOM (Out of Memory) 是怎么回事。

我们分两部分讲:先讲内存分配器的角色,再讲Buddy System的具体操作。


第一部分:内存分配器 (Memory Allocator) 是干嘛的?

想象一下,操作系统(OS)是土地局,它拥有所有的物理内存(RAM)。 你的程序是开发商,你想盖房子(存数据)。

如果你每盖一个小厕所(比如 new int,4字节)都要跑一趟土地局去申请 4 字节的地,土地局和你的效率都会低到爆炸。

内存分配器(Allocator)  就是夹在中间的**“二房东” (或者说批发商转零售商**)。

它的核心工作流程:

  1. 批发:分配器一次性向操作系统(土地局)申请一大块地(比如 2MB 的 Huge Page)。这通常通过系统调用 mmap 或 sbrk 实现。
  2. 零售:当你在代码里写 let s = Box::new(10) 或 malloc(10) 时,分配器从它手里的那块大地上,切下来一小块给你。
  3. 回收与复用:当你 free 或 drop 时,分配器把这块地收回来,标记为“空闲”,留着下次给别的变量用。注意:它通常不会立刻把地还给操作系统,因为还要用,留着更高效。

为什么需要它?

  • 速度:系统调用(找 OS 要内存)很慢(上下文切换)。分配器在用户态切分内存极快。
  • 管理:它要解决碎片化问题。

第二部分:Buddy System (伙伴系统)

Buddy System 是一种非常经典、优雅的内存管理算法。它主要用于 Linux 内核管理物理内存页(Page),但它的思想也被很多用户态分配器(如 jemalloc)借鉴用于管理大块内存。

它的核心哲学是:二进制切分(一分为二)

1. 核心规则

  • 所有内存块的大小必须是 2 的幂次方 (2n):4KB, 8KB, 16KB, 32KB...
  • 伙伴 (Buddy) :一块大内存切成两半后,这俩互为“伙伴”。

2. 分配过程 (Splitting) —— “蛋糕切切切”

假设内存池里只有一块 64KB 的大蛋糕。 现在你要申请 7KB 的内存。

  1. 对齐:7KB 不是 2 的幂,分配器会把它向上取整到最近的 8KB

  2. 查找

    • 看有没有现成的 8KB?没有。
    • 找大一号的 16KB?没有。
    • ... 直到找到 64KB。
  3. 切分

    • 把 64KB 一刀两断 -> 变成两个 32KB。(互为伙伴)
    • 拿其中一个 32KB,再一刀两断 -> 变成两个 16KB
    • 拿其中一个 16KB,再一刀两断 -> 变成两个 8KB
  4. 交付

    • 把其中一个 8KB 给你用。
    • 剩下的那些(32KB, 16KB, 8KB)挂在空闲链表上等待别人用。

3. 回收过程 (Coalescing) —— “兄弟团聚”

这就是 Buddy System 最牛的地方:极速合并

当你用完归还那 8KB 时:

  1. 分配器立刻计算它的“伙伴”是谁(通过地址运算,非常快)。
  2. 检查伙伴:如果伙伴也是空闲的,合并!变成 16KB。
  3. 继续向上:检查这 16KB 的伙伴在不在?如果在,再合并!变成 32KB。
  4. 连锁反应:如果一路畅通,它们最终会变回最初的那块 64KB 大蛋糕。

为什么叫 Buddy?  因为它们生来就是一对。内存地址只有最后一位不同。 比如地址 0000 和 1000(二进制) 是伙伴。合并后变成 0000 (跨度变大)。


第三部分:Buddy System 的优缺点

没有完美的算法,Buddy System 也是一种权衡。

优点

  1. 合并极快:因为地址是对齐的,通过位运算(XOR)就能算出伙伴地址,不需要遍历链表。
  2. 减少外碎片 (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 字节?直接给你个格子。
  • 效果:几乎没有浪费(除了最后一点点零头)。

总结

  1. 内存分配器是你的管家,帮你向操作系统批发内存,再零售给你,让你不用每次都去排队(系统调用)。

  2. Buddy System 是一种管理大块内存的算法。

    • 核心是 2 的幂次方 和 二分法
    • 分配时像切蛋糕,不断一分为二。
    • 回收时像贪吃蛇,兄弟相遇立刻融合成更大的块。
  3. 主要问题内碎片(要33给64)。

  4. 现代内存管理通常是 Buddy (管大页) + Slab (管小对象)  的组合拳。

你在 Rust 里写的 Box::new 背后的分配器,大概率就在做这些复杂的切割和合并工作,只为了让你感觉不到内存管理的存在。