3. 内存管理
3.1 内存管理的概念
3.1.1 内存管理的基本原理和要求
内存的基础知识
内存可存放数据,程序执行前需要先放到内存中才能被CPU处理,缓和CPU与硬盘之间的速度矛盾。
内存管理的概念
操作系统作为资源的管理者,需要管理那些?
- 负责内存空间的分配与回收。
- 提供某种技术从逻辑上对内存空间进行扩充(虚拟技术
- 提供地址转换功能,负责程序的逻辑地址 与物理地址的转换。

- 内存保护,保证各进程在各自存储空间内运行,互不干扰。
- 在CPU中设置一对上下限寄存器,存放进程的上,下限地址。进程的指令要访问某个地址时,CPU检查是否越界。
- 采取重定位寄存器(存放进程的起始物理地址 )和界地址寄存器(存放进程中最大的逻辑地址)

进程的内存映像

#define
不会单独分配空间,而是在编译的时候就将代码中的X替换成了1024了,它虽然放在紫色区域,但它和常量截然不同。
css
[ 进程 ](内存)
-----内核区:
----- 进程控制块PCB。
----- 内核栈,内核数据结构
-----用户区
-----用户栈:在函数大括号内定义的局部变量,函数调用时传入的参数。
-----共享库的存储映射区:被调用的库函数
-----堆:由malloc/free分配回收的数据
-----未初始化的数据段(BSS):未初始化的全局/静态变量
-----已初始化数据段(Data):已初始化的全局/静态变量
-----只读代码/数据:程序代码。由const关键字修饰的常变量
-----未使用区(预留空间)
3.1.2 连续分配管理方式
连续分配:指为用户进程分配的必须是一个连续的内存空间。
单一连续分配
内存被分为了系统区 和用户区 ,内存中只能有一道用户程序,用户程序独占整个用户区域空间。如下图中,只能有一个进程A
- 优点:实现简单,无外部碎片。
- 缺点:只能用于单用户,单任务的操作系统中,有内部碎片;存储器利用率低。
固定分区分配
为了能在内存中装入多道程序,且这些程序之间又不会相互干扰,于是将整个用户空间划分为若干个固定大小的分区。

- 分区大小相等:大小固定,不能满足不同大小的进程需求,缺乏灵活性。
- 分区大小不等:增加了灵活性,nice。
操作系统建立一个数据结构-----分区说明表,来实现各个分区的分配与回收,每个表项包括对应分区的大小,起始地址,状态(是否已分配)。
- 优点:实现简单,无外部碎片
- 缺点:当用户程序太大时候,可能所有分区都不满足,此时不得不采取覆盖技术来解决,但这又会降低性能,而且也会产生内部碎片,内存占用率低。
动态分区分配
这种分配方式不会预先划分内存分区,而是在进程装入内存时,根据进程的大小动态地建立分区。
操作系统用什么样的数据结构记录内存使用情况
- 空闲分区表
- 空闲分区链
当很多空闲分区都能满足需求时,应该选择哪个分区进行分配
当一个新作业装入内存时,需按照一定的动态分区分配算法,从空闲分区表和空闲分区链中选择一个。
如何进行分区的分配与回收
如果两个空闲分区相邻,需要进行合并。如果回收区前后都有,同样也得合并。
如果回收前后都没有空闲分区,就需要新增空闲分区。
各表的顺序不一致,具体要看动态分区分配算法来确定。
- 动态分区没有内部碎片,但是有外部碎片,有些空闲分区太小了,虽然加起来满足,但是无法连续,所以用不上。 (可以通过紧凑技术来解决碎片)

动态分区分配算法
首次适应算法(First Fit)
-
算法思想:每次从低地址开始查找,找到第一个能满足大小的空闲分区。
-
算法实现: 空闲分区以按地址依次递增,每次分配内存时顺序查找空闲分区链(分区表),找到大小能满足要求的第一个空闲分区。
最佳适应算法
-
**算法思想:**由于动态分区分配是一种连续分配方式,因此为了大进程到来能有连续的大片空间,可以尽可能多的留下大片的空闲区,优先使用小的空闲区。
-
算法实现:空闲分区按容量依次递增,每次分配内存时,顺序查找空闲分区表(链),找到大小能满足要求的第一个空闲分区。
-
缺点:会留下越来越多,很小的,难以利用的碎片,产生了很多外部碎片。
最坏适应算法
- 算法思想: 为了解决最佳适应算法产生的外部碎片,可以在每次分配优先使用最大的连续空闲区,这样分配后剩余的空闲就不会太小。
- 如何实现: 空闲分区按容量依次递减,每次分配内存时,顺序查找空闲分区表(链),找到大小能满足要求的第一个空闲分区。
- 缺点:每次都选择大分区分配,但是如果大分区用完,之后又有大进程来了,就没有内存分区可以用了。
邻近适应算法
- 算法思想: 首次适应算法每次都从链头开始查找,这也导致了低地址部分会出现很小的空闲分区,每次分配查找,都要经过这些分区,也增加查找的开销,如果可以每次都从上次查找结束的位置开始索引,就能解决上述问题。
- 算法实现: 空闲分区以地址递增排序(可排成一个循环队列)每次从上次查找结束的位置开始查找空闲分区表(链),找到一个合适的分区。

3.1.3 基本分页存储管理

将内存 空间分为一个个大小相等的分区 ,每个分区就是页框/页帧/物理块,页框号从0开始。
将进程的逻辑地址 空间也分为与页框大小相等的一个个部分,每个部分被称为页/页面页号也是从0开始。
操纵系统以页框为单位为各个进程分配 内存空间,进程的页面 与内存的页框 有一一对应的关系。
各个页面不必连续存放,可以放到不相邻的各个页框中。
页表
为了知道进程的每个页面在内存中哪里,操作系统为每一个进程创建了一种数据结构---页表,其存放在PCB(进程控制块)中。

快表
快表(又称联想寄存器 ),是一种访问速度比内存快很多的高速缓存 ,用来存放最近访问的页表项的副本 ,可以加速地址变换的速度,与此对应,内存中的页表常被称为慢表。



两级页表
单级页表存在的问题
- 页表必须连续存放,因此,当页表很大时,需要占用很多个连续的页框
- 没必要让整个页表常驻内存,因为进程在一段时间内可能只需要访问某几个特定的页面
我们可以为页表再次创建一个"页表",称为页目录表,外层页表,顶层页表。


对于问题2,我们可以在需要访问页面的时候再把页面调入内存(虚拟内存存储技术) 给一个页表项增加一个标志位,若访问的页面不在内存,则产生缺页中断,然后将页面从外存调入内存。
3.1.4 基本分段存储管理
进程的地址空间:按照程序自身的逻辑关系划分为若干个段 ,每个段都有一个段名(在低级语言中,程序使用段名来编程),每段从0开始编址。
内存分配规则:以段为单位进行分配,每个段在内存中占据连续空间 ,但各段之间可以不相邻。
段表
程序分多个段,各段离散的装入内存,需从物理内存中找到各个逻辑段存放的位置,因此,每个进程建立了一张段映射表。--- "段表"
- 每个段对应一个段表项,其中记录了该段在内存中的起始位置(又称基址)和段长。
- 各个段表项的长度是相同的,段号可以省略。

段表也可以引入快表机构,加快地址变换速度。
分页和分段的对比
页是信息的物理单位 。分页的主要目的是为了实现离散分配 ,提高内存利用率。分页仅仅是系统管理上的需要,完全是系统行为,对用户是不可以见的。
段是信息的逻辑单位 。分段的目的是更好的满足用户需求。一个段通常包含着一组属于一个逻辑块的信息。分段是对用户可见的,用户编程时需要显示给出段名。
页的大小固定且系统决定。
段的长度不固定,决定于用户编写的程序。
对比点 | 逻辑地址 | 物理地址 |
---|---|---|
谁生成 | CPU(程序) | 内存硬件 |
用户可见吗 | 用户/程序员可见 | 用户不可见 |
存在位置 | 进程虚拟地址空间 | 真实内存单元 |
转换关系 | 通过段表、页表、段页式映射 | 最终访问用物理地址 |
逻辑地址:
分页是由页号和页内偏移量组成。
分段是由段号和段内偏移量组成。
物理地址:
分页 的用户进程地址空间为一维 的,程序员只需要给出一个记忆符表示地址就好了。
分段 的用户进程地址空间为二维 的,需要标记段长度 ,和段地址。
分段 比分页 更容易实现信息的共享和保护,因为分段是按照逻辑划分,它共享和保护这段逻辑就好了,而分页很有可能出现整个页框中一半是需要保护的,另一半不需要保护,就会混乱。
3.1.5 段页式管理

段页式系统的逻辑地址由段号,页号,页内地址(页内偏移量)组成。
- 段号的位数决定了每个进程最多可以分几个段
- 页号位数决定了每个段最大有多少页
- 页内偏移量决定了页面大小,内存块大小是多少。
每个段对应一个段表项,每个段表项由段号 ,页表长度 ,页表存放块号(页表起始地址),注意每个段表长度相等,段号是隐含的。
每个页面对应一个页表项,里面含有页号,页面存放的内存块,每个页表项长度相同,页号也是隐含的。

特性 | 分页(Paging) | 分段(Segmentation) | 段页式(Segmentation with Paging) |
---|---|---|---|
划分依据 | 按固定大小的页划分逻辑空间与物理内存 | 按**逻辑模块(段)**划分 | 先按逻辑段 划分,再在段内按页划分 |
优点 | 1. 消除外部碎片2. 支持虚拟内存,分配简单 | 1. 符合程序逻辑 ,支持共享与保护 | 1. 结合两者优点:既有逻辑保护/共享 ,又消除外部碎片 |
缺点 | 1. 产生内部碎片 2. 共享/保护不灵活 | 1. 产生外部碎片2. 段长不固定,管理复杂 | 1. 地址转换复杂(需两级查表)2. 硬件开销大 |
透明性 | 对程序员透明 | 对程序员可见 | 对程序员部分可见(逻辑上按段,物理上按页) |
地址结构 | [页号, 页内偏移] | [段号, 段内偏移] | [段号, 页号, 页内偏移] |
典型用途 | 现代OS的虚拟内存实现 | 提供模块级共享/保护 | 现代主流处理器(x86)使用,如段页结合管理内存 |
3.2 虚拟内存管理
3.2.1 虚拟内存的基本概念
传统存储管理 有连续分配 和非连续分配,很多用不到的数据也会长期占用内存,导致内存利用率不高。
- 一次性:作业必须一次性全部装入内存后才能开始运行。如果作业很大,无法装入内存,则导致大作业无法运行。
- **驻留性:**一旦作业装入内存,就会一直留在内存中,直到作业结束,但事实上并不是进程中所有数据都要同时访问,有时候只需要一部分就行了,这就导致了浪费内存资源。
那上述的缺点,就可以用虚拟存储技术来解决。
在程序装入时,可以将程序中很快会用到的部分装入内存,暂时不用的部分留在外存。在执行过程中,所访问的信息不存在 ,再由操作系统将所需信息从外存调入内存 ,若内存空间不足 ,则由操作系统负责将内存中暂时用不到的换出到外存 ,在用户看来似乎比实际内存大的多,这就是虚拟内存,所以它具有以下三个特征:
- 多次性:作业无需一次性全部装入内存,可多次调入内存。
- 对换性:允许在作业运行过程中,将作业换入,换出。
- 虚拟性:从逻辑上扩充了内存,使用户看到的内存容量,远大于实际容量。
虚拟内存的实现:
- 请求分页存储管理
- 请求分段存储管理
- 请求段页式存储管理
3.2.2 请求分页管理方式
- 请求调页:当所需数据内存中不存在时,由操作系统从外存调入内存。
- 页面置换:当内存中空间不足时候,由操作系统将内存中暂时不用的页面换出外存。
这俩功能的实现,也需要一个数据结构来存储,在基本分页存储中有页表,在请求分页管理系统中也要有页表。


3.2.3 页面置换算法
当内存中满了,需要新加页面时,就需要用到页面置换算法 ,将内存中暂时不用的信息换出到外存。页面的换入,换出需要磁盘I/O,会有较大的开销,因此,应该追求更少的缺页率。
最佳置换算法(OPT)
每次选择淘汰的页面都是以后永不使用 ,或者在最长时间内不再被访问的页面 ,这样可以保证最低的缺页率,但是操作系统无法预知未来啊,所以这个算法是实现不了的,只能模拟一下。

先进先出置换算法(FIFO)
每次选择淘汰 的页面是最早进入内存的页面。
实现方式:把调入内存的页面根据调入的先后顺序排成一个队列 ,需要换出页面时选择对头页面即可,队列长度取决于系统为进程分配了多少个内存块。

上图可以发现,为进程分配的物理块数增大时,缺页次数不减反增,这就是Belady异常所以它的性能很差。
最近最久未使用置换算法(LRU)
每次淘汰 的页面是最近最久未使用的页面。
实现方法:赋予每个页面对应的页表项,用访问字段记录该页面自上次被访问以来所经历的时间t ,当需要淘汰页面时,选择有页面t值最大的 ,即最久未使用的页面。

这个算法性能好,但实现困难,开销大,需要专门的硬件支持。
时钟置换算法(CLOCK)
这是一种性能和开销比较均衡的算法,又称CLOCK算法或最近未用算法。
简单的CLOCK算法实现 :在页面设置一个访问位 ,将内存中的页面链接成一个循环队列,当某页被访问时,设置为1,当需要页面置换的时候,就去队列中找,如果是0就换出,是1就设置为0暂不换出,这样子即使全为1,下一轮也有页面可换,所以这个算法最多会进行两次扫描。


简单时钟置换算法仅考虑到一个页面最近是否被访问过,而如果页面没有被修改,就不需要执行I/O操作写回外存。只有淘汰的页面被修改过 ,才需要写回外存。所以条件相同的时,应优先淘汰没有修改过的页面 ,避免I/O操作,这就是改进时钟置换算法的思想。修改位 = 1 / 0 表示修改/未修改。
(访问位,修改位)--->(1,1)表示近期访问又被修改。
算法实现:将所有可能被置换的页面排成一个循环队列,然后最多有四轮扫描,每轮失败则进行下一轮。
- 第一轮:寻找(0,0)
最近没有访问,没有修改的
不修改任何标志位,找到后进行置换。 - 第二轮:寻找(0,1)
最近没有访问,但修改过的
将访问位设置为0,若找到则进行替换。 - 第三轮:寻找(0,0)
最近访问过,没有修改
不修改任何标志位,找到后进行置换。 - 第四轮:寻找(0,1)
最近访问过,并且修改过的
肯定会找的,然后进行置换。

3.2.4 页框分配
**驻留集:**指请求分页存储管理中给进程分配的物理块的集合。在采用虚拟存储技术的系统中,驻留集大小比进程大小要小。
驻留集太大,会导致多道程序并发度下降。驻留集太小,会导致缺页频繁。
内存分配策略
固定分配:操作系统为每个进程分配固定数目的物理块,在进程运行期间不可改变,即驻留集大小不变。
可变分配:先为每一个进程分配一定数目的物理块,在进程运行期间根据情况增加或减少,即驻留集大小可变。
局部置换:发生缺页时只能选进程自己的物理块进行置换。
全局置换:可以将操作系统保留的空闲物理块 或者别的进程的物理块置换到外存,再分配给缺页进程。
上面可以搭配为,固定分配局部置换 ,可变分配局部置换 ,可变分配全局置换。
策略类型 | 页框分配是否可变 | 置换范围 | 优点 | 缺点 |
---|---|---|---|---|
固定分配局部置换 | 否 | 本进程内部 | 独立性强,互不影响 | 资源利用率低 |
可变分配局部置换 | 是 | 本进程内部 | 灵活,效率较高 | 开销较大 |
可变分配全局置换 | 是 | 系统全局 | 利用率最高 | 进程互相影响 |
何时调入页面:
- 预调页策略:主要用于进程的首次调入,由程序员指出应该调入哪些部分。
- 请求调页策略:进程在每次发现缺页的时候才将所缺的页面调入内存。
何处调入页面:
- 系统拥有足够对换区,页面的调入调出都是从对换区进行,速度快。
- 系统缺少足够的对换区,凡是不会修改的数据,直接从文件区调入内存,对于可能修改的数据,进入对换区。
- UNIX方式,运行进程之前,进程有关的数据全部放在文件区,页面第一次使用,是从文件区调入内存,如果置换的话,是换出到对换区。
刚刚换出页面又要换入内存,马上又要换出内存,这种频繁的页面调度就是抖动现象,主要原因就是分配给进程的物理块不够。
3.2.5 内存映射文件
内存映射文件 --- 操作系统像上层程序员提供的功能(系统调用)
内存映射文件 是 将磁盘文件的一部分或全部映射到进程的虚拟内存空间 ,从而允许进程像访问内存一样直接读写文件内容的一种技术。
- 方便程序员访问文件数据。
- 方便多个进程共享同一个文件。
