本章了解操作系统是如何管理内存的。首先要知道计算机的体系结构和内存分层体系,才能了解计算机是怎么有效管理内存的。
1.计算机体系结构/内存分层体系
计算机体系结构
计算机体系结构主要包括三大部分
- CPU:主要完成对程序执行的一个控制。
- 内存:主要放置了代码以及相关的数据。
- I/O设备:比如硬盘、键盘、鼠标等都属于I/O设备,它们各自有不同的功能,来配合程序发挥更大的作用。
内存分层体系
计算机的内存层次可以看到从上到下包括很多个类别。第一个是寄存器,第二个是cache(缓存),这两部分都是位于CPU内部的,特点是速度快但是容量小,能放置的数据是有限的。
为此,在操作系统里面还有一块很大的数据叫做主存,或者叫物理内存。主存存放运行的程序,容量大,但是相较于前两者速度较慢。
主存里面可能放置了多个程序同时运行,有可能出现对内存需求比较大,就将内存中放不下的数据放到硬盘(外存)中去。利用操作系统的帮助,其实可以实现不用的数据暂时先放在硬盘上,等用到的时候再加载进内存,这样虽然慢一些,但可以满足内存的需求,这就是后面会提到的虚拟内存机制。当然内存的数据会在掉电后消失,也需要保存到外存。
这样来看,计算机内存层次呈“金字塔”结构。
操作系统在内存管理中的目标
操作系统在内存管理中有以下4个目标:
- 抽象:希望程序通过操作系统的帮助,可以不用太过关注底层的细节。它不用考虑物理内存在什么地方,不用考虑外设在什么地方,它只用访问一段操作系统告诉它的连续地址空间就可以了,我们把这段连续地址空间称为逻辑地址空间。
- 保护:程序在执行的过程中有可能去访问别的程序的地址空间,我们希望通过操作系统保护每个程序独立的地址空间,防止恶意程序的破坏。
- 共享:保护程序独立地址空间并不意味着完全隔离,不同的程序之间可能会有交互,那就需要操作系统提供一个共享地址空间,来使得程序之间能够安全、可靠地交互。
- 虚拟化:当在内存中放了很多程序之后,可能会出现内存不够的情况,通过虚拟内存的机制将最需要的数据放在内存中,将暂时不需要的数据放在外存中,通过这种方式可以有效地实现一个虚拟的更大的地址空间。如上图所示。
为了实现这些目标,操作系统采用了一下方法:
- 程序重定位
- 分段
- 分页
- 虚拟内存
- 按需分页虚拟内存
这些方法会在后面介绍。
2.地址空间&地址生成
在介绍操作系统管理内存的方法之前,先要了解什么是地址空间以及地址空间是怎么生成的。
地址空间定义
一般地址空间分为两种,物理地址空间和逻辑地址空间。
物理地址空间是硬件支持的地址空间,是实际落在硬盘和内存上的地址空间,而逻辑地址空间是在CPU运行时程序看到的一段地址空间。逻辑地址空间看起来是一段简单的一维地址,其实它只是为了方便用户程序而存在的,到了真正访问的时候还会通过一定的对应关系映射到实际的物理地址空间上去。如上图例子所示。
逻辑地址生成
逻辑地址是为了方便用户程序使用而存在的,那它是如何生成的呢?
程序看到的是一个符号的逻辑地址(变量),这个符号逻辑地址通过编译、汇编、链接、程序加载最终生成一个可以在内存中运行的逻辑地址,这一系列步骤不需要操作系统的帮助,借助应用程序、编译器以及加载器(loader)就可以完成。
物理地址生成
这个图可以看到,应用程序在访问一个指令的时候,这个指令所处的逻辑地址是如何对应到内存中的物理地址上。CPU取出指令执行时,需要知道指令的物理地址,CPU刚开始只知道指令的逻辑地址,需要查找逻辑地址和物理地址的映射关系。CPU中有个模块叫MMU(内存管理单元),MMU保存了这种映射关系的表,CPU可以查表完成映射。
整个映射流程可以描述成以下步骤:
- CPU执行指令,CPU中的ALU(计算逻辑单元)部件需要知道这条指令的内容,它带着逻辑地址这个参数发出映射请求。
- CPU中的MMU回去查找表中是否有对应的物理地址,如果没有,就去内存中找,如果找到的话,CPU控制器会对内存发出请求获取某一个物理地址的内容,这个内容其实就是指令的内容。
- 主存将物理地址的内容通过总线返回给CPU,CPU拿到内容后就能开始指令的执行。
这个流程里操作系统的作用在于提前把逻辑地址和物理地址的映射关系建立好。另一方面,操作系统也保证了程序访问的地址空间是合法、安全的。
MMU表中映射的map会告诉每个程序能够合法访问的地址空间,如果越界访问会被操作系统拒绝 。
3.连续内存分配
连续内存分配定义是给进程分配一块不小于指定大小的连续物理内存区域。随之而来的问题就是内部碎片。
内存碎片是不能被利用的空闲内存,分为外部碎片和内部碎片两种。外部碎片是分配单元之间未被使用的内存,比如进程P1和P2之间蓝色区域;内部碎片是分配单元内部未被使用的内存,比如进程P2没有用到的分配给它的空闲空间。
当程序被加载执行时,操作系统给一个进程分配指定大小的分区(块、内存块),分区的地址是连续的。操作系统如何分配?涉及到动态分区的分配策略,主要有以下三种:
- 最先匹配(first-fit)
- 最佳匹配(best-fit)
- 最差匹配(worst-fit)
最先匹配(First Fit Allocation)策略
思路:分配n个字节,使用第一个可用的空间比n大的空闲块。
示例:分配400字节,使用第一个1KB的空闲块。
原理&实现:
实现简单
需求:
- 按地址排序的空闲块列表。
- 分配需要找一个合适的分区。
- 重分配需要检查,看是否有自由分区能够合并,并与相邻的空闲分区合并。
优点:
- 简单
- 易于产生更大的空闲块,向着地址空间的尾部
劣势:
- 外部碎片
- 不确定性
最佳匹配(Best Fit Allocation)策略
思路:分配n个字节,使用最小的可用空闲块,已知块的尺寸比n大。
实例:分配400字节,使用第三个500Byte的空闲块(最小)。
原理&实现:
为了避免分割大空闲块
为了最小化外部空间产生的尺寸
需求:
- 按尺寸排列的空闲块列表。
- 分配需要找一个合适的分区。
- 重分配需要检查,看是否有自由分区能够合并,并与相邻的空闲分区合并。
优点:
- 当大部分分配是小尺寸时非常有效
- 比较简单
劣势:
- 外部碎片
- 重分配慢
- 易产生很多没用的微小碎片(不怎么好)
最差匹配(Worst Fit Allocation)策略
思路:分配n个字节,使用最大的可用空闲块,已知块的尺寸比n大。
实例:分配400字节,使用第二个2KB的空闲块(最大)。
原理&实现:
为了避免有太多微小碎片
需求:
- 按尺寸排列的空闲块列表。
- 分配很快(获得最大的分区)
- 重分配需要检查,看是否有自由分区能够合并,并与相邻的空闲分区合并,然后调整空闲块列表。
优点:
- 假如分配是中等尺寸效果最好
劣势:
- 外部碎片
- 重分配慢
- 易于破碎大的空闲块以致大分区无法被分配
这三种分配策略没有说哪一种能满足所有需求,这些都是动态分配最基础的分配策略,后面会介绍其它复杂的分配策略。
4.碎片整理
我们看到无论采用哪种分配策略,都会多多少少产生内部碎片和外部碎片,那我们希望碎片少一点,后续能够有大块、连续的内存空间给其他进程使用,就需要操作系统在回收内存的时候对内存空间进行碎片整理。
压缩式碎片处理
重置程序以合并孔洞
要求所有程序都是动态可重置的
比如说左边内存空间有五个内存块空闲,那么可以通过内存拷贝的方式变成右边的样子,出现一个连续的较大空间。
议题:
- 何时重置?首先思考程序正在运行的时候能做拷贝工作吗?运行时做拷贝工作会导致访问地址出错。在程序停止(等待)的时候能做拷贝工作吗?这时候是可以的,但要考虑开销大不大。
- 开销:把程序从一个地方拷贝到另一个地方开销大不大?也许一次拷贝很快,但假如频繁执行拷贝工作,开销还是很大的。
交换式碎片处理
运行程序需要更多的内存:有时候一个内存空间可能通过压缩式碎片处理有一个五个内存块的空闲空间,但是一个新进程需要6个内存块,这时候就需要交换式碎片处理解决问题。
抢占等待的程序&回收它们的内存
比如现在P3运行时需要内存,发现P4处于等待状态且等待时间比较长,可以把P4的数据先放到磁盘上去,
议题:
- 选择哪些程序交换?
- 在什么时候做换入或者换出的操作?
- 开销:换入换出的粒度一般是以单个进程大小作为一个粒度,那么大程序换入换出的开销也比较大。
这些换入换出的议题会在学习虚拟内存的时候继续讨论。