现代操作系统:第三章 内存管理

操作系统的工作是将这个存储体系抽象成为一个有用的模型并将管理这个抽象模型

操作系统中管理分层存储体系的部分称为存储管理器。它的任务是有效的管理内存,即记录哪些内存是正在使用的,哪些内存是空闲的,在进程需要时为其分配内存,在进程使用完成的时候为其释放内存。

3.1 无存储器的抽象

最开始并没有对存储器进行抽象,直接简单粗暴的使用物理内存地址,直接从0到某个上限值。每个地址可容纳一定的二进制位存储单元, 通常为8位。这个时期的组织内存的三种方式如下:

在这里插入图片描述

即使没有存储器抽象,同时运行多个程序也是可能的。操作系统只需要把当前内存中所有内容保存到磁盘文件中,然后把下一个程序读入到内存中再运行即可。只要在某个时间内存中只有一个程序,那么冲突就不会发生。

3.2 一种存储抽象:地址空间

把物理地址直接暴露给进程会带来下面几个严重的问题:

  1. 如果用户程序可以寻址内存的每一个字节,它们就可以很容易的破坏操作系统,从而系统慢慢的停止运行。
  2. 使用这种模型,想要同时运行多个程序时很困难的。

3.2.1 地址空间的概念

地址空间是一个进程可以用于寻址的一套地址集合。每个进程都有一个自己的地址空间,并且这个地址空间独立于其他进程的地址空间。

为了把内部虚拟地址空间转换为真实物理地址,就需要动态重定位, 典型的办法就是给CPU配置两个特殊寄存器,即基址寄存器界限寄存器。 进程加载到内存后,基址寄存器用来记录进程物理内存的开始位置, 界限寄存器用来保存程序的长度(也可理解为记录进程物理内存的结束位置)。

如果计算机的物理内存足够大,可以保存所有的进程,那么之前提到的方案或多或少都是可行的,但是实际上,所有进程所需要的RAM数量总和通常远远超过存储器能够支持的范围。

这里有两种方法可以处理内存超载的调用方法:

  1. 交换技术:即把一个进程完整的调入内存,使该进程运行一段时间,然后把它存回到磁盘中。
  2. 虚拟内存技术:该策略使程序能有一部分被调用到内存的情况下运行。

在这里插入图片描述

内存紧缩:如上图, 运行时间长了,进程间的内存就可能有小块未使用的空闲区,把小块合成一大块连续内存,就叫内存紧缩。但是会耗费大量CPU时间。

由于程序在运行的过程中,大部分进程的占用内存会在运行时不断增长的。一种可用的方法就是:当还如或者移动内存的时候为他分配一些额外的内存

在这里插入图片描述

供变量动态分配和释放作为堆使用的一个数据段。存放普通局部变量和返回地址的一个堆栈段。

3.2.3 空闲内存分配

在动态内存分配的时候,操作系统必须对其进行管理。操作系统一般有两种方法跟踪内存使用情况:位图和空闲区链表。

1. 使用位图的存储管理

使用位图时,内存被划分为几个或几千个字节的单元,即位图中的一位,0表示空闲,1表示占用。如下:

在这里插入图片描述

其中阴影区表示空闲内存,图b 是一张位图。图a 一共有A/B/C/D/E 五个进程在运行。每个单元的大小很重要,单元越小,则位图越大, 单元越大,则位图越小。

2.使用链表的存储管理

上图c 则是用链表对内存进行管理,内存被划为很多内存段(每一段不不一定相等),连接起来就是一张链表,一共都四个域:1.指示标志(P表示空闲,H表示进程占用), 2、起始地址; 3.长度;4、指向下一个节点的指针。如下图:

在这里插入图片描述

刚开始时 A,X,B三个进程在运行,内存被划为为三段,如果X终止了,则内存还是三段,只是X原本占用的这一段就变为了空闲区(如图a)。如果进程X,B都终止了,则内存变为了两段(如图b),如果A,X,B全部终止,则进程就变为了一段,如图d.相对应的,在链表中的三个内存节点就变成了一个新的节点,老的三个节点被删除掉。

当按照地址顺序在链表中存放进程和空闲区时,有几种算法可以为创建进程来分配内存:

  1. 首次适配算法。
  2. 下次适配算法。
  3. 最佳适配算法。
  4. 最差适配算法。
  5. 快速适配算法。

3.3 虚拟内存

为了解决程序大于内存本身的问题,提出了这么一种解决办法:把程序分割成很多片段,然后程序执行时,先执行第一块,然后执行第二块,这样,运行时,就只需要把程序的部分加载到内存即可,这个由操作系统动态的在内存和磁盘上换入换出,这个方法就就演变出了虚拟内存

虚拟内存的基本思想:每个程序有自己的地址空间,把这个空间分割为多块,每一块称为一页或页面(page),每一页都有连续的地址范围。这些页被映射到物理内存,但并不是所有的页都必须再内存中才能运行程序。当程序引用到物理内存的地址空间时,由硬件立刻执行必要的映射。当程序引用到一部分不在物理内存的地址空间时,由操作系统负责将缺失的部分装入物理内存并重新执行失败的命令。

3.3.1 分页

程序自己的地址称为虚拟地址,它们构成了虚拟地址空间,虚拟地址通过内存管理单元(Memory Management Unit, MMU) 映射到物理内存地址上,从而被执行。示例图如下:

在这里插入图片描述

虚拟地址按照固定大小划分为若干个页面, 在物理内存中对应的单元称为页框(page frame),它们大小通常是一样的。可以理解为每个页框可以容纳一个页面。 如上图3-9, 虚拟页面16个,但是实际物理地址只有8个页框,如果程序要访问一个没有映射到物理页框的页面,就会产生缺页中断或称为缺页错误,

在实际硬件中,用一个“在/不在” 位(present/absent bit)来记录页面在内存中的情况,如0表示不再位, 1表示在位。

MMU当注意到该页面没有被映射(在图中用叉号表示),于是CPU陷入到操作系统当中,这个陷阱称为缺页中断。操作系统就会把选择其中一个使用较少的页框,那上面的内容清除,随后把需要访问的页面读到刚才回收的页框中,修改映射关系,然后重新启动引起陷阱的指令。

在这里插入图片描述

在有16个虚拟页面的情况下, 页表由16个虚拟页面组成,假如现在要把一个页面加载进物理内存, 高四位代表在页表的位置, 0010换成10进制就是2, 编号2的这个页面时在位的,并且高三位的物理地址就是110,再加上低12位,就组成了一个真实的物理地址 输出。 这就完成了16位虚拟地址到15位物理地址的转换。

3.3.2 页表

虚拟地址到物理地址的映射可以概括如下:虚拟地址被分为虚拟页号(高位部分)和偏移量(低位部分)。

虚拟地址到物理地址的具体流程:虚拟页号也可以作为页表的索引,以找到该虚拟页面对应的页表项。由页表项可以找到页框号。然后把页框号拼接到偏移量的高位端,以替换掉虚拟页号,形成送往内存的物理地址。

页表的目的是把虚拟页面映射为页框。从数学的角度上将,页表是一个函数,它的参数是虚拟页号,结果是物理页框号,通过这个函数可以把虚拟地址中的虚拟页面域替换成页框域,从而形成物理地址。

页表项的结构

页框号、在不/在 位, 保护位,修改位,访问位,高速缓存禁止位。

  • 保护位:指一个页允许什么类型的访问, 读、写、可读写。R W X
  • 修改位:是否有修改过,修改过,则则需要先写入磁盘保存才能丢弃,如果没修改过,更换其他页面时,就可以直接丢弃了。
  • 访问位:是否正在访问、包裹读、写。
  • 高速缓存禁止位:禁止告诉缓存。

在这里插入图片描述

虚拟内存的本质上是用来创建一个新的抽象概念----地址空间:这个概念是对于物理内存的抽象,类似于进程是对物理处理器(CPU)的抽象。虚拟内存的实现,是将虚拟地址空间分解成页,并将每一页映射到物理内存的某个页框或者解除映射。

在任何分页系统中我们都需要考虑下面两个问题:

  • 虚拟地址映射到物理地址的映射必须要非常快
  • 如果虚拟地址空间很大,页表项也很大。

3.3.3 加速分页过程

1. 转换检测缓冲区

为了解决虚拟地址映射到物理地址的映射必须要非常快的问题,计算机可以设置一个小型的硬件设备,将虚拟地址直接映射到物理地址,而不必再访问页表。这种设备叫做转换检测缓冲区 TLB。

TLB的工作原理:将一个虚拟地址放入MMU进行转换时,硬件首先通过将该虚拟地址页号与TLB所有表项同时进行匹配,判断页面是否在其中。如果发现了一个有效的匹配并且要进行的操作并不违反保护位,则将页框号直接从TLB中取出而不再访问页表。

2. 软件管理TLB

当发生TLB访问失效的时候,将硬件管理TLB设备交给操作系统来管理,操作系统必须首先找到该页面,然后从TLB中删除一个项,接着装在一个新的项,最后执行出错前的指令。

  • 软失效:当一个页面访问在内存中,而不再TLB中,将产生软失效。
  • 硬失效:当一个页面本身不在内存中,则将产生硬失效。

3.3.4 针对大内存的页表

1.多级页表

第一种方法就是使用多级页表。在图中,32为的虚拟地址被划分为10位的PT1域和10位的PT2域和12位的Offset域。因为偏移量是12位,所以页面大小是4KB。

在这里插入图片描述

引入多级页表的原因是避免把全部页表一直保存在内存中。特别是那些不需要的页表就不应该保留。

2.倒排页表

针对页式调度层次不断增长的另一种解决方案是倒排页表。表项记录了哪一个(进程,虚拟页面)对定位于该页框。

在这里插入图片描述

3.4 页面置换算法

当缺页中断发生时,操作系统必须在内存中选择一个页面将其换出内存,以便为即将调入页面腾出空间.如果要换出的页面在内存驻留期间已经被修改过了,就必须把它写回到磁盘上的副本,如果该页面没有被修改(如果一个包含程序正文的页面),那么它在磁盘上的副本已经是最新的,不需要写回。直接用调入的页面覆盖被淘汰的页面就可以了。

在发送缺页中断时,需要选择一个页面,将其换出内存,应该选择哪一个页面呢,有如下算法:

  • 最优页面置换算法
  • 最近未使用页面算法
  • 先进先出页面置换算法
  • 第二次机会页面置换算法
  • 时钟页面置换算法
  • 最近最少使用页面置换算法
  • 工作集页面置换算法
  • 工作集时钟页面置换算法

针对他们的总结如下:

在这里插入图片描述

总结:

  1. 最优置换算法在当前页面中置换最后要访问的页面。不幸的是,没有办法来判断哪个页面是最后要访问的,因此实际上该算法不能使用。然而,它可以作为衡量其他算法的基准。
  2. NRU(最近未使用算法)算法根据R位和M位的状态把页面分成四类。从编号最小的类中随机选择一个页面置换,该算法容易实现,但是性能不是很好。
  3. FIFO(先进先出算法)通过维护一个页面的链表来记录他们装入内存的顺序。淘汰最老的页面,但是该页面可能仍在使用,因此FIFO算法不是一个最好的选择。
  4. 第二次机会算法是对FIFO算法的改进,他在移出页面前先检查该页面释放正在被使用。如果该页面正在被使用,就保留该页面。这个改进大大提高了性能。
  5. 时钟算法是第二次机会算法的另一种实现。它具有相同的性能特征,而且只需要更少的执行时间。
  6. LRU(最近最少未使用算法)是一种非常优秀的算法,但是只能通过特定的硬件实现。
  7. NFU是一种近似LRU的算法,但是它的性能不是很好
  8. 最后两种算法都使用了工作集。工作集算法有合理的性能,但它的实现开销过于巨大。总之最后两种算法是老化算法和工作集时钟算法,他们分别基于LRU和工作集,它们都具有良好的页面调度性能。

3.5 分页系统中存在的问题

3.5.1 局部分配策略和全局分配策略

如果页面置换算法只考虑当前进程所分配的页面,这种算法就称为局部页面置换算法。如果考虑所有在内存中的页面,那么这种算法就是全局页面置换算法。局部算法可以有效的位每个进程分配固定大小的内存片段。全局算法在可以运行的进程之间动态地分配页框。

全局算法在通常情况下比局部算法做的更好,当工作集的大小随进程运行时间发生变化时这种现象更加明显。若使用局部算法,即使有大量的空闲页框存在,工作集的增长也会导致颠簸。

在这里插入图片描述

3.5.2 负载控制

即使使用最优置换算法并对进程采用理想的全局分配,系统也可能发生颠簸。主要原因是,一旦所有进程的组合工作集超过了内存的容量,就可能发生颠簸。

减少竞争内存的进程数量是一个好方法将一部分进程交换到磁盘,并且释放他们所占有的所有页面。即使是使用分页,交换也是有需要的,只是现在的交换是用来减少对内存潜在的需求,而不是收回它的页面。

3.5.3 页面大小

页面大小是操作系统可以选择的一个参数。一般操作系统页面大小为4K。

随便选择一个程序正文段、数据段、堆栈段很可能不会恰好装满整个页面,平均的情况下,最后一个页面中有一半是空的。空余的空间就浪费掉了,这种浪费叫做内部碎片。

为了进行必要的平衡,操作系统有时候会为系统中的不同部分使用不同的页面大小。如,内核使用大的页面,而用户进程使用小的页面。

3.5.4 分配的指令空间和数据空间

大多数计算机的进程只有一个地址空间,既存放程序和正文。但是地址空间通常太小了,这就使得程序员对地址空间的使用出现困难。

在这里插入图片描述

解决方案:为指令(程序正文)和数据设置分离的地址空间,分别是I空间和D空间。

3.5.5 共享空间

在大型多道程序设计系统中,几个不同的用户同时运行一个程序是可见的。所以说共享页面效率最高,但是并不是所有的页面都适合共享。特别的那些只读的页面(诸如程序文本)可以共享,但是数据页面是不可以共享的。

如果系统支持分离的I空间和D空间,那么两个或者多个进程来共享程序就变得非常简单了,这些进程使用相同的I空间页表和不同的D空间页表。

写时复制

在这里插入图片描述

3.5.6 共享库

如果任何一个进程对一个数据页面进行修改,系统就会为该进程复制这个数据页面的副本,并且这个副本是进程私有的,也就是说会执行“写时复制”。

一个更加通用的技术就是使用使用共享库

3.5.7 内存映射文件

共享库实际上是一种更为通用的机制----内存映射文件的一个特例。内存映射文件的思想是:进程可以通过发起一个系统调用,将一个文件映射到其虚拟地址空间的一部分。

如果两个或者两个以上的进程同时映射了一个文件的时候,它们就可以通过共享内存进行通信。一个进程在共享内存上完成了写操作,此时此刻另一个进程在映射这个文件的虚拟地址空间上执行读操作时,它就可以看到一个进程写操作的结果。

3.5.8 清除策略

很多分页系统都有一个分页守护进程的后台进程,它在大多数时候都是睡眠的,但定期被唤醒以检查内存的状态。如果空闲页框过少,分页守护进程通过预定的页面置换算法选择页面换出内存。如果这些页面装入内存后被修改过,则将他们写回到磁盘上去。

3.5.9 虚拟内存接口

程序员可以对内存映射进行控制,程序可以通过对内存映射进行控制来实现两个或则多个进程共享同一部分的内存进行高效的消息传递。

3.6 有关实现的问题

3.6.1 与分页有关的工作

操作系统在下面这四段时间内做有关分页的工作:进程创建时,进程执行时,缺页中断和进程终止时。

3.6.2 缺页中断处理

3.6.3 指令备份

当程序不在内存中的页面的时候,引起缺页中断的指令会半途停止并引发操作系统地陷阱。在操作系统取出所需要的页面后,它需要重新启动引起陷阱的指令。但这并不是一件很容易实现的事情。

CPU的设计者提出了一种解决方法,就是通过使用一个隐藏内部的寄存器。在每条指令执行之前,把程序计数器的内容复制到该寄存器中。

3.6.5 后备存储

在磁盘上分配页面空间的最简单的算法就是设置特殊的交换分区。

最简单的情况下,当第一个进程启动的时候,留出于这个进程一样大的交换区块,剩余的为总空间减去这个交换分区。当新进程启动后,它们同样被分配与其核心映射相同大小的交换分区。进程结束后,会释放其磁盘的上的交换分区。

与每个进程所对应的是其交换分区的磁盘地址,即进程映像所保存的地方。这一个信息是记录在进程表里面的。

在这里插入图片描述

3.6.6 策略和机制的分配

一个如何分离策略和机制的简单列子如图示。其中存管理系统可以分成如下三个部分:

  1. 一个底层MMU处理程序
  2. 一个作为内核一部分的缺页中断程序
  3. 一个运行在用户空间中的外部页面调度程序。

在这里插入图片描述

3.7 分段

一个编译器在编译过程中会建立许多表,其中可能包括:

  1. 被保存起来供打印清单使用的源程序正文。
  2. 符号表,包含变量的名字和属性。
  3. 包含所有用到的整形变量和浮点变量的表。
  4. 语法分析树。
  5. 编译器内部过程调用的堆栈。

一个直观并且通用的方法是在机器上提供多个互相独立的称为段的地址空间。每个段由一个从0 到最大的线性地址序列构成。各个段的长度可以是0到某个允许的最大值之间的任何一个值。不同的段的长度可以不同,并且通常情况下都不相同。段的长度在运行期间可以动态改变。

在这里插入图片描述

接下来我们对分段和分页进行了比较:

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/qq_21125183/article/details/83901444
今日推荐