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