操作系统内存管理方式:从连续分配到虚拟内存
操作系统内存管理方式:从连续分配到虚拟内存
内存管理是操作系统里非常核心的一部分。程序运行时,指令和数据都要放到内存中,CPU 才能不断取指、执行、读写数据。如果没有操作系统的管理,多个进程会很容易互相覆盖内存,程序也必须直接面对复杂的物理地址。
所以,内存管理要解决的不只是“把内存分给程序”这么简单。它还要负责地址转换、空间隔离、内存保护、碎片控制、共享、换入换出,以及在物理内存有限的情况下让程序感觉自己拥有一片连续且足够大的地址空间。
这篇文章按内存管理方式的演进顺序梳理:连续分配、分页、分段、段页式管理和虚拟内存。
一、为什么需要内存管理
从程序的角度看,它希望内存最好具备几个特点:
- 地址从 0 开始,空间连续,访问简单;
- 自己的数据不会被其他程序随便修改;
- 需要更多空间时,系统能够尽量满足;
- 代码、库、共享数据可以被合理复用;
- 程序不用关心真实物理内存到底长什么样。
但从操作系统的角度看,真实情况要复杂得多:
- 物理内存容量有限;
- 多个进程要同时运行;
- 进程大小不同,生命周期不同;
- 进程可能动态申请和释放内存;
- 有些页面暂时不用,可以先放到磁盘;
- CPU 访问内存时还要保证速度,不能每次地址转换都太慢。
因此,内存管理的本质是:在有限的物理内存上,为多个进程提供稳定、安全、尽量高效的地址空间抽象。
二、几个基础概念
在讨论具体方式之前,先把几个常见概念理清楚。
逻辑地址是程序看到的地址,也叫虚拟地址。程序中的变量、函数、数组访问,通常都基于逻辑地址。
物理地址是真实内存条上的地址。CPU 最终访问内存时,必须得到物理地址。
地址重定位指的是把程序中的逻辑地址转换成实际物理地址。这个过程可以在编译、装入或运行时完成。现代操作系统主要依赖运行时地址转换,也就是由硬件 MMU 和操作系统配合完成。
地址空间是进程能够访问的地址范围。每个进程通常都有独立的虚拟地址空间,这样进程之间可以互相隔离。
内碎片是已经分配出去但没有被充分利用的空间。例如系统按固定大小分配内存,一个进程实际只用了其中一部分,剩下的小块空间也不能再分给别人。
外碎片是空闲内存总量足够,但分散成了很多不连续的小块,导致无法满足一个较大的连续内存申请。
换入换出是指把暂时不用的内存内容写到磁盘,需要时再读回内存。它是虚拟内存能够工作的基础之一。
三、连续内存分配方式
连续分配是比较早期、也比较直观的内存管理方式。它的核心思想是:给一个进程分配一段连续的物理内存。
1. 单一连续分配
单一连续分配通常用于非常简单的系统。内存被分成两部分:
- 一部分给操作系统;
- 另一部分给一个用户程序。
这种方式实现简单,但问题也明显:一次只能运行一个用户程序,内存利用率低,也缺乏多进程并发能力。
2. 固定分区分配
固定分区分配会把内存提前划分成若干个固定大小的分区,每个分区装入一个进程。
它相比单一连续分配支持了多道程序,但也带来了内碎片问题。比如一个分区大小是 100 MB,某个进程只需要 60 MB,剩下 40 MB 虽然空着,但也不能再分给其他进程。
固定分区的另一个问题是灵活性差。如果进程比所有分区都大,就无法装入;如果分区设计得太大,又会浪费空间。
3. 动态分区分配
动态分区分配不再提前固定分区大小,而是在进程装入时,根据进程实际需要动态划出一块连续空间。
常见分配算法包括:
首次适应:从空闲分区表开头开始找,找到第一个足够大的分区就分配。它速度较快,但容易在低地址部分留下很多小碎片。
最佳适应:每次选择能够满足需求的最小空闲分区。它看起来最节省空间,但容易留下很多特别小、难以再次利用的碎片。
最坏适应:每次选择最大的空闲分区。它希望切分之后剩下的空间仍然可用,但可能很快破坏大块空闲区。
邻近适应:从上次查找结束的位置继续向后查找,避免每次都从头开始扫描。
动态分区的主要问题是外碎片。内存中可能存在很多空闲块,总空闲空间不小,但它们不连续,无法分给需要大块连续空间的进程。
解决外碎片的一种方法是紧凑,也就是移动进程位置,把分散的空闲空间合并成一整块。但紧凑需要移动内存内容,并修改地址映射,开销较大。
四、分页存储管理
分页的核心思想是:不要求进程在物理内存中连续存放。
操作系统把进程的逻辑地址空间划分成固定大小的页,把物理内存划分成同样大小的页框。进程的每一页可以放到任意一个空闲页框中。
1 | 逻辑地址空间:页 0 | 页 1 | 页 2 | 页 3 |
这样,进程在逻辑上仍然看到连续地址,但在物理内存中可以离散存放。
1. 页表与地址转换
分页系统中,每个进程都有自己的页表。页表记录逻辑页号到物理页框号的映射关系。
逻辑地址通常被拆成两部分:
1 | 逻辑地址 = 页号 + 页内偏移 |
地址转换过程可以理解为:
1 | 页号 -> 查页表 -> 得到页框号 |
用一个简单流程表示:
1 | flowchart LR |
分页最大的优点是消除了外碎片,因为物理内存可以按页框离散分配。但它仍然可能产生内碎片。比如页大小是 4 KB,进程最后一页只用了 1 KB,剩下 3 KB 就浪费了。
2. TLB 的作用
如果每次访问内存都要先查页表,再访问真实数据,就会让一次内存访问变成至少两次内存访问,性能损耗很明显。
为了解决这个问题,CPU 中通常会有 TLB,也叫快表。TLB 是页表项的高速缓存,保存最近使用过的页号到页框号映射。
访问流程大致是:
- 先查 TLB;
- 如果命中,直接得到页框号;
- 如果未命中,再查内存中的页表;
- 查到后把结果写入 TLB,方便下次使用。
TLB 能发挥作用,是因为程序访问内存通常具有局部性:最近访问过的地址,接下来很可能还会访问。
3. 多级页表
如果进程虚拟地址空间很大,单级页表可能非常庞大。很多地址范围实际上没有使用,但页表仍然可能要为它们预留表项。
多级页表的思想是把页表也分层。只有某个虚拟地址范围真的被使用时,才为它创建下一级页表。
这样可以减少页表本身占用的内存,但地址转换时可能需要多次查表。因此现代系统通常依赖 TLB 来缓解多级页表带来的访问开销。
五、分段存储管理
分页是从物理内存管理角度出发,把地址空间切成大小固定的页。分段则更贴近程序的逻辑结构。
一个程序可以自然地分成多个段:
- 代码段;
- 数据段;
- 堆段;
- 栈段;
- 共享库段。
分段系统中,逻辑地址通常由两部分组成:
1 | 逻辑地址 = 段号 + 段内偏移 |
每个进程有一张段表,段表项中通常包含:
- 段基址:该段在物理内存中的起始位置;
- 段长:该段的长度;
- 访问权限:例如可读、可写、可执行。
地址转换时,系统先根据段号找到段表项,再检查段内偏移是否超过段长。如果没有越界,就用段基址加上段内偏移得到物理地址。
分段的好处是符合程序员和编译器看到的逻辑结构,也方便做共享和保护。例如代码段可以设置为只读,多个进程也可以共享同一段库代码。
但分段的问题是段长不固定,物理内存仍然需要给每个段分配连续空间,所以它可能产生外碎片。
六、段页式存储管理
分页和分段各有优点:
- 分页方便物理内存分配,避免外碎片;
- 分段符合程序逻辑结构,方便共享和保护。
段页式管理就是把两者结合起来。它先把程序按逻辑划分成多个段,再把每个段内部划分成固定大小的页。
段页式地址通常可以拆成三部分:
1 | 逻辑地址 = 段号 + 段内页号 + 页内偏移 |
地址转换大致如下:
1 | 段号 -> 查段表 -> 找到该段对应的页表 |
段页式管理既保留了分段的逻辑结构,又通过分页避免了段必须连续存放的问题。代价是地址转换过程更复杂,需要段表和页表共同参与。
七、虚拟内存管理
虚拟内存是现代操作系统非常重要的机制。它的核心思想是:程序运行时,不必把整个进程一次性全部装入物理内存,而是只把当前需要的部分放入内存,其余部分暂时放在磁盘上。
虚拟内存依赖一个重要经验:局部性原理。
局部性包括两种:
- 时间局部性:刚访问过的数据,很可能很快再次访问;
- 空间局部性:访问某个地址后,很可能访问它附近的地址。
因为局部性存在,程序在某一段时间内真正频繁使用的页面通常只是整个地址空间的一小部分。
1. 请求分页
请求分页是虚拟内存最常见的实现方式。进程开始运行时,不需要把所有页面都装入内存。只有当程序访问某个尚未在内存中的页面时,才由操作系统把它从磁盘调入内存。
如果访问的页面不在内存中,就会触发缺页中断。
缺页中断的大致处理过程是:
- CPU 发现页表项无效,触发缺页中断;
- 操作系统接管,判断访问是否合法;
- 如果访问非法,终止进程或发送异常信号;
- 如果访问合法,从磁盘找到对应页面;
- 选择一个空闲页框,必要时淘汰某个已有页面;
- 把目标页面读入内存;
- 更新页表和 TLB;
- 重新执行刚才被中断的指令。
虚拟内存带来的好处很明显:
- 程序可以使用比物理内存更大的地址空间;
- 多个进程可以更高效地共享物理内存;
- 进程启动时不必一次性加载全部内容;
- 操作系统可以把暂时不用的页面换出到磁盘。
但它不是“无限内存”。如果频繁缺页,程序会大量等待磁盘 I/O,性能会急剧下降。
2. 页面置换算法
当内存中没有空闲页框时,操作系统需要选择一个页面换出,这就是页面置换。
常见算法包括:
OPT 最优置换算法:淘汰未来最长时间不会被访问的页面。它理论上最优,但需要知道未来访问序列,现实中无法真正实现,通常作为比较基准。
FIFO 先进先出算法:淘汰最早进入内存的页面。实现简单,但可能淘汰掉仍然频繁使用的页面,并且存在 Belady 异常,即页框数增加时缺页次数反而可能增加。
LRU 最近最少使用算法:淘汰最长时间没有被访问的页面。它利用了时间局部性,效果通常较好,但精确实现成本较高。
Clock 时钟算法:用访问位近似实现 LRU。它把页面组织成环形队列,通过访问位判断页面最近是否被使用过,在实现成本和效果之间取得折中。
3. 抖动现象
如果系统给进程分配的物理页框太少,进程的工作集放不下,就会频繁发生缺页。CPU 大量时间都花在等待页面换入换出上,而不是执行真正的程序逻辑,这种现象叫抖动。
抖动说明虚拟内存并不是没有代价的。操作系统需要控制多道程序数量,合理分配页框,并尽量让进程的工作集留在内存中。
八、几种内存管理方式对比
| 管理方式 | 基本思想 | 主要优点 | 主要问题 |
|---|---|---|---|
| 单一连续分配 | 一个用户程序占用一整块内存 | 实现简单 | 只能运行一个程序,利用率低 |
| 固定分区分配 | 内存提前划分为固定分区 | 支持多道程序,实现不复杂 | 内碎片明显,灵活性差 |
| 动态分区分配 | 按进程大小动态分配连续空间 | 比固定分区更灵活 | 会产生外碎片 |
| 分页管理 | 逻辑空间和物理内存都按固定大小分页 | 消除外碎片,分配灵活 | 有内碎片,页表管理有开销 |
| 分段管理 | 按程序逻辑结构划分内存 | 方便共享和保护,符合程序结构 | 会产生外碎片 |
| 段页式管理 | 先分段,再对段分页 | 兼顾逻辑结构和离散分配 | 地址转换复杂 |
| 虚拟内存 | 只把当前需要的页面放入内存 | 扩大地址空间,提高内存利用率 | 缺页和页面置换会带来开销 |
如果把这些方式连起来看,会发现内存管理的演进方向很清楚:
1 | 连续分配 -> 离散分配 -> 逻辑保护 -> 按需装入 -> 虚拟地址空间 |
早期方式更关注“如何把程序放进物理内存”,现代方式更关注“如何给进程提供一个稳定、安全、高效的虚拟地址空间”。
九、常见问题整理
1. 分页和分段有什么区别?
分页是系统管理物理内存的一种方式,页大小固定,对程序员通常不可见。分段是按照程序的逻辑结构划分地址空间,段大小不固定,更接近代码段、数据段、栈段这样的程序组织方式。
简单说:分页面向物理管理,分段面向逻辑结构。
2. 为什么分页会有内碎片,分段会有外碎片?
分页按固定大小分配。一个进程最后一页可能用不满,因此会产生内碎片。
分段按不同长度的段分配,每个段需要连续空间。随着段的创建和释放,空闲空间可能分散成很多小块,因此会产生外碎片。
3. TLB 为什么能加速地址转换?
页表通常在内存中。如果每次访问数据前都要查页表,访问成本会变高。TLB 缓存最近使用的页表项,命中时可以快速得到页框号,减少访问页表的次数。
TLB 能有效工作,是因为程序通常具有局部性。
4. 什么是缺页中断?
当进程访问的页面不在物理内存中时,硬件会触发缺页中断。操作系统随后判断访问是否合法,并在合法的情况下把页面从磁盘调入内存,然后恢复程序执行。
缺页中断不是普通函数调用,而是一次由硬件和操作系统共同处理的异常流程。
5. 虚拟内存是不是无限内存?
不是。虚拟内存只是让程序看到一个更大的虚拟地址空间,并允许操作系统把暂时不用的页面放到磁盘上。
它仍然受到磁盘空间、地址位数、页表规模和性能的限制。如果页面频繁换入换出,程序会变得非常慢。
6. 页面大小越大越好吗?
不一定。
页面大一些,页表项数量会减少,TLB 覆盖范围也可能变大。但页面太大,会导致更多内碎片,并且每次缺页读入的数据更多,可能浪费 I/O。
页面小一些,内碎片减少,但页表会变大,地址转换和管理成本也会上升。
页面大小本质上是在内存利用率、页表开销和 I/O 效率之间做权衡。
十、总结
操作系统内存管理方式可以看作一条逐步抽象的路线。
连续分配最直观,但容易浪费空间,也难以适应复杂的多进程环境。分页让物理内存可以离散分配,解决了外碎片问题。分段让地址空间更贴近程序逻辑,方便共享和保护。段页式管理结合了分页和分段的优点。虚拟内存进一步把“进程看到的地址空间”和“真实物理内存”分离开,让程序可以按需装入、独立运行,并获得更好的隔离性。
理解这些方式时,可以抓住三条主线:
- 内存是否要求连续;
- 地址如何从逻辑地址转换为物理地址;
- 碎片、保护、共享和性能之间如何权衡。
内存管理并不是单纯的数据结构问题,它背后体现的是操作系统最重要的能力之一:在有限硬件资源上,构造出稳定、隔离、易用的运行环境。