🐧 第五章 内存管理
📄 一句话概括

直接分物理内存会撞上外部碎片:空闲总量够,就是凑不出连续的一块。所以真正的解法不是治碎片,是绕过去——用虚拟内存。

📝 本节和前面几节的关系

5.2 讲的是【内存不够】,这一节讲【内存明明够,却分不出去】——这是两种完全不同的病。

一、一块 16 单位的内存,四步就碎掉

假设只有一块连续内存,按【首次适配】发:从头找第一块够大的空闲区,切下来给他。 发四个 4 单位出去,内存正好切完。 然后第一块和第三块的进程退出了,于是空闲变成两块 4 单位,中间夹着两块在用。 这时候来一个要 6 单位的请求:空闲总量是 8,比 6 大,但没有一块连续的有 6。 这就是外部碎片:碎的不是内存,是【连续性】。 注意 8 单位的空闲分不成 6+2 吗:不能,因为中间那块是别人的。

二、碎片的三种解法,都只是缓解

一、紧凑(compaction):把所有在用的块挪到一起。问题:挪完所有指针都失效了——这也是 Java 的 GC 要停世界的原因。 二、换合适大小的块:best fit、worst fit。只是换一种碎法,不解决根本。 三、按固定大小切:这样就没有【大小不一】的问题了——代价是内部碎片(要 5 给 8,浪费 3)。 三种都是【在这个框架里打转】。

三、直接分物理内存的三个硬毛病

一、地址不固定:程序每次被装到内存的位置不同,那它编译时定的地址还有意义吗? 二、没有隔离:一个程序算错一个下标,就可能改掉另一个程序的数据。 三、没法共享:同一份库代码要装好几份,内存白白多花。 这三个毛病,靠【把分配器写得更聪明】一个都解决不了。

四、所以答案是绕过去

虚拟内存的思路:别让程序看见物理地址。 程序看到的地址是【虚拟的】,由页表翻译成物理地址,翻译表可以随便改。 于是:地址固定了(虚拟地址不变,物理页可以搬)、隔离有了(各用各的表)、共享有了(两张表填同一个物理页号)。 碎片问题也没有消失,但它被关进了【物理页】这一层,而且因为页大小统一,它退化成了【页够不够】的问题。 这就是本书后面所有内容的地基。

五、动手 Demo:直接发物理内存——先撞上碎片的墙

这一节写一个 16 单位的【首次适配】分配器,真跑一遍发块、释放、再申请,看它怎么卡住。

size = 16
free = [(0, size - 1)]

def alloc(n):
    for i, (a, b) in enumerate(free):
        if b - a + 1 >= n:
            got = (a, a + n - 1)
            if b - a + 1 == n:
                free.pop(i)
            else:
                free[i] = (a + n, b)
            return got
    return None

for n in (4, 4, 4, 4):
    print("申请", n, "->", alloc(n))
print("空闲", free)
# 释放第 1 块与第 3 块之后……
print("再申请 6 ->", alloc(6))

实跑输出:

--------------------------------------------------------------------
直接发物理内存——先撞上碎片的墙
--------------------------------------------------------------------
① 一块 16 单位的内存,按【首次适配】发

  这一步          空闲块           结果
  --------------------------------------------
  申请 4          [4-15]           拿到 0-3
  申请 4          [8-15]           拿到 4-7
  申请 4          [12-15]          拿到 8-11
  申请 4          (空)           拿到 12-15
  释放第 1 块     [0-3]            —
  释放第 3 块     [0-3] [8-11]     —
  申请 6          [0-3] [8-11]     **失败**

  此刻总空闲 = 8 单位,**最大连续块只有 4**,而要的是 6。
  **空闲总量够(8 > 6),但凑不出一块连续的——这叫【外部碎片】。**
  **换成申请 2 呢?**拿到 0-1,成了——**碎是碎了,小块还能用。**

② 直接发物理内存,还有三个更硬的毛病

  毛病                 后果                         谁来解决
  -----------------------------------------------------------------
  地址不固定           程序每次装进来地址都不同     虚拟内存
  没有隔离             一个程序能改另一个的内存     页表 + 保护位
  进程之间没法共享     同一份代码要装好几份         页表指向同一页

  **所以真正做法不是【解决碎片】,而是【绕过去】**:
  **程序看到的是连续的虚拟地址,背后由页表映射到散落的物理页。**

四块 4 单位发完,内存正好用光。释放第 1 块和第 3 块之后,空闲是 [0-3] 与 [8-11] 两块。 这时候申请 6 就失败了——总空闲 8,最大连续块只有 4。 但申请 2 就成功了:碎是碎了,小块还能用。 所以碎片不是【内存坏了】,是【形状不对】。 这就是为什么后来大家不再纠结【怎么切得更好】,而是【让程序看不见物理地址】。

📋 Linux 上可以直接看碎片情况:cat /proc/buddyinfo——一行里每列是【连续 2 的 N 次方页】还有几块。最后一列不动,说明大块内存拿不出来。

30 秒实验

  1. cat /proc/buddyinfo,看这台机器还能凑出多少块连续的 1024 页。
  2. cat /proc/pagetypeinfo | head -20,按【可移动/不可移动】分类的碎片情况。
  3. 写一个小脚本发若干不同大小的块再释放,统计最大连续空闲块变小的过程。

常见误区

  1. 【碎片是内存被浪费了】。碎掉的内存还在空闲列表里,只是形状不对,凑不出大块。
  2. 【内存够大就不会有碎片问题】。恰恰相反——内存越大跑得越久,碎得越细。
  3. 【紧凑一下就好了】。紧凑要把在用的块全挪走,所有指向它们的指针都得改,代价极高。
自测三问(点开对答案) 1. 什么是外部碎片? —— **空闲总量够,但没有一块足够大的连续空闲区**。碎的是连续性,不是总量。 2. 直接分物理内存有哪三个硬毛病? —— **地址不固定、没有隔离、没法共享**。**这三个都靠【改分配器】解决不了,只能靠虚拟内存。** 3. 虚拟内存怎么处理碎片问题的? —— **它没有消灭碎片,而是把碎片关进【物理页】这一层**。因为页大小统一,**问题从【形状不对】退化成【页够不够】。**

相关笔记