覆盖:
目标:
在较小的可用内存中运行较大的程序。
方法:
依据程序逻辑,将程序划分为若干功能相对独立的模块,不会同时执行的模块共享同一块内存区域。具体有以下几点:
必要部分(常用功能)的代码和数据常驻内存;
可选部分(不常用功能)放在其它程序模块中,只在需要时加载到内存;
不存在调用关系的模块可以相互覆盖,共用同一块内存区域。
交换:
交换与覆盖讨论的尺度是不太一样的。覆盖是一个程序内,如果无法全部加载到内存,而交换是内存足够放一个程序,而无法放多个程序。
目标:
增加正在运行或需要运行的程序的内存。
实现方法:
可将暂时不能运行的程序放到外存;
换入换出的基本单位是进程;
换出是把一个进程的整个地址空间保存到外存;
换入是把一个进程的整个地址空间从外存加载到内存。
覆盖与交换对比:
覆盖:
只能发生在没有调用关系的模块之间;
程序员须给出模块之间的逻辑覆盖关系;
发生在运行程序的内部模块间。
交换:
以进程为单位;
不需要模块间的逻辑覆盖关系;
发生在内存进程间。
交换是可以通过操作系统实现的,而覆盖是无法在操作系统中实现的,必须有程序员具体实现,增大了编程的难度。
虚拟存储基本概念:
- 思路:将不常用的部分内存块暂存到外存。
- 原理:装载程序时,只将当前指令执行需要的部分页面或段装入内存;指令执行中需要的指令或数据不在内存中(缺页或缺段)时,处理器通知操作系统将相应的页或段调入内存中;操作系统将内存中暂时不用的页面或段保存到外存中,如何判定哪些不常用,需要后续的置换算法。
- 实现方式:虚拟页式存储、虚拟段式存储。
虚拟页式存储:
在页式存储管理的基础上,增加请求调页和页面置换。具体思路是当用户程序要装载到内存运行时,只装入部分页面就启动程序运行。进程在运行中发现有需要的代码或数据不在内存时,则向系统发出缺页异常请求。操作系统在处理缺页异常时,将外存中相应的页面调入内存,使得进程能够继续运行。如下图所示,在页表中加入标志位,当发现缺页时触发缺页异常,操作系统接管,找到相应页面置入内存,并将标志位修改为有效。
虚拟页式管理中,页表项结构有相应调整,如下图:增加了几个标志位。其中驻留位用来表示是否在内存中;修改位表示在内存中的该页是否被修改过,因为如果没有修改过而外存中又有这一页面时不需要置换,直接作废该页表项即可,如果被修改过则需要重新置换;访问位表示该页是否被访问过(读或写),用于置换算法,用于近似统计是否经常访问;保护位表示该页的允许访问方式,如可读可写。
缺页异常:
缺页异常即发现需要的内存页不在内存中。却也异常处理流程如下图:即执行命令时CPU给出需要的逻辑地址,查找页表;发现缺页,产生缺页异常;操作系统启用缺页异常例程,查找页面在外存中的位置;从物理内存中找空闲页帧并将页面换入空闲处;将页表项修改为有效;重新执行导致异常的指令。可以看到这是最顺利的情况,如果换入时发现没有空闲页帧的话则需要额外几步:根据页面置换算法选择要被换出的页帧;判断该页帧是否被修改过,如果被修改过则写回外存,否则直接作废;将其对应的页表项改为无效;然后将需要的页帧换入空出来的区域,后续与最开始的流程一致。
页面置换算法
功能:
置换算法是指当出现缺页异常时,需要调入新页面而内存已满时,置换算法选择被置换的物理页面。
设计目标:
尽可能减少页面的调入调出次数;
把未来不再访问或短期内不访问的页面调出。
页面锁定:
了解具体的置换算法之前,先了解一个概念,页面锁定。页面锁定是用来描述某些必须常驻内存的逻辑页面,比如操作系统的关键部分,再比如一些要求响应速度的代码和数据。页面锁定是通过页表中的锁定标志位实现的。
分类:
- 局部置换算法:
置换页面的选择范围仅限于当前进程占用的物理页面内。具体又有一系列算法:最优算法、先进先出算法、最近最久未使用算法,最近最久未使用算法又衍生出两种近似算法:时钟算法、最不常用算法,之后进行详细介绍。 - 全局置换算法;
置换页面的选择范围是所有可换出的物理页面。具体有工作集算法、缺页率算法。
局部置换算法:
前面对置换算法进行了分类,接下来分别进行讨论。
1. 最优置换算法:
因为无法知道未来的访问顺序,实际是无法实现的,只是用来针对每个运行完的程序马后炮式地计算最优解,用于评价其他实际算法的性能优劣。
2. 先进先出置换算法(FIFO):
选择在内存驻留时间最长的页面进行置换。具体是通过链表实现:链表元素按驻留内存的时间排序,链首最长,链尾最短,出现缺页时,选择链首页面进行置换,新页面加到链尾。因此实现简单,但是性能较差,因为调出的页面可能是经常访问的,进程分配物理页面数增加时,缺页并不一定减少(这一点我是这样理解的:当分配给一个进程的物理页面增多时,进程就有更多的空间给它调配,最优置换算法就可以实现更少的缺页异常),这个现象被称为Belady现象。因此该算法很少单独使用。
3. 最近最久未使用算法(Least Recently Used, LRU):
选择最长时间没有被引用的页面进行置换。如果某些页面长时间未被访问,则他们在将来还可能会长时间不会被访问。具体实现是在缺页时计算内存中每个逻辑页面的上一次访问时间,选择上一次使用到当前时间最长的页面置换出去。该算法是最优置换算法的一种近似,但仍然很复杂,很难实现。比如下面是LRU算法的两种可能的实现算法:
a. 页面链表。系统维护一个按最近一次访问时间排序的页面链表,链表首节点是最近使用的页面,尾节点是最久未使用的页面,访问内存时找到相应页面,并把它移到链表之首,缺页时,置换链表尾节点的页面。
b. 活动页面栈。访问页面时将此页号压入栈顶,并将栈内相同的页号抽出,缺页时置换栈底的页面。
可以看到这两种方式存在一个问题就是平时不缺页时开销较大,每次访问页面都需要遍历链表或栈,找到相同的元素进行处理。
4. 时钟置换算法(Clock):
时钟置换算法是最近最久未使用算法的优化。先进先出是完全不考虑最近访问情况,最近最久未使用算法是将所有页面在整个运行过程中的访问情况都进行考虑。而时钟置换算法是这两者的折中,即仅对页面的访问情况进行大致统计。具体实现
在页表项中增加访问位,描述页面在过去一段时间内的访问情况;
各页面组织成环形链表;
指针指向最先调入的页面;
页面装入内存时,访问位初始化为0;
访问页面时,被访问页面的访问位置1;
缺页时,从指针处开始顺序扫描,如果访问位为0则置换该页,如果访问位为1则修改为0并移动指针到下一个页面,直到找到可置换页面。
时钟置换算法实际还有一些改进。比如减少修改页的缺页处理开销。因为被修改的页如果要被置换,需要先写到外存,再将需要的页写入内存,开销至少乘2。因此为了减小修改过的页被置换,可以遇到被修改过的页指针就跳过。而在系统空闲时定期地将内存写入外存。实现通过在页面中增加修改位,并在访问时进行相应修改,缺页扫描时跳过有修改的页面。
5. 最不常用置换算法(Least Frequently Used, LFU):
最不常用置换算法页式最近最久未使用算法的优化。思路就是在缺页时,置换访问次数最少的页面。可以通过在每个页面设置一个访问技术,访问到一个页面时,访问计数加1,缺页时置换计数最小的页面。存在的问题就是统计开销大,并且新拿进来的页面可能因为计数较少马上又被置换出去,对于后面这个问题的一个解决方法是已经计的数定期地衰减。
可以看到LFU与LRU的区别。LRU关注多久未被访问,时间越短越好,LFU关注访问次数,次数越多越好。
LFU与时钟算法都是对LRU算法的一种简化近似,开销减小,同时精度下降。LFU是比较难实现的,因此在内存管理中基本不会采用,但是在读硬盘文件的时候对时间要求不高的场景中还是可以使用的。
Belady现象:
当一个进程频繁出现缺页异常时,应该分配给进程更多的物理页面,分配完后按照常理,缺页异常出现的频率应该降低。但是如果算法不好,很可能不会降低,我们把这种现象称为Belady现象。但是LRU算法是没有Belady现象的。时钟算法和改进的时钟算法也都是没有Belady现象的。分配更多的页面以这为更大的缓存,缓存越大命中率肯定升高。
全局置换算法:
局部置换算法没有考虑进程访问的差异,有时候给一个进程多分配一个物理页面可以大幅度降低它的缺页率。全局置换算法就是要为进程分配可变数目的物理页面。它需要解决以下几个问题:
进程在不同阶段的内存需求是变化的
分配各进程的内存也需要在不同阶段有所变化。
全局置换算法需要确定分配给进程的物理页面数
- 工作集置换算法:
- 缺页率置换算法(Page-Fault-Frequency, PFF)
- 抖动和负载控制