目录
1 虚拟内存的背景
无论是分页还是分段,程序运行的基本要求就是必须全部放入内存后方可运行,如果进程大于内存的容量或者内存中同时运行多个进程,那么进程就无法执行了,解决这个问题有两种方法,即覆盖和动态加载,但是这两种工作都是由程序员手动来做而且实现很复杂。
上面的问题究其本质就是内存不够用了,那么很容易想到的就是扩充内存,可以从物理上扩充内存容量,但是是受限的,如32位操作系统支持的内存最大为4GB
,64位操作系统支持的内存最大为128GB
,并且购买内存也较为昂贵。那么是否可以从逻辑上扩充内存容量呢?答案是可以的,本文内容就是讲解如何从逻辑上扩充内存。
常规的存储方式具有如下特征:
- 一次性:作业在运行前需要一次性的全部装入内存;
- 驻留性:作业装入内存后,便一直驻留在内存中,直到作业结束。
正是由于一次性和驻留性,使得程序中暂时不用的数据占用了大量的内存空间,从而需要运行的作业无法装入内存。那么一次性和驻留性是必需的吗?人们对程序做了很多的研究发现程序在执行过程中其实不是要运行所有部分:
- 程序通常有处理异常错误的代码,很少执行
- 数组、链表和表通常分配了比实际需要更多的内存
- 程序的某些选项或特点可能很少使用
- 即使需要完整的程序,也并不是同时需要所有的程序
通过上述四个特点,我们可以发现程序运行时往往只运行了一部分,这个特点我们叫做局部性原理。
1.1 局部性原理
局部性原理的定义是在一段时间内,程序的执行仅局限于某个部分;相应地,它所访问的存储空间也局限于某个区域内。出现局部性原理有如下原因:
- 程序在执行时,除了少部分的转移和过程调用指令外,大多数仍是顺序执行的;
- 子程序调用将会使程序的执行由一部分内存区域转至另一部分区域,也就是说程序只是从一个局部跳跃到另一个局部而已;
- 程序中存在许多循环结构,循环体中的指令被多次执行;
- 程序中还包括许多对数据结构的处理,如对连续的存储空间——数组的访问,往往局限于很小的范围内。
因此我们说程序是具有局部性的,局部性主要体现在两个方面:
- 时间局部性: 如果程序中的某条指令一旦执行,则不久的将来该指令可能再次被执行;如果某个存储单元被访问,则不久以后该存储单元可能再次被访问;产生时间局限性的典型原因是在程序中存在着大量的循环操作;
- 空间局部性: 一旦程序访问了某个存储单元,则在不久的将来,其附近的存储单元也最有可能被访问。 即程序在一段时间内所访问的地址,可能集中在一定的范围内,其典型原因是程序是顺序执行的。
1.2 虚拟内存
虚拟内存是一种允许进程部分装入内存就可以执行的技术,它基于的原理就是局部性原理,因为程序具有局部性,所以只需要把当前需要执行的程序内容装入内存即可,这个时候用户看到的逻辑地址空间就比物理地址空间大,要实现这个功能就必须允许页面能够被换入和换出。
如下图,虚拟内存virtual memory
显然是比实际内存physical memory
大的,只需要把当前要执行的部分装入内存即可;用memory map
来映射当前哪些部分是要装入内存的,类似页表,当运行到某个位置的时候就可以查询它在内存还是在外存;当要运行新的程序时或当前内存不足时就要和外存进行页面的换入和换出:
虚拟内存具有如下特征:
- 离散性:在内存分配时采用离散的分配方式,是虚拟存储器的最基本的特征;
- 多次性:一个作业被分成多次调入内存运行,即在作业运行时没有必要将其全部装入,只须将当前要运行的那部分程序和数据装入内存即可。是虚拟存储器最重要的特征;
- 对换性:作业运行过程中信息在内存和外存的对换区之间换进、换出;
- 虚拟性:从逻辑上扩充内存容量,使用户所看到的内存容量远大于实际内存容量。
2 请求调页
实现虚拟存储器要解决如下三个问题 :
- 程序部分运行可以吗? 是可以的,依照程序局部性原理,取出要用的页,然后装入内存即可;
- 发现程序不在内存时,如何将其装入后继续运行? 用请求调页技术,当发生缺页时,产生缺页中断,将外存上的页调入内存;
- 内存无空间时怎么办? 用页面置换,将部分页面换出内存。
2.1 页面调入策略
为能使进程运行,事先需将一部分要执行的程序和数据调入内存,有两种调页的策略:
- 预调页策略:主动的页面调入策略,即把那些预计很快会被访问的程序或数据所在的页面,预先调入内存;这个策略的性能取决于预测的准确率,预测的准确率不高(50%),主要用于进程的首次调入。也有的系统将预调页策略用于请求调页。
- 请求调页策略:当进程在运行中发生缺页时,由系统将缺页调入内存;目前虚拟存储器系统大多采用此策略;但在调页时须花费较大的系统开销,如需频繁启动磁盘I/O。
请求调页只有在一个页需要的时候才把它换入内存,这是请求调页的好处,或者说是虚拟内存的好处。它需要需要很少的I/O,需要很少的内存,能够快速响应,并且可以支持多用户。当需要某个页的时候判断它是否在内存中是需要进行查阅的,通常存在一个bit
位表示它是不是在内存,若不在内存中就要调入内存。
在具体实现的时候需要对进程页表的修改,也需要缺页中断的支持。请求分页的页表机制是在纯分页的页表机制上形成的,由于只将应用程序的一部分调入内存,还有一部分仍在磁盘上,故需在页表中再增加若干项,供程序(数据)在换进、换出时参考。在请求分页系统中的每个页表项如下图所示:
其中增加的各字段说明如下:
- 状态位(存在位P) :用于指示该页是否已调入内存,供程序访问时参考;
- 访问字段A:用于记录本页在一段时间内被访问的次数,或最近已有多长时间未被访问,提供给置换算法选择换出页面时参考;
- 修改位M:表示该页在调入内存后是否被修改过。由于内存中的每一页都在外存上保留一份副本,因此,若未被修改,在置换该页时就不需将该页写回到外存上,以减少系统的开销和启动磁盘的次数;若已被修改,则必须将该页重写到外存上,以保证外存中所保留的始终是最新副本;
- 外存地址:用于指出该页在外存上的地址,通常是物理块号,供调入该页时使用。
完成页面调页还需要缺页中断机构的支持,在请求分页系统中,每当所要访问的页面不在内存时,便要产生一缺页中断,请求操作系统将所缺页调入内存。与一般中断的主要区别在于:
- 缺页中断在指令执行期间产生和处理中断信号,而一般中断在一条指令执行完后检查和处理中断信号;
- 缺页中断返回到该指令的开始重新执行该指令,而一般中断返回到该指令的下一条指令执行;
- 一条指令在执行期间,可能产生多次缺页中断;
下图是缺页中断的处理过程,现在要加载一个程序 M
,❶首先要查询页表,发现该页在页表中是 i
(invalid),表示不在内存,❷这个时候就产生一个缺页中断,❸操作系统就会根据在页表中指向的外存的地址找到它,❹随后从外存放入内存,放入的时候要找一个空闲页,一旦放进去了以后,❺页表就要更新,此时中断就结束了,❻接着就要返回到这个程序重新执行:
上面整个过程主要是执行以下三个操作:
- 处理缺页中断;
- 从磁盘读入所需的页;
- 重新开始被中断的进程。
其中最大的一部分时间开销为第二步,即从磁盘读入所需的页,因此我们希望减少读入的次数,也就是降低缺页率。
缺页率 = 访问内存次数 / 不成功访问次数
3 页面置换
随着装入内存的程序越来越多,内存可能会有装满的情况下,这个时候如果来了新的程序想要进入内存,就必须执行页面置换,将内存中暂不使用的程序先从内存调出到外存。
如下图的两个用户程序,其中用户程序1需要载入程序M
,用户程序2需要载入程序B
,而此时M
载入到内存后,内存已经满了,程序B
再要装入内存已经没有位置了,所以此时要将现在内存中的某个程序置换出去。
现在置换有如下几种方法:
- 终止用户进程:一旦终止用户进程,进程就会释放内存空间,那么内存就腾出位置来了,这种方法的代价是比较大的;
- 交换进程:中级调度,释放其所有帧,降低多道程序的度,这种方法的代价也是比较大的,因为是以整个进程为单位,进行的 I/O 操作开销较大;
- 页面置换:以页为单位做交换,这种方法的开销相比是最小的。
页面置换的执行步骤如下:
- 找到页面在磁盘中的位置,找到之后便要把它读入内存,就要找到一个空闲的帧;
- 若有空闲的帧遍可以直接装入,若没有空闲的帧就要选择一个页调换出去,同时修改页表,再把页面装入内存;
页面置换过程如下图所示,❶牺牲当前内存中的某个页, 置换到外存上,❷修改页表标志位,❸将页面置换进内存中,❹更新页表:
可以发现在页面置换过程中,需要两个页面传输,一个换出,一个换入。但是有时候只需要一次置换就可以,因为有些程序在内存中并没有被修改过,所以它不需要换到外存去更新数据,只用牺牲它,将新调入的程序覆盖它即可,这里用到的方法就是前面提到的修改位。
页面置换的总的流程图如下图所示,图中的快表指的是联想寄存器:
4 页面置换算法
在进程运行过程中,如果发生缺页, , 而内存中又无空闲块时可以将内存中的某一页换到磁盘的对换区。那么到底选择调出哪一个页,可以根据页面置换算法来确定,置换算法的好坏将直接影响系统的性能,不适当的算法可能会导致进程发生 “抖动” (Thrashing) 。
抖动 (Thrashing):如果进程分配到的帧数量小于计算机体系结构所要求的最小数量,那么必须暂停进行执行。并将其置换出去,使其所有分配帧空闲。这么做的原因就是如果进程没有这些必需的帧,那么很快会出现缺页,此时需置换某个页,然而,其所有页都在使用,置换出去的页立刻又需要置换进来,因此,会不断的产生缺页。这种频繁的调页行为称作抖动 (Thrashing),也叫颠簸。
页面置换最大的问题就是到底换哪一个页,若换出的某个页很快就又要用到又要换进来,这样的效率是很低的,所以我们希望我们换出的页是今后很长一段时间内不再用到的页,这样就能降低系统的缺页率,我们来衡量一个页面置换算法的好坏主要是通过缺页率的大小,从理论上讲 , 应将那些以后不再被访问的页面换出,或把那些在较长时间内不会再被访问的页面换出,在实际的过程中有很多的置换算法能够接近理论目标,为什么说是理论上的,因为我们人是不知道哪些页面是要换的。
我们通过运行一个内存访问的特殊序列(访问序列),计算这个序列的缺页次数来评估算法。这个序列我们假定为 7, 0, 1, 2, 0, 3, 0, 4, 2, 3, 0, 3, 2, 1, 2, 0, 1, 7, 0, 1
,后面讨论算法时都会用到这个序列。
4.1 最佳算法(OPT, optimal)
最佳算法中被置换的页将是之后最长时间不被使用的页,其置换过程如下:
上述过程的缺页次数是
9
9
9,置换次数是
6
6
6。这个算法的缺点就是在实际过程中,我们并不知道这个内存访问序列,尤其是在多道批处理系统中,更是无法预测,所以最佳算法只是理论上最优的算法,现实中是无法实现的,我们通常用它来衡量其他算法的性能。
4.2 先进先出置换算法(FIFO)
先进先出置换算法中是按照内存先来先得,先进来的先出去这种方式来选择置换的页,其置换过程如下:
上述过程的缺页次数是
15
15
15,置换次数是
12
12
12。这个算法的性能几乎是比最佳算法差了一倍了,导致性能不好的原因是刚刚换出去的页,很可能又要被换进来,于是增加了缺页率,因此有了下面第三种置换算法。
4.3 最近最久未使用置换算法(LRU)
虽然并不知道页面未来的使用情况,但是可以使用离过去最近的情况作为不远将来的近似,可以选择最近最少使用的页进行置换,其置换过程如下:
上述过程的缺页次数是
12
12
12,置换次数是
9
9
9。这个算法的性能显然比先进先出置换算法要好,但是实现LRU算法需要硬件支持,记录物理页的使用情况。
但是实际上可能没有足够的硬件支持,所以就有了LRU的近似算法,如基于访问位的算法,二次机会算法。
- 访问位算法:每个页都与一个位相关联,初始值为
0
,每当这个页被访问的时候就把这个页置位1
,所以在选择置换的页时就可以看这个访问位,看谁是未被访问过的。但是这个算法有不足的地方就在于我们并不知道这个置换顺序,因为有可能有的页时很久都没有使用过的,有的页只是最近未被使用过的,理论上来说很久未被使用的页大概率以后不会再使用了,而最近未使用的页很可能再被使用欧冠。- 二次机会算法 (clock算法):同样它也需要访问位的支持,它会把所有的页组成一个环,同样未被访问时,访问位置
0
,访问位就置1
,在要置换时,我们以顺时针的方向遍历这个环来寻找访问位为0
的页换出去,若找到访问位为1
的页,就把它置位0
,代表着给它一次机会,这也是二次机会算法名字的由来。如果所有页的访问位都为1,则此算法退化为FIFO算法。二次机会算法执行过程如下图所示:
5 帧(页)分配
前面提到每个进程要运行则必须给它分配一定的内存空间,它才能把需要的内容放到内存去执行,那么如何给进程分配内存空间呢?首先我们要保证给它分配的空间是能够让它正常的运行的,即保证进程正常运行所需的最小物理块数,若系统为某进程所分配的物理块数少于此值时,进程将无法正常运行(频繁发生缺页),这个数目取决于指令的格式、功能和寻址方式。
具体分配多个页,有如下的分配方式:
- 平均分配:比如有100个页,和5个进程,则每个进程分给20个页;
- 按比例分配:根据每个进程的大小比例来分配;
- 优先分配:根据优先级而不是大小来使用比率分配策略。
如果进程 P i P_i Pi 产生了一个缺页,我们知道这个时候需要使用页面替换算法来替换一个页面,所替换页面的位置分为如下两种:
- 全局替换:进程在所有的页中选择一个替换页面;一个进程可以从另一个进程中获得页面;
- 局部替换:每个进程只从属于它自己的页中选择。
所以当进行全局置换的时候,进程所分配的页数是可以变化的,因为它占用了其他进程的页,因此使用全局置换可能造成其他进程的运行错误;当进行局部置换的时候,进程所分配的页数是固定不变的,因为它只在自己所属的范围内置换。