第7讲 物理内存管理I

物理内存管理 I:从空闲页记录到伙伴系统与 SLAB

虚拟内存管理回答的是“程序看到的虚拟地址如何映射到物理地址”。物理内存管理进一步回答的是:真实的物理内存页由谁管理、如何记录空闲状态、如何分配连续物理页、如何快速分配内核小对象。

《物理内存管理 I》的重点不是换页、页替换或工作集模型,而是物理内存分配器本身。它讨论的是操作系统拿到一大片真实物理内存后,如何把它组织成可分配、可回收、可合并、可复用的资源。

本讲可以用一条主线概括:

1
2
3
4
5
6
7
8
9
物理内存

位图分配器:用 bit 记录每个物理页是否空闲

伙伴系统:高效分配 2^n 个连续物理页

SLAB / SLUB:把页级大块切成小对象

Linux 内核和用户态分配器共同使用

❗ 核心结论: 物理内存管理 I 的核心是“如何分配物理页”。位图、伙伴系统和 SLAB/SLUB 分别解决空闲页记录、连续页分配和小对象分配问题。

1. 物理内存管理 I 解决什么问题

物理内存可以粗略看成一组连续编号的物理页。假设页大小为 4KB,那么系统可以把物理内存划分为:

1
2
3
4
5
物理页 0
物理页 1
物理页 2
物理页 3
...

操作系统需要回答几个问题:

  • 哪些物理页是空闲的?
  • 哪些物理页已经被分配?
  • 如果需要连续 4 个物理页,应该从哪里找?
  • 分配后如何记录状态?
  • 释放后如何恢复空闲状态?
  • 内核只需要几十字节对象时,是否也要分配一整页?

因此,本讲可以分为三个层次:

层次 解决的问题 典型单位
位图分配器 如何记录每个物理页是否空闲 4KB 页
伙伴系统 如何高效分配连续物理页 2^n 个页
SLAB / SLUB 如何快速分配内核小对象 几十或几百字节

物理内存管理 II 主要讨论“物理内存不够时怎么办”,例如页面换出、页替换策略和工作集模型。本讲只讨论“物理内存如何分配”。


❗ 本节核心结论: 物理内存管理 I 关注的是物理页的分配与回收,不重点讨论内存不足时的换页策略。

2. 评价维度:利用率、速度与碎片

物理内存分配器通常要在几个目标之间权衡:

  1. 分配速度:申请内存时能否快速找到合适区域。
  2. 释放速度:释放内存后能否快速恢复管理结构。
  3. 内存利用率:分配出去的空间是否尽量接近真实需求。
  4. 碎片控制:系统运行一段时间后,是否仍能分配较大的连续空间。

碎片主要分为两类:外部碎片和内部碎片。

类型 含义 例子
外部碎片 空闲空间总量足够,但分散在不同位置,无法满足连续分配 有很多 4KB 空闲页,但无法找到连续 16KB
内部碎片 分配出去的块比实际需求大,多出来的部分浪费在块内部 申请 15KB,系统分配 16KB,多出 1KB

可以用一个直观类比理解:

类比 含义
外部碎片 酒店有多个空房间,但不连续;客人要求住连续房间,所以安排不了
内部碎片 客人只需要单人间,但酒店只能给双人间,多出来的床位浪费了

这两个碎片问题贯穿整节课。位图分配器容易遇到连续页查找困难;伙伴系统通过拆分和合并缓解外部碎片,但会引入内部碎片;SLAB/SLUB 则是为了减少小对象分配造成的内部浪费。


❗ 本节核心结论: 物理内存分配器的设计核心是权衡速度、利用率、外部碎片和内部碎片。

3. 位图分配器:最朴素的物理页管理方式

位图分配器的思想非常直接:用一个 bit 表示一个物理页是否空闲。

例如:

1
bit bitmap[N];

如果系统有 N 个 4KB 物理页,那么:

1
2
bitmap[i] = 0 表示第 i 个物理页空闲
bitmap[i] = 1 表示第 i 个物理页已分配

3.1 分配连续物理页

如果要分配 n 个连续物理页,位图分配器需要扫描 bitmap,寻找连续 n 个 0:

1
2
3
4
5
6
7
扫描 bitmap

找到连续 n 个 0

把这 n 个 bit 设为 1

返回第一个物理页的起始地址

伪代码可以写成:

1
2
3
4
5
6
for (int i = 0; i < N; i++) {
if (bitmap[i] 到 bitmap[i + n - 1] 全部为 0) {
把这些 bit 全部设为 1;
return FREE_MEM_START + i * PAGE_SIZE;
}
}

3.2 释放连续物理页

释放时,操作系统根据物理地址反推出页号:

1
page_idx = (addr - FREE_MEM_START) / PAGE_SIZE

然后把对应的 bit 改回 0:

1
2
3
bitmap[page_idx] = 0
bitmap[page_idx + 1] = 0
...

3.3 位图分配器的优缺点

位图分配器的优点是:

  • 结构简单;
  • 容易理解;
  • 记录状态的空间开销较小;
  • 每个页是否空闲一目了然。

缺点也很明显:

  • 找连续页时可能需要线性扫描;
  • 系统运行久了以后,空闲页可能分散;
  • 连续大块分配可能变慢或失败。

例如 bitmap 状态为:

1
1 0 1 0 0 1 0 1 0 0 0 1

如果系统要找连续很多个 0,就可能扫描很久。


❗ 本节核心结论: 位图分配器适合说明“如何记录物理页状态”,但查找连续空闲页可能较慢。

4. 伙伴系统:连续物理页分配的核心机制

伙伴系统是更接近真实操作系统的页级分配机制。它的核心思想是:只按 2 的幂大小分配连续物理页块。

假设页大小为 4KB,那么伙伴系统管理的块大小通常是:

阶数 页数 大小
0 1 页 4KB
1 2 页 8KB
2 4 页 16KB
3 8 页 32KB
4 16 页 64KB

因此,伙伴系统不直接分配 7KB、15KB、17KB 这种任意大小,而是向上取整到合适的 2^n 页块。

例如:

1
2
3
申请 7KB  → 分配 8KB
申请 15KB → 分配 16KB
申请 17KB → 分配 32KB

这会产生一定内部碎片,但换来更简单、更高效的管理结构。


❗ 本节核心结论: 伙伴系统用 2^n 个连续页作为分配单位,用规则化的块大小换取更高效的分配和合并。

5. 分配时拆分,释放时合并:伙伴系统好在哪里

伙伴系统最重要的一句话是:

1
分配时不断拆分;释放时尽量合并。

这个设计的好处在于:既能避免小请求浪费大块,又能在释放后尽量恢复大块连续空间。

5.1 分配时拆分:避免大块被小请求浪费

假设当前只有一个 32KB 空闲块:

1
[              32KB              ]

现在程序申请 7KB。伙伴系统需要分配 8KB,但当前没有 8KB 块,于是它会拆分:

1
2
3
4
5
[              32KB              ]

[ 16KB ][ 16KB ]

[ 8KB ][ 8KB ][ 16KB ]

然后分配一个 8KB:

1
[8KB 已分配][8KB 空闲][16KB 空闲]

如果不拆分,系统可能直接把 32KB 给这个 7KB 请求,浪费太多。拆分让系统尽量给出“刚好够用”的 2^n 大小块。

5.2 释放时合并:缓解外部碎片

如果刚才分配出去的 8KB 被释放,并且它的 8KB 伙伴也空闲:

1
[8KB 空闲][8KB 空闲][16KB 空闲]

就可以合并成:

1
[      16KB 空闲      ][16KB 空闲]

如果这两个 16KB 也是伙伴,还可以继续合并:

1
[              32KB 空闲              ]

这就是伙伴系统缓解外部碎片的关键。它不会让释放后的空闲块永远碎成小块,而是尽量合并回大块。

5.3 为什么伙伴容易找到

伙伴系统中的“伙伴”不是随便找的。两个互为伙伴的块:

  • 大小相同;
  • 物理地址相邻;
  • 由同一个更大块拆分而来。

因此,释放某个块时,系统可以根据地址和块大小快速计算它的伙伴地址。抽象公式可以理解为:

1
buddy_addr = block_addr ^ block_size

这意味着系统不必全局搜索“谁能和我合并”,而是能直接定位候选伙伴。

5.4 这个方法的本质权衡

操作 方法 好处
分配 大块拆成小块 尽量减少过度分配
释放 小块合并成大块 缓解外部碎片
查找伙伴 通过地址计算 合并效率较高
块大小 固定为 2 的幂 数据结构简单

但它也有代价:

1
2
3
4
5
申请 17KB

分配 32KB

浪费 15KB

所以伙伴系统本质上是一个 tradeoff:

1
2
3
牺牲一定内部碎片

换取更快分配、更快释放、更容易合并、更适合连续物理页管理

❗ 本节核心结论: 伙伴系统通过“分配时拆分、释放时合并”在分配速度、连续空间恢复和内部碎片之间取得平衡。

6. 链表数组:伙伴系统如何组织空闲块

伙伴系统通常使用“链表数组”管理空闲块。

所谓链表数组,就是:

1
2
一个数组,数组的每个元素是一条链表。
不同链表管理不同大小的空闲块。

假设页大小为 4KB:

1
2
3
4
5
free_area[0]:管理 1 页块,也就是 4KB
free_area[1]:管理 2 页块,也就是 8KB
free_area[2]:管理 4 页块,也就是 16KB
free_area[3]:管理 8 页块,也就是 32KB
free_area[4]:管理 16 页块,也就是 64KB

可以表示为:

1
2
3
4
5
6
7
free_area[0] -> 4KB块 -> 4KB块 -> NULL

free_area[1] -> 8KB块 -> 8KB块 -> NULL

free_area[2] -> 16KB块 -> NULL

free_area[3] -> 32KB块 -> 32KB块 -> NULL

6.1 为什么用“数组 + 链表”

数组的作用是快速定位大小。

例如申请 8KB,系统直接看:

1
free_area[1]

如果 free_area[1] 有空闲块,直接取一个。

如果没有,就向更高阶查找:

1
2
3
free_area[2]:16KB
free_area[3]:32KB
...

找到更大块后再拆分。

链表的作用是保存多个同大小的空闲块。同一种大小可能有很多块,链表适合把它们串起来。

6.2 申请 7KB 的例子

7KB 向上取整为 8KB,因此需要找 free_area[1]

如果 free_area[1] 有空闲块:

1
free_area[1] -> [8KB] -> [8KB]

直接取一个。

如果没有 8KB,但有 16KB:

1
free_area[2] -> [16KB]

就拆成两个 8KB:

1
2
3
[16KB]

[8KB][8KB]

其中一个分配出去,另一个挂回 free_area[1]

6.3 释放 8KB 的例子

释放一个 8KB 块时,系统会:

  1. 计算它的 8KB 伙伴地址;
  2. 检查伙伴是否空闲;
  3. 如果伙伴空闲,把两个 8KB 合并成 16KB;
  4. 继续检查 16KB 块的伙伴;
  5. 能合并就继续向上合并;
  6. 不能合并时,把最终块挂到对应 free_area[k] 链表中。

这个过程让伙伴系统可以在释放时尽量恢复大块连续空间。

这张图强调:伙伴系统释放时不是简单“放回链表”,而是先尽量合并,直到无法继续合并为止。


❗ 本节核心结论: 伙伴系统的链表数组用数组区分块大小,用链表保存同大小空闲块,从而支持快速查找、拆分和合并。

7. 位图和伙伴系统不是上下级关系

位图和伙伴系统容易被误认为是上下级关系,但更准确的说法是:它们是同一层的两种页级物理内存分配方案。

它们都解决类似问题:

1
2
3
如何记录空闲物理页?
如何分配连续物理页?
如何释放物理页?

区别在于实现方式不同。

对比项 位图分配器 伙伴系统
管理单位 单个物理页 2^n 个连续页组成的块
空闲状态记录 每页一个 bit 每阶一条空闲链表
分配方式 扫描 bitmap 找连续 0 按阶查链表,不够则拆大块
释放方式 对应 bit 置 0 尝试与伙伴合并
优点 简单、省空间 更适合连续页分配和回收
缺点 找连续页可能慢 有内部碎片,结构更复杂

真正明显的上下级关系是:

1
2
3
4
5
6
7
物理内存

伙伴系统

SLAB / SLUB

内核小对象

也就是说:

  • 位图和伙伴系统是同层替代方案;
  • 伙伴系统和 SLAB/SLUB 是上下级关系。

❗ 本节核心结论: 位图和伙伴系统都是页级物理内存分配方法,不是上下级;伙伴系统与 SLAB/SLUB 才是上下级关系。

8. 为什么伙伴系统之上还需要 SLAB / SLUB

伙伴系统已经能分配物理页,为什么还需要 SLAB 或 SLUB?

原因是:伙伴系统的最小单位通常是一个物理页,而内核经常需要分配远小于一页的小对象。

例如内核中经常分配:

  • struct task_struct
  • struct file
  • struct inode
  • struct vm_area_struct
  • 各类缓存对象和描述符

这些对象可能只有几十字节或几百字节。如果每次都从伙伴系统分配一个 4KB 页,会造成严重内部碎片。

例如申请 128 字节对象:

1
2
3
伙伴系统最小分配 4KB = 4096B
实际需要 128B
浪费 4096 - 128 = 3968B

因此需要在伙伴系统之上再建立一层小对象分配器。

SLAB/SLUB 的基本思想是:

1
2
3
4
5
先从伙伴系统拿一批页

把这批页切成固定大小的小对象

内核申请小对象时,从对象池里快速取出

例如从伙伴系统拿到 4KB:

1
[              4KB              ]

如果这是 128B 对象池,就切成:

1
[128B][128B][128B][128B]...[128B]

以后内核申请 128B 对象,就不必再直接找伙伴系统,而是从这个池子中取一个对象。


❗ 本节核心结论: 伙伴系统适合页级大块分配;SLAB/SLUB 适合内核小对象分配,能显著减少小对象导致的内部碎片。

9. SLAB / SLUB 的核心结构:对象池与 slab

SLAB 家族包括 SLAB、SLUB、SLOB 等实现。本讲重点可以抓住 SLUB 的思想:按对象大小建立缓存池,每个缓存池管理若干 slab。

一个 slab 可以理解为:从伙伴系统拿到的一块连续内存,再切成固定大小对象。

例如一个 128B 对象缓存池:

1
2
3
4
5
kmem_cache: 128B 对象池

slab A: [128B][128B][128B]...
slab B: [128B][128B][128B]...
slab C: [128B][128B][128B]...

如果内核申请 100B,系统可能从 128B 对象池中分配一个对象。这样虽然仍有少量内部碎片,但比直接分配 4KB 页小得多。

9.1 SLUB 为什么快

SLUB 快的原因主要有两个:

  1. 复用对象池:不需要每次都向伙伴系统申请页。
  2. 固定大小对象:分配和释放可以通过简单链表操作完成。

分配 128B 对象时:

1
2
3
4
5
找到 128B kmem_cache

从当前 slab 的空闲对象链表取出一个对象

返回对象地址

释放时:

1
把对象插回所在 slab 的空闲对象链表

很多操作可以做到接近 O(1)。


❗ 本节核心结论: SLAB/SLUB 通过对象池复用固定大小小块,避免频繁调用伙伴系统,从而提高内核小对象分配速度。

10. SLAB/SLUB 中的链表结构

SLAB/SLUB 中也大量使用链表结构,而且至少有两层链表思想:

  1. slab 之间的链表
  2. slab 内部空闲对象链表

10.1 slab 之间的链表:current、partial、full

在 SLUB 的抽象模型中,一个对象缓存池可以维护三类 slab:

名称 含义
current 当前优先使用的 slab
partial 还有空闲对象、但不是全空或全满的 slab 链表
full 已经没有空闲对象的 slab 链表

可以表示为:

1
2
3
4
5
6
7
8
9
10
11
current

[ slab A:正在使用 ]

partial

[ slab B:还有空闲对象 ] -> [ slab C:还有空闲对象 ] -> NULL

full

[ slab D:已经满了 ] -> [ slab E:已经满了 ] -> NULL

分配时:

1
2
3
4
5
6
7
优先从 current slab 分配对象

如果 current 满了,把它移到 full

从 partial 取一个 slab 作为新的 current

如果 partial 也没有,就向伙伴系统申请新的 slab

释放时:

1
2
3
4
5
对象回到所在 slab

如果原来是 full,现在变得不满,则移回 partial

如果某个 slab 全空,可能归还给伙伴系统

10.2 slab 内部的空闲对象链表

一个 slab 内部被切成许多固定大小对象。例如:

1
2
slab A:
[128B][128B][128B][128B][128B][128B]

其中部分对象已分配,部分对象空闲:

1
[已用][空闲][已用][空闲][空闲][已用]

这些空闲对象会被串成空闲链表:

1
2
3
free_list

[空闲对象1] -> [空闲对象2] -> [空闲对象3] -> NULL

分配对象时:

1
2
3
obj = free_list.head
free_list.head = obj->next
return obj

释放对象时:

1
2
obj->next = free_list.head
free_list.head = obj

这样分配和释放都很快。

10.3 空闲对象的 next 指针存在哪里

一个重要细节是:空闲对象本身已经不被使用,所以可以把它自身开头的几个字节拿来存 next 指针。

例如一个 128B 空闲对象可以看成:

1
[ next 指针 | 剩余空间................ ]

当对象被分配出去时,这 128B 全部交给使用者。当对象被释放回来后,它又可以重新作为链表节点,开头位置保存下一个空闲对象的地址。

因此,SLUB 不需要为每个空闲小对象额外分配独立链表节点。

10.4 SLUB 的整体结构

可以把 SLUB 的对象缓存池表示为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
kmem_cache:128B 对象池

├── current
│ ↓
│ slab A
│ ├── 128B 已用
│ ├── 128B 空闲 -> free_list
│ ├── 128B 已用
│ └── 128B 空闲

├── partial
│ ↓
│ slab B -> slab C -> slab D

└── full

slab E -> slab F

这张图强调:SLUB 既要管理 slab 之间的状态,也要管理每个 slab 内部的空闲对象。


❗ 本节核心结论: SLAB/SLUB 中既有 slab 之间的链表,也有 slab 内部的空闲对象链表;这种结构让小对象分配和释放非常快。

11. 伙伴系统与 SLAB/SLUB 的关系

伙伴系统和 SLAB/SLUB 是上下级关系。

伙伴系统负责页级别的连续物理内存分配:

1
4KB、8KB、16KB、32KB、64KB...

SLAB/SLUB 从伙伴系统拿到页级大块,再切成小对象:

1
32B、64B、128B、256B、512B...

可以表示为:

一个直观类比是:

1
2
伙伴系统像批发市场,按箱发货;
SLUB 像小卖部,把一箱货拆开,一件一件卖给内核。

如果内核要一个 128B 对象,不应该直接让伙伴系统给一整页,而应该从 SLUB 的 128B 对象池中取一个。

如果 SLUB 的对象池没有可用 slab,它才向伙伴系统申请新的页级大块。


❗ 本节核心结论: 伙伴系统给 SLAB/SLUB 提供页级大块;SLAB/SLUB 再把大块切成内核小对象。二者是上下级协作关系。

12. Linux 内存管理架构:用户态与内核态的分工

Linux 内存管理可以粗略分成用户态分配器和内核态分配器两层。

12.1 用户态:malloc/free 不是系统调用本身

用户程序通常使用:

1
2
malloc();
free();

mallocfree 通常不是系统调用本身,而是 C 库中的用户态分配器接口。

常见用户态分配器包括:

分配器 说明
ptmalloc glibc 默认分配器
jemalloc 常见高性能内存分配器
tcmalloc Google 的高性能内存分配器

用户态分配器会先在自己的堆管理结构中找空间。如果不够,再通过系统调用向内核申请更多虚拟内存,例如:

  • brk
  • mmap

12.2 内核态:真正管理物理内存资源

内核负责更底层的内存管理,包括:

  • 虚拟内存区域管理;
  • 缺页异常处理;
  • 页表管理;
  • 物理页分配器;
  • SLAB/SLUB 小对象分配器;
  • 页回收;
  • OOM killer;
  • 内存控制组等。

可以这样理解:

1
2
3
4
5
6
7
8
9
10
11
12
13
用户态 malloc/free

用户态分配器 ptmalloc/jemalloc/tcmalloc

brk 或 mmap 向内核申请虚拟地址空间

程序第一次访问时触发缺页异常

内核从伙伴系统分配物理页

建立页表映射

程序继续运行

❗ 本节核心结论: 用户态 malloc/free 管理的是用户进程视角下的内存块;内核负责虚拟地址区域、缺页异常、页表映射和真实物理页分配。

13. malloc(100) 背后可能发生什么

假设用户程序执行:

1
char *p = malloc(100);

这个调用背后可能经历:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
用户程序调用 malloc(100)

glibc 的 ptmalloc 检查已有空闲块

如果用户态堆中有合适小块,直接返回

如果不够,通过 brk 或 mmap 向内核申请更多虚拟内存

内核记录新的虚拟内存区域

程序第一次真正访问这块内存

触发缺页异常

内核从物理页分配器拿一个物理页

伙伴系统分配页级物理内存

内核建立虚拟页到物理页的映射

程序继续执行

这里有一个关键点:

1
malloc(100) 不一定立刻分配真实物理内存。

它可能只是让用户态分配器返回一段可用虚拟地址范围。真正的物理页可能要等第一次访问时才由缺页异常触发分配。

这与虚拟内存管理 II 中的延迟映射是一条连续逻辑:

1
2
3
4
5
先获得虚拟地址区域

第一次访问时再分配物理页

建立页表映射

❗ 本节核心结论: malloc(100) 和“立刻拿到 100 字节物理内存”不是一回事;真实物理页可能在首次访问时才由内核分配。

14. 一个完整例子:从物理页到内核小对象

假设内核需要频繁分配一个 128B 的结构体对象。

14.1 如果只用伙伴系统

如果每次都从伙伴系统申请:

1
2
3
4
5
申请 128B

伙伴系统最小给 4KB

浪费 3968B

这对频繁出现的小对象非常低效。

14.2 使用 SLUB 后

SLUB 可以先向伙伴系统申请一个 4KB slab:

1
[              4KB              ]

然后切成 128B 对象:

1
[128B][128B][128B][128B]...[128B]

4KB 可以切出:

1
4096 / 128 = 32 个对象

之后每次内核申请 128B:

1
从 current slab 的 free_list 拿一个对象

释放时:

1
把对象放回 free_list

如果当前 slab 满了,SLUB 去 partial 链表找还有空闲对象的 slab。如果 partial 也没有,再向伙伴系统申请新的 slab。

整个过程可以概括为:

1
2
3
4
5
6
7
物理内存

伙伴系统分配 4KB 页

SLUB 把 4KB 切成 32 个 128B 对象

内核快速分配和释放这些对象

❗ 本节核心结论: 伙伴系统解决“从物理内存拿页”,SLUB 解决“把页切成小对象并高效复用”。

15. 易混点总结

15.1 物理内存管理不是完全独立于虚拟内存管理

物理内存管理的底层对象是物理页,但它经常与虚拟内存管理协作。

例如:

1
2
3
4
5
6
7
mmap 申请虚拟地址区域

第一次访问触发缺页异常

内核从伙伴系统分配物理页

页表建立虚拟页到物理页的映射

虚拟内存管理负责地址空间和映射,物理内存管理负责真实物理页资源。

15.2 分页机制缓解外部碎片,但不代表物理内存完全不需要连续页

分页让虚拟页可以映射到不连续的物理页,因此普通程序不一定需要连续物理内存。

但系统中仍有场景需要连续物理页,例如:

  • 大页;
  • 某些驱动;
  • DMA;
  • 连续内存分配需求。

因此,物理内存管理仍然要考虑连续页分配和外部碎片。

15.3 伙伴系统不是没有碎片

伙伴系统主要缓解外部碎片,但会产生内部碎片。

例如:

1
2
3
4
5
申请 17KB

分配 32KB

浪费 15KB

所以伙伴系统不是完美分配器,而是在效率和碎片之间做折中。

15.4 位图和伙伴系统不是上下级

位图和伙伴系统都是页级物理内存分配方案。它们是同层替代关系。

真正的上下级关系是:

1
2
3
伙伴系统

SLAB/SLUB

15.5 SLAB/SLUB 不是替代伙伴系统

SLAB/SLUB 依赖伙伴系统。它不能凭空产生物理内存,而是先从伙伴系统拿到页级大块,再切成小对象。


❗ 本节核心结论: 本讲最容易混淆的是“位图与 buddy 的同层关系”“buddy 与 SLUB 的上下级关系”“虚拟内存管理与物理页分配的协作关系”。

16. 复习清单

学完《物理内存管理 I》后,应能回答以下问题:

  1. 物理内存管理 I 和物理内存管理 II 的区别是什么?
  2. 什么是物理页?
  3. 物理内存分配器需要考虑哪些评价指标?
  4. 外部碎片和内部碎片有什么区别?
  5. 位图分配器如何记录物理页状态?
  6. 位图分配器如何分配连续物理页?
  7. 位图分配器的优点和缺点是什么?
  8. 伙伴系统为什么只分配 2^n 页大小的块?
  9. 伙伴系统中“分配时拆分”是什么意思?
  10. 伙伴系统中“释放时合并”是什么意思?
  11. 伙伴系统为什么能缓解外部碎片?
  12. 伙伴系统为什么会产生内部碎片?
  13. 什么是伙伴系统中的链表数组?
  14. free_area[0]free_area[1]free_area[2] 分别管理什么?
  15. 位图和伙伴系统是上下级关系吗?
  16. 为什么伙伴系统之上还需要 SLAB/SLUB?
  17. 一个 slab 是什么?
  18. SLUB 中 currentpartialfull 分别表示什么?
  19. slab 内部的空闲对象链表如何工作?
  20. 为什么空闲对象本身可以存放 next 指针?
  21. 伙伴系统和 SLUB 是什么关系?
  22. 用户态 malloc(100) 是否一定立刻分配真实物理内存?
  23. malloc → brk/mmap → 缺页异常 → buddy → 页表映射 这条链路如何理解?

结论

《物理内存管理 I》的核心是理解现代操作系统如何组织真实物理内存资源。

这一讲可以压缩为一句话:

位图用 bit 记录每个物理页是否空闲;伙伴系统用链表数组管理不同大小的连续页块,并通过拆分与合并提高连续页分配能力;SLAB/SLUB 建立在伙伴系统之上,把页级大块切成固定大小小对象,从而支持内核高效分配小内存对象。

真正掌握这一讲,需要形成三个层次直觉:

  1. 位图层面:物理内存可以被看成一组页,每页有空闲或已分配状态。
  2. 伙伴系统层面:连续物理页按 2^n 块管理,分配时拆分,释放时合并。
  3. SLAB/SLUB 层面:内核小对象不能每次都直接拿一整页,而应该从对象池中快速分配。

因此,物理内存管理并不是单纯“找一块空内存”这么简单。它是一套围绕速度、碎片、连续性和对象复用构造出来的分层分配体系。