接下来几节都是对虚拟存储的讲解。虚拟存储是非连续存储管理的扩展。通过将内存中的数据暂存到外存的方式,为进程提供更大的内存空间。虚拟存储出现的主要原因是因为程序规模的增长速度远远大于存储器容量的增长速度,导致内存空间不够用。其实针对内存空间不够用的问题有多重解决方案,比如覆盖、交换、虚拟存储。它们的概念如下:
- 覆盖:应用程序手动把需要的指令和数据加载到内存;
- 交换:操作系统自动把暂时不能执行的程序保存到外存中;
- 虚拟存储:在有限的内存中,以页为单位自动装入更多更大的程序
覆盖
目标:在较小的可用内存中运行较大的程序。
方法:依据程序逻辑,将程序划分为若干功能相对独立的模块,不会同时执行的模块共享同一块内存区域。具体有以下几点:
- 必要部分(常用功能)的代码和数据常驻内存;
- 可选部分(不常用功能)放在其它程序模块中,只在需要时加载到内存;
- 不存在调用关系的模块可以相互覆盖,共用同一块内存区域。
具体操作如下:假设有一个程序总大小190K,调用关系如下图。我们按照上述原则对程序进行分组,比如A单独一组,B与C没有调用关系因此B、C一组,D、E、F没有调用关系分为一组,分配内存时按照组内最大的模块分配内存,如图中右侧,A划分20K,B、C这组分配50K,D、E、F这组分配40K,按照需求向内存中加载。这样这个程序可以在内存大小为110K的机器中运行。那么这种分组方式是不是最优的呢?答案是否定的,我们可以将大小相近的分到一组,比如A单独一组20K,B、E、F无调用关系50K,C、D无调用关系30K,总共需要100K内存。因此不同的分组方式对内存的需求是不一样的。想要严格讨论哪种方式内存需求最小是很复杂的。
缺点
- 增加编程困难。需要程序员划分功能模块,并确定模块之间的覆盖关系,增加了编程的复杂度;
- 增加执行时间。需要将各程序模块在内存模块中换入换出,实际是用时间换空间。
交换
交换与覆盖讨论的尺度是不太一样的。覆盖是一个程序内,如果无法全部加载到内存,而交换是内存足够放一个程序,而无法放多个程序。
目标:增加正在运行或需要运行的程序的内存。
实现方法:
- 可将暂时不能运行的程序放到外存;
- 换入换出的基本单位是进程;
- 换出是把一个进程的整个地址空间保存到外存;
- 换入是把一个进程的整个地址空间从外存加载到内存。
问题
- 交换时机:只当内存空间不足或有不足的可能的时候才进行换入换出;
- 交换区大小:存放所有用户进程的所有内存映像的拷贝;
- 程序换入时的重定位:换入时不是放回原处,因此需要程序运行过程中采用动态地址映射。
覆盖与交换对比:
- 覆盖:
- 只能发生在没有调用关系的模块之间;
- 程序员须给出模块之间的逻辑覆盖关系;
- 发生在运行程序的内部模块间。
- 交换:
- 以进程为单位;
- 不需要模块间的逻辑覆盖关系;
- 发生在内存进程间。
交换是可以通过操作系统实现的,而覆盖是无法在操作系统中实现的,必须有程序员具体实现,增大了编程的难度。
虚拟存储
虚拟存储是想把一部分内存中的内容暂时存放到外存中,以提供更大的内存空间。例如
虚拟存储的目标:
- 只把部分程序放到内存中,从而运行比物理内存大的程序。并且由操作系统完成,无需程序员的干涉。
- 实现进程在内存和外存在之间交换,从而获得更多的空闲内存空间。在内存与外存之间只交换进程的部分内容。
局部性原理
具体讲虚拟存储之前先了解一下局部性原理。局部性原理是指程序在执行过程中的一个较短时期,所执行的指令地址和指令的操作数地址,分别局限于一定区域。比如通常指令在代码段,操作数在数据段。具体体现在以下几个方面:
- 时间局部性。一条指令的一次执行和下次执行,一个数据的一次访问与下次访问都集中在一个较短时期内;
- 空间局部性。当前指令和邻近的几条指令,当前访问的数据和邻近的几条数据都集中在一个较小的区域内;
- 分支局部性。一条跳转指令的两次执行,很可能跳转到相同的内存位置。
由于局部性原理的存在,从理论上讲,虚拟存储技术是能够实现的,可以取得满意的效果。
虚拟存储基本概念:
- 思路:将不常用的部分内存块暂存到外存。
- 原理:装载程序时,只将当前指令执行需要的部分页面或段装入内存;指令执行中需要的指令或数据不在内存中(缺页或缺段)时,处理器通知操作系统将相应的页或段调入内存中;操作系统将内存中暂时不用的页面或段保存到外存中,如何判定哪些不常用,需要后续的置换算法。
- 实现方式:虚拟页式存储、虚拟段式存储。
虚拟存储的基本特征:
- 不连续性。物理内存分配非连续,虚拟地址空间使用非连续。
- 大用户空间。提供给用户的虚拟内存可大于实际的物理内存。
- 部分交换。虚拟存储支队部分虚拟地址空间进行调入和调出。
虚拟存储的支撑技术:
- 硬件支持。页式或段式存储中的地址转换机制。除之前非连续内存管理中所讲的地址转换,还要增加判断所需要的内容不再内存中。
- 操作系统支持。管理内存和外存间页面或段的换入和换出。
虚拟页式存储:
在页式存储管理的基础上,增加请求调页和页面置换。具体思路是当用户程序要装载到内存运行时,只装入部分页面就启动程序运行。进程在运行中发现有需要的代码或数据不在内存时,则向系统发出缺页异常请求。操作系统在处理缺页异常时,将外存中相应的页面调入内存,使得进程能够继续运行。如下图所示,在页表中加入标志位,当发现缺页时触发缺页异常,操作系统接管,找到相应页面置入内存,并将标志位修改为有效。
虚拟页式管理中,页表项结构有相应调整,如下图:增加了几个标志位。其中驻留位用来表示是否在内存中;修改位表示在内存中的该页是否被修改过,因为如果没有修改过而外存中又有这一页面时不需要置换,直接作废该页表项即可,如果被修改过则需要重新置换;访问位表示该页是否被访问过(读或写),用于置换算法,用于近似统计是否经常访问;保护位表示该页的允许访问方式,如可读可写。
示例:
下面是X86 32位系统以4K为页帧大小时的页表项结构:帧号项占20位,标志位中除了前面提到的驻留位、可写标志、访问位、修改位,还有几个没提到过的。一个是用户态标志,表示用户态是否可以访问;保留位,用于扩展,比如现在很多32位系统不仅支持4G内存;CD位,缓存是否有效,是否允许启用高速缓存;WT是否写通。
缺页异常:
缺页异常即发现需要的内存页不在内存中。缺页异常处理流程如下图:
执行命令时CPU给出需要的逻辑地址,查找页表;发现缺页,产生缺页异常;操作系统启用缺页异常例程,查找页面在外存中的位置;从物理内存中找空闲页帧并将页面换入空闲处;将页表项修改为有效;重新执行导致异常的指令。
可以看到这是最顺利的情况,如果换入时发现没有空闲页帧的话则需要额外几步:根据页面置换算法选择要被换出的页帧;判断该页帧是否被修改过,如果被修改过则写回外存,否则直接作废;将其对应的页表项改为无效;然后将需要的页帧换入空出来的区域,后续与最开始的流程一致。
虚拟页式存储中的外存是如何管理的呢?首先是在何处保存未被映射的页?因为必须能够方便地找到在外存中的页面内容,因此我们需要一个交换空间,有两种方式实现:磁盘或文件。Linux就是直接有交换分区。或者文件采用特殊格式存储未被映射的页面。
虚拟页式存储中的外存选择也是个问题。代码段本身就是存储在可执行文件中的,因此代码段是直接指向你的可执行文件;动态加载的共享库程序段也直接指向相应的文件;其他段放在对换区。
虚拟页式存储管理的性能如何评价呢?
我们定义了一个有效存储访问时间(effective memory access time)EAT。
举个例子:可以看到想要效率较高,需要缺页率p极小。