第7讲 物理内存管理I

第7讲 物理内存管理I
agsd物理内存管理 I:从空闲页记录到伙伴系统与 SLAB
虚拟内存管理回答的是“程序看到的虚拟地址如何映射到物理地址”。物理内存管理进一步回答的是:真实的物理内存页由谁管理、如何记录空闲状态、如何分配连续物理页、如何快速分配内核小对象。
《物理内存管理 I》的重点不是换页、页替换或工作集模型,而是物理内存分配器本身。它讨论的是操作系统拿到一大片真实物理内存后,如何把它组织成可分配、可回收、可合并、可复用的资源。
本讲可以用一条主线概括:
1 | 物理内存 |
❗ 核心结论: 物理内存管理 I 的核心是“如何分配物理页”。位图、伙伴系统和 SLAB/SLUB 分别解决空闲页记录、连续页分配和小对象分配问题。
1. 物理内存管理 I 解决什么问题
物理内存可以粗略看成一组连续编号的物理页。假设页大小为 4KB,那么系统可以把物理内存划分为:
1 | 物理页 0 |
操作系统需要回答几个问题:
- 哪些物理页是空闲的?
- 哪些物理页已经被分配?
- 如果需要连续 4 个物理页,应该从哪里找?
- 分配后如何记录状态?
- 释放后如何恢复空闲状态?
- 内核只需要几十字节对象时,是否也要分配一整页?
因此,本讲可以分为三个层次:
| 层次 | 解决的问题 | 典型单位 |
|---|---|---|
| 位图分配器 | 如何记录每个物理页是否空闲 | 4KB 页 |
| 伙伴系统 | 如何高效分配连续物理页 | 2^n 个页 |
| SLAB / SLUB | 如何快速分配内核小对象 | 几十或几百字节 |
物理内存管理 II 主要讨论“物理内存不够时怎么办”,例如页面换出、页替换策略和工作集模型。本讲只讨论“物理内存如何分配”。
❗ 本节核心结论: 物理内存管理 I 关注的是物理页的分配与回收,不重点讨论内存不足时的换页策略。
2. 评价维度:利用率、速度与碎片
物理内存分配器通常要在几个目标之间权衡:
- 分配速度:申请内存时能否快速找到合适区域。
- 释放速度:释放内存后能否快速恢复管理结构。
- 内存利用率:分配出去的空间是否尽量接近真实需求。
- 碎片控制:系统运行一段时间后,是否仍能分配较大的连续空间。
碎片主要分为两类:外部碎片和内部碎片。
| 类型 | 含义 | 例子 |
|---|---|---|
| 外部碎片 | 空闲空间总量足够,但分散在不同位置,无法满足连续分配 | 有很多 4KB 空闲页,但无法找到连续 16KB |
| 内部碎片 | 分配出去的块比实际需求大,多出来的部分浪费在块内部 | 申请 15KB,系统分配 16KB,多出 1KB |
可以用一个直观类比理解:
| 类比 | 含义 |
|---|---|
| 外部碎片 | 酒店有多个空房间,但不连续;客人要求住连续房间,所以安排不了 |
| 内部碎片 | 客人只需要单人间,但酒店只能给双人间,多出来的床位浪费了 |
这两个碎片问题贯穿整节课。位图分配器容易遇到连续页查找困难;伙伴系统通过拆分和合并缓解外部碎片,但会引入内部碎片;SLAB/SLUB 则是为了减少小对象分配造成的内部浪费。
❗ 本节核心结论: 物理内存分配器的设计核心是权衡速度、利用率、外部碎片和内部碎片。
3. 位图分配器:最朴素的物理页管理方式
位图分配器的思想非常直接:用一个 bit 表示一个物理页是否空闲。
例如:
1 | bit bitmap[N]; |
如果系统有 N 个 4KB 物理页,那么:
1 | bitmap[i] = 0 表示第 i 个物理页空闲 |
3.1 分配连续物理页
如果要分配 n 个连续物理页,位图分配器需要扫描 bitmap,寻找连续 n 个 0:
1 | 扫描 bitmap |
伪代码可以写成:
1 | for (int i = 0; i < N; i++) { |
3.2 释放连续物理页
释放时,操作系统根据物理地址反推出页号:
1 | page_idx = (addr - FREE_MEM_START) / PAGE_SIZE |
然后把对应的 bit 改回 0:
1 | bitmap[page_idx] = 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 | 申请 7KB → 分配 8KB |
这会产生一定内部碎片,但换来更简单、更高效的管理结构。
❗ 本节核心结论: 伙伴系统用 2^n 个连续页作为分配单位,用规则化的块大小换取更高效的分配和合并。
5. 分配时拆分,释放时合并:伙伴系统好在哪里
伙伴系统最重要的一句话是:
1 | 分配时不断拆分;释放时尽量合并。 |
这个设计的好处在于:既能避免小请求浪费大块,又能在释放后尽量恢复大块连续空间。
5.1 分配时拆分:避免大块被小请求浪费
假设当前只有一个 32KB 空闲块:
1 | [ 32KB ] |
现在程序申请 7KB。伙伴系统需要分配 8KB,但当前没有 8KB 块,于是它会拆分:
1 | [ 32KB ] |
然后分配一个 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 | 申请 17KB |
所以伙伴系统本质上是一个 tradeoff:
1 | 牺牲一定内部碎片 |
❗ 本节核心结论: 伙伴系统通过“分配时拆分、释放时合并”在分配速度、连续空间恢复和内部碎片之间取得平衡。
6. 链表数组:伙伴系统如何组织空闲块
伙伴系统通常使用“链表数组”管理空闲块。
所谓链表数组,就是:
1 | 一个数组,数组的每个元素是一条链表。 |
假设页大小为 4KB:
1 | free_area[0]:管理 1 页块,也就是 4KB |
可以表示为:
1 | free_area[0] -> 4KB块 -> 4KB块 -> NULL |
6.1 为什么用“数组 + 链表”
数组的作用是快速定位大小。
例如申请 8KB,系统直接看:
1 | free_area[1] |
如果 free_area[1] 有空闲块,直接取一个。
如果没有,就向更高阶查找:
1 | free_area[2]:16KB |
找到更大块后再拆分。
链表的作用是保存多个同大小的空闲块。同一种大小可能有很多块,链表适合把它们串起来。
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 | [16KB] |
其中一个分配出去,另一个挂回 free_area[1]。
6.3 释放 8KB 的例子
释放一个 8KB 块时,系统会:
- 计算它的 8KB 伙伴地址;
- 检查伙伴是否空闲;
- 如果伙伴空闲,把两个 8KB 合并成 16KB;
- 继续检查 16KB 块的伙伴;
- 能合并就继续向上合并;
- 不能合并时,把最终块挂到对应
free_area[k]链表中。
这个过程让伙伴系统可以在释放时尽量恢复大块连续空间。
flowchart TD
A[释放 8KB 块] --> B[计算 8KB 伙伴地址]
B --> C{伙伴是否空闲?}
C -- 否 --> D[挂入 free_area[1]]
C -- 是 --> E[合并为 16KB]
E --> F[计算 16KB 伙伴地址]
F --> G{伙伴是否空闲?}
G -- 否 --> H[挂入 free_area[2]]
G -- 是 --> I[继续向上合并]
这张图强调:伙伴系统释放时不是简单“放回链表”,而是先尽量合并,直到无法继续合并为止。
❗ 本节核心结论: 伙伴系统的链表数组用数组区分块大小,用链表保存同大小空闲块,从而支持快速查找、拆分和合并。
7. 位图和伙伴系统不是上下级关系
位图和伙伴系统容易被误认为是上下级关系,但更准确的说法是:它们是同一层的两种页级物理内存分配方案。
它们都解决类似问题:
1 | 如何记录空闲物理页? |
区别在于实现方式不同。
| 对比项 | 位图分配器 | 伙伴系统 |
|---|---|---|
| 管理单位 | 单个物理页 | 2^n 个连续页组成的块 |
| 空闲状态记录 | 每页一个 bit | 每阶一条空闲链表 |
| 分配方式 | 扫描 bitmap 找连续 0 | 按阶查链表,不够则拆大块 |
| 释放方式 | 对应 bit 置 0 | 尝试与伙伴合并 |
| 优点 | 简单、省空间 | 更适合连续页分配和回收 |
| 缺点 | 找连续页可能慢 | 有内部碎片,结构更复杂 |
真正明显的上下级关系是:
1 | 物理内存 |
也就是说:
- 位图和伙伴系统是同层替代方案;
- 伙伴系统和 SLAB/SLUB 是上下级关系。
❗ 本节核心结论: 位图和伙伴系统都是页级物理内存分配方法,不是上下级;伙伴系统与 SLAB/SLUB 才是上下级关系。
8. 为什么伙伴系统之上还需要 SLAB / SLUB
伙伴系统已经能分配物理页,为什么还需要 SLAB 或 SLUB?
原因是:伙伴系统的最小单位通常是一个物理页,而内核经常需要分配远小于一页的小对象。
例如内核中经常分配:
struct task_structstruct filestruct inodestruct vm_area_struct- 各类缓存对象和描述符
这些对象可能只有几十字节或几百字节。如果每次都从伙伴系统分配一个 4KB 页,会造成严重内部碎片。
例如申请 128 字节对象:
1 | 伙伴系统最小分配 4KB = 4096B |
因此需要在伙伴系统之上再建立一层小对象分配器。
SLAB/SLUB 的基本思想是:
1 | 先从伙伴系统拿一批页 |
例如从伙伴系统拿到 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 | kmem_cache: 128B 对象池 |
如果内核申请 100B,系统可能从 128B 对象池中分配一个对象。这样虽然仍有少量内部碎片,但比直接分配 4KB 页小得多。
9.1 SLUB 为什么快
SLUB 快的原因主要有两个:
- 复用对象池:不需要每次都向伙伴系统申请页。
- 固定大小对象:分配和释放可以通过简单链表操作完成。
分配 128B 对象时:
1 | 找到 128B kmem_cache |
释放时:
1 | 把对象插回所在 slab 的空闲对象链表 |
很多操作可以做到接近 O(1)。
❗ 本节核心结论: SLAB/SLUB 通过对象池复用固定大小小块,避免频繁调用伙伴系统,从而提高内核小对象分配速度。
10. SLAB/SLUB 中的链表结构
SLAB/SLUB 中也大量使用链表结构,而且至少有两层链表思想:
- slab 之间的链表;
- slab 内部空闲对象链表。
10.1 slab 之间的链表:current、partial、full
在 SLUB 的抽象模型中,一个对象缓存池可以维护三类 slab:
| 名称 | 含义 |
|---|---|
current |
当前优先使用的 slab |
partial |
还有空闲对象、但不是全空或全满的 slab 链表 |
full |
已经没有空闲对象的 slab 链表 |
可以表示为:
1 | current |
分配时:
1 | 优先从 current slab 分配对象 |
释放时:
1 | 对象回到所在 slab |
10.2 slab 内部的空闲对象链表
一个 slab 内部被切成许多固定大小对象。例如:
1 | slab A: |
其中部分对象已分配,部分对象空闲:
1 | [已用][空闲][已用][空闲][空闲][已用] |
这些空闲对象会被串成空闲链表:
1 | free_list |
分配对象时:
1 | obj = free_list.head |
释放对象时:
1 | obj->next = free_list.head |
这样分配和释放都很快。
10.3 空闲对象的 next 指针存在哪里
一个重要细节是:空闲对象本身已经不被使用,所以可以把它自身开头的几个字节拿来存 next 指针。
例如一个 128B 空闲对象可以看成:
1 | [ next 指针 | 剩余空间................ ] |
当对象被分配出去时,这 128B 全部交给使用者。当对象被释放回来后,它又可以重新作为链表节点,开头位置保存下一个空闲对象的地址。
因此,SLUB 不需要为每个空闲小对象额外分配独立链表节点。
10.4 SLUB 的整体结构
可以把 SLUB 的对象缓存池表示为:
1 | kmem_cache:128B 对象池 |
flowchart TD
KC[kmem_cache: 128B 对象池] --> CUR[current slab]
KC --> PAR[partial slab 链表]
KC --> FULL[full slab 链表]
CUR --> O1[已用对象]
CUR --> O2[空闲对象]
O2 --> O3[下一个空闲对象]
O3 --> O4[下一个空闲对象]
PAR --> S1[slab B]
S1 --> S2[slab C]
FULL --> F1[slab D]
F1 --> F2[slab E]
这张图强调: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... |
可以表示为:
flowchart TD
PM[物理内存] --> BUDDY[伙伴系统:页级连续块]
BUDDY --> SLUB[SLAB/SLUB:小对象分配器]
SLUB --> OBJ1[task_struct]
SLUB --> OBJ2[file]
SLUB --> OBJ3[inode]
SLUB --> OBJ4[vm_area_struct]
一个直观类比是:
1 | 伙伴系统像批发市场,按箱发货; |
如果内核要一个 128B 对象,不应该直接让伙伴系统给一整页,而应该从 SLUB 的 128B 对象池中取一个。
如果 SLUB 的对象池没有可用 slab,它才向伙伴系统申请新的页级大块。
❗ 本节核心结论: 伙伴系统给 SLAB/SLUB 提供页级大块;SLAB/SLUB 再把大块切成内核小对象。二者是上下级协作关系。
12. Linux 内存管理架构:用户态与内核态的分工
Linux 内存管理可以粗略分成用户态分配器和内核态分配器两层。
12.1 用户态:malloc/free 不是系统调用本身
用户程序通常使用:
1 | malloc(); |
但 malloc 和 free 通常不是系统调用本身,而是 C 库中的用户态分配器接口。
常见用户态分配器包括:
| 分配器 | 说明 |
|---|---|
ptmalloc |
glibc 默认分配器 |
jemalloc |
常见高性能内存分配器 |
tcmalloc |
Google 的高性能内存分配器 |
用户态分配器会先在自己的堆管理结构中找空间。如果不够,再通过系统调用向内核申请更多虚拟内存,例如:
brkmmap
12.2 内核态:真正管理物理内存资源
内核负责更底层的内存管理,包括:
- 虚拟内存区域管理;
- 缺页异常处理;
- 页表管理;
- 物理页分配器;
- SLAB/SLUB 小对象分配器;
- 页回收;
- OOM killer;
- 内存控制组等。
可以这样理解:
1 | 用户态 malloc/free |
❗ 本节核心结论: 用户态 malloc/free 管理的是用户进程视角下的内存块;内核负责虚拟地址区域、缺页异常、页表映射和真实物理页分配。
13. malloc(100) 背后可能发生什么
假设用户程序执行:
1 | char *p = malloc(100); |
这个调用背后可能经历:
1 | 用户程序调用 malloc(100) |
这里有一个关键点:
1 | malloc(100) 不一定立刻分配真实物理内存。 |
它可能只是让用户态分配器返回一段可用虚拟地址范围。真正的物理页可能要等第一次访问时才由缺页异常触发分配。
这与虚拟内存管理 II 中的延迟映射是一条连续逻辑:
1 | 先获得虚拟地址区域 |
❗ 本节核心结论: malloc(100) 和“立刻拿到 100 字节物理内存”不是一回事;真实物理页可能在首次访问时才由内核分配。
14. 一个完整例子:从物理页到内核小对象
假设内核需要频繁分配一个 128B 的结构体对象。
14.1 如果只用伙伴系统
如果每次都从伙伴系统申请:
1 | 申请 128B |
这对频繁出现的小对象非常低效。
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 | 物理内存 |
❗ 本节核心结论: 伙伴系统解决“从物理内存拿页”,SLUB 解决“把页切成小对象并高效复用”。
15. 易混点总结
15.1 物理内存管理不是完全独立于虚拟内存管理
物理内存管理的底层对象是物理页,但它经常与虚拟内存管理协作。
例如:
1 | mmap 申请虚拟地址区域 |
虚拟内存管理负责地址空间和映射,物理内存管理负责真实物理页资源。
15.2 分页机制缓解外部碎片,但不代表物理内存完全不需要连续页
分页让虚拟页可以映射到不连续的物理页,因此普通程序不一定需要连续物理内存。
但系统中仍有场景需要连续物理页,例如:
- 大页;
- 某些驱动;
- DMA;
- 连续内存分配需求。
因此,物理内存管理仍然要考虑连续页分配和外部碎片。
15.3 伙伴系统不是没有碎片
伙伴系统主要缓解外部碎片,但会产生内部碎片。
例如:
1 | 申请 17KB |
所以伙伴系统不是完美分配器,而是在效率和碎片之间做折中。
15.4 位图和伙伴系统不是上下级
位图和伙伴系统都是页级物理内存分配方案。它们是同层替代关系。
真正的上下级关系是:
1 | 伙伴系统 |
15.5 SLAB/SLUB 不是替代伙伴系统
SLAB/SLUB 依赖伙伴系统。它不能凭空产生物理内存,而是先从伙伴系统拿到页级大块,再切成小对象。
❗ 本节核心结论: 本讲最容易混淆的是“位图与 buddy 的同层关系”“buddy 与 SLUB 的上下级关系”“虚拟内存管理与物理页分配的协作关系”。
16. 复习清单
学完《物理内存管理 I》后,应能回答以下问题:
- 物理内存管理 I 和物理内存管理 II 的区别是什么?
- 什么是物理页?
- 物理内存分配器需要考虑哪些评价指标?
- 外部碎片和内部碎片有什么区别?
- 位图分配器如何记录物理页状态?
- 位图分配器如何分配连续物理页?
- 位图分配器的优点和缺点是什么?
- 伙伴系统为什么只分配
2^n页大小的块? - 伙伴系统中“分配时拆分”是什么意思?
- 伙伴系统中“释放时合并”是什么意思?
- 伙伴系统为什么能缓解外部碎片?
- 伙伴系统为什么会产生内部碎片?
- 什么是伙伴系统中的链表数组?
free_area[0]、free_area[1]、free_area[2]分别管理什么?- 位图和伙伴系统是上下级关系吗?
- 为什么伙伴系统之上还需要 SLAB/SLUB?
- 一个 slab 是什么?
- SLUB 中
current、partial、full分别表示什么? - slab 内部的空闲对象链表如何工作?
- 为什么空闲对象本身可以存放
next指针? - 伙伴系统和 SLUB 是什么关系?
- 用户态
malloc(100)是否一定立刻分配真实物理内存? malloc → brk/mmap → 缺页异常 → buddy → 页表映射这条链路如何理解?
结论
《物理内存管理 I》的核心是理解现代操作系统如何组织真实物理内存资源。
这一讲可以压缩为一句话:
位图用 bit 记录每个物理页是否空闲;伙伴系统用链表数组管理不同大小的连续页块,并通过拆分与合并提高连续页分配能力;SLAB/SLUB 建立在伙伴系统之上,把页级大块切成固定大小小对象,从而支持内核高效分配小内存对象。
真正掌握这一讲,需要形成三个层次直觉:
- 位图层面:物理内存可以被看成一组页,每页有空闲或已分配状态。
- 伙伴系统层面:连续物理页按
2^n块管理,分配时拆分,释放时合并。 - SLAB/SLUB 层面:内核小对象不能每次都直接拿一整页,而应该从对象池中快速分配。
因此,物理内存管理并不是单纯“找一块空内存”这么简单。它是一套围绕速度、碎片、连续性和对象复用构造出来的分层分配体系。




