计算机系统(三):内存管理(下篇)

目录

引言

3.5 页面置换算法

3.5.1 最优页面置换算法

3.5.2 最近未使用页面置换算法

3.5.3 先进先出页面置换算法

3.5.4 第二次机会页面置换算法

3.5.5 最近最少使用页面置换算法

3.4.6 用软件模拟LRU(老化算法)

3.5.7 工作集页面置换算法

页面置换算法小结


引言

上一篇博客我们学习了虚拟内存的相关知识,在最后的MMU和TLB部分,我们提到,当发生缺页中断时,操作系统必须在内存中选择一个 页面将其换出内存,以便为即将调入的页面腾出空间。如果要换出的页面在内存驻留期间已经被修改过,就必须把它写回磁盘以更新该页面在磁盘上的副本,如果该页面没有被修改过,那么它在磁盘上的副本已经是最新的,不需要回写。直接用调入的页面覆盖被淘汰的页面就可以了。

当发生缺页中断时,虽然可以随机地选择一个页面来置换,但是如果每次都选择不常使用的页面会提升系统的性能。如果一个被频繁使用的页面被置换出内存,很可能它在很短时间内又要被调入内存,这会带来不必要的开销。人们已经从理论和实践两个方面对页面置换算法进行了深入的研究。本篇博客我们将介绍几个最重要的算法。

3.5 页面置换算法

“页面置换"问题不是缺页中断的专属问题,它在计算机设计的其他领域中也同样会发生。

例如,多数计算机把最近使用过的存储块保存在一个或多个高速缓存中。当这些高速缓存存满之后就必须选择一些块丢掉。除了花费时间较短外(几纳秒),这个问题同页面置换问题完全一样。 之所以花费时间较短,其原因是丢掉的高速缓存块可以从内存中获得,而内存既没有寻道时间也不存在旋转延迟。

第二个例子是Web服务器。服务器可以把经常访问的一些Web页面存放在存储器的高速缓存中。但是,当存储器高速缓存已满并且要访问一个不在高速缓存中的页面时,就必须要置换高速缓存中的某个Web页面。在高速缓存中的Web页面不会被修改,因此在磁盘中的Web页面的副本总是最新的,而在虚拟存储系统中,内存中的页面既可能是干净页面也可能是脏页面,除了这一点不同之外,置换Web页面和置换虚拟内存中的页面需要考虑的问题是类似的。

3.5.1 最优页面置换算法

很容易就可以描述出最好的页面置换算法,虽然此算法不可能实现。该算法是这样工作的:

在缺页中断发生时,有些页面在内存中,其中有一个页面(包含下一条指令的页面)将很快被访问,其他页面则可能要到10、100或1000条指令后才会被访问,每个页面都可以用在该页面首次被访问前所要执行的指令数作为标记。最优页面置换算法规定应该置换标记最大的页面。如果一个页面在80条指令内不会被使用,另外一个页面在600条指令内不会被使用,则置换前一个页面,从而把因需要调入这个页面而发生的缺页中断推迟到将来,越久越好。

这个算法唯一的问题就是它是无法实现的。当缺页中断发生时,操作系统无法知道各个页面下一次将在什么时候被访问。就跟最短作业优先调度算法一样,系统不知道哪个作业是最短的 。

当然,我们可以想一个大胆的方法,通过仿真程序先运行一遍程序,跟踪所有页面的访问情况,然后在第二次运行时利用第一次运行时收集的信息是可以实现最优页面置换算法的。用这种方式,可以通过最优页面置换算法对其他可实现算法的性能进行比较。如果操作系统达到的页面置换性能只比最优算法差1%,那么即使花费大量的精力来寻找更好的算法最多也只能换来1%的性能提高。

并且,我们要清楚在仿真程序上运行程序,记录的页面访问情况的记录只针对特定程序和特定的输入,因此从中导出的性能最好的页面置换算法也只是针对这个程序和输入。虽然这个方法对评价页面置换算法很有用,但它在实际系统中却不能使用。下面将研究可以在实际系统中使用的算法。

3.5.2 最近未使用页面置换算法

为使操作系统能够收集有用的统计信息,在大部分具有虚拟内存的计算机中,系统为每一页面设置了两个状态位。当页面被访问(读或写)时设置R位;当页面被写入(修改)时设置M位。这些位包含在每个页表项中。每次访问内存时更新这些位,因此由硬件来设置它们是必要的。一旦设置某位为1,它就直保持1直到操作系统将它复位。

如果硬件没有这些位,则可以使用操作系统的缺页中断和时钟中断机制进行以下的模拟:当启动一个进程时,将其所有的页面都标记为不在内存:一旦访问任何一个页面就会引发一次缺页中断,此时操作系统就可以设置R位(在它的内部表中),修改页表项使其指向正确的页面,并设为READ ONLY模式,然后重新启动引起缺页中断的指令;如果随后对该页面的修改又引发一次缺页中断,则操作系统设置这个页面的M位并将其改为READ/WRITE模式。

可以用R位和M位来构造一个简单的页面置换算法:当启动一个进程时,它的所有页面的两个位都由操作系统设置成0,R位被定期地清零,以区别最近没有被访问的页面和被访问的页面。

当发生缺页中断时,操作系统检查所有的页面并根据它们当前的R位和M位的值,把它们分为4类:

第0类没有被访问,没有被修改R = 0,M = 0
第1类没有被访问,已被修改R = 0,M = 1
第2类已被访问,没有被修改R = 1,M = 0
第3类已被访问,已被修改R = 1,M = 1

注:尽管第1类初看起来似乎是不可能的,但是一个第3类的页面在它的R位被时钟中断清零后就成了第1类。时钟中断不清除M位是因为在决定一个页面是否需要写回磁盘时将用到这个信息。清除R位而不清除M位产生了第1类页面。

NRU(Not Recently Used,最近未使用)算法随机地从类编号最小的非空类中挑选一个页面淘汰。这个算法隐含的意思是,在最近一个时钟滴答中(典型的时间是大约20ms)淘汰一个没有被访问的已修改页面(R = 0,M = 1)要比淘汰一个被频繁使用的“干净”页面好,因为上一个刚被使用和修改过。NRU的主要优点是易于理解和能够有效地被实现,虽然它的性能不是最好的,但是已经够用了。

3.5.3 先进先出页面置换算法

另一种开销较小的页面置换算法是FIFO(First-In First-Out,先进先出)算法。为了解释它是怎样工作的,设想有一个超市,它有足够的货架展示k种不同的商品。有一天,某家公司介绍了一种新的成功产品,所以容量有限的超市必须撤掉一种旧的商品以便能够展示该新产品。

一种可能的解决方法就是找到该超市中库存时间最长的商品并将其替换掉(比如某种120年以前就开始卖的商品),理由是现在已经没有人喜欢它了。这实际上相当于超市有一个按照引进时间排列的所有商品的链表。新的商品被加到链表的尾部,链表头上的商品则被撤掉。

同样的思想也可以应用在页面置换算法中。由操作系统维护一个所有当前在内存中的页面的链表,最新进入的页面放在表尾,最早进入的页面放在表头。当发生缺页中断时,淘汰表头的页面并把新调入的页面加到表尾。当FIFO用在超市时,可能会淘汰剃须膏,但也可能淘汰面粉、盐或黄油这一类常用商品。因此,当它应用在计算机上时也会引起同样的问题,即替换掉常用的页面,所以很少使用纯粹的FIFO算法。

3.5.4 第二次机会页面置换算法

FIFO算法可能会把经常使用的页面置换出去,为了避免这一问题,对该算法做一个简单的修改:检查最老页面的R位。如果R位是0,那么这个页面既老又没有被使用,可以立刻置换掉:如果是1,就将R位清0,并把该页面放到链表的尾端,修改它的装入时间使它就像刚装入的一样,然后继续搜索。这一算法称为第二次机会(second chance)算法,它相当于FIFO和NRU的结合。如下图所示:

页面A到H按照进入内存的时间顺序保存在链表中。假设在时刻20处发生了一次缺页中断。这时最老的页面是A,它是在时刻0到达的。如果A的R位是0,则将它淘汰出内存,或者把它写回磁盘(如果M = 1),或者只是简单地放弃(如果M = 0);另一方面,如果其R位已经设置了(R = 1),则将A放到链表的尾部并且重新设置“ 装入时间”为当前时刻(20), 然后清除R位(R = 0)。然后从B页面开始继续搜索合适的页面。

第二次机会算法就是寻找一个在最近的时钟间隔内没有被访问过的页面。如果所有的页面都被访问过了,该算法就简化为纯粹的FIFO算法。假设上图中所有页面的R位都被设置了,操作系统将会一个一个地把每个页面都移动到链表的尾部并清除被移动的页面的R位。最后算法又将回到页面A,此时它的R位已经被清除了,因此A页面将被淘汰,所以这个算法总是可以结束的。

3.5.5 最近最少使用页面置换算法

我们虽然无法实现最优页面置换算法,但是我们可以思考出一个可以实现的近似算法。首先我们发现,在前面几条指令中频繁使用的页面很可能在后面的几条指令中被使用。反过来说,很久没有使用的页面很有可能在未来较长的一段时间内仍然不会被使用。

LRU(Least Recently Used,最近最少使用

在缺页中断发生时,置换未使用时间最长的页面。这个策略称为LRU页面置换算法。

虽然LRU在理论上是可以实现的,但代价很高。为了完全实现LRU,需要在内存中维护一个所有页面的链表,最近最多使用的页面在表头,最近最少使用的页面在表尾。困难的是在每次访问内存时都必须要更新整个链表。在链表中找到一个页面,删除它,然后把它移动到表头是一个非常费时的操作,即使使用硬件实现也样费时(假设有这样的硬件)。

然而,还是有一些使用特殊硬件实现LRU的方法。首先考虑一个最简单的方法,这个方法要求硬件有一个64位计数器C,它在每条指令执行完后自动加1,每个页表项必须有一个足够容纳这个计数器值的域。在每次访问内存后,将当前的C值保存到被访问页面的页表项中。一旦发生缺页中断,操作系统就检查所有页表项中计数器的值,找到值最小的一个页面,这个页面就是最近最少使用的页面。

3.4.6 用软件模拟LRU(老化算法)

前面一种LRU算法虽然在理论上是可以实现的,但只有非常少的计算机拥有这种硬件。因此,需要一个能用软件实现的解决方案。一种可能的方案称为NFU(Not Frequently Used,最不常用)算法。该算法将每个页面与一个软件计数器相关联,计数器的初值为0。每次时钟中断时,由操作系统扫描内存中所有的页面,将每个页面的R位(它的值是0或1)加到它的计数器上。这个计数器大体上跟踪了各个页面被访问的频繁程度。发生缺页中断时,则置换计数器值最小的页面。

NFU的主要问题是它从来不忘记任何事情,这可能反而使他置换有用的页面。我们假设一个场景,在一个多次(扫描)编译器中,在第一次扫描中被频繁使用的页面在程序进入第二次扫描时,其计数器的值可能仍然很高。实际上,如果第一次扫描的执行时间恰好是各次扫描中最长的,含有以后各次扫描代码的页面的计数器可能总是比含有第一次扫描代码的页面的计数器小,结果是操作系统将置换有用的页面而不是不再使用的页面。

幸运的是只需对NFU做一个小小的修改就能使它很好地模拟LRU。其修改分为两部分:

首先,在R 位被加进之前先将计数器右移一位;

其次,将R位加到计数器最左端的位而不是最右端的位。

修改以后的算法称为老化(aging)算法,下图展示了它是如何工作的:

假设在第一个时钟滴答后,页面0~ 5的R位值分别是1、0、1、0、1、1。换句话说,在时钟滴答0到时钟滴答1期间,访问了页0、2、4、5,它们的R位设置为1,而其他页面的R位仍然是0。对应的6个计数器在经过移位并把R位插入其左端后的值如图a) 所示。图中后面的4列是在下4个时钟滴答后的6个计数器的值。

发生缺页中断时,将置换计数器值最小的页面。如果一个页面在前面4个时钟滴答中都没有访问过,那么它的计数器最前面应该有4个连续的0,因此它的值肯定要比在前面三个时钟滴答中都没有被访问过的页面的计数器值小,比如上面的例子就是置换页面3。

也就是说判断的顺序应该是从e往前比较各个页的访问情况,直到有一个页面比另一个页面更“老”,比不出来就继续往前比。

该老化算法与LRU有两个区别。

如图e) 中的页面3和页面5,它们都连续两个时钟滴答没有被访问过了,而在两个时钟滴答之前的时钟滴答中它们都被访问过。根据LRU,如果必须置换一个页面,则应该在这两个页面中选择一个。然而现在的问题是,我们不知道在时钟滴答1到时钟滴答2期间它们中的哪一个页面是后被访问到的。因为在每个时钟滴答中只记录了一位,所以无法区分在一个时钟滴答中哪个页面在较早的时间被访问以及哪个页面在较晚的时间被访问,因此,我们所能做的就是置换页面3,原因是页面5在更往前的两个时钟滴答中也被访问过而页面3没有。

第二个区别是老化算法的计数器只有有限位数(本例中是8位),这就限制了其对以往页面的记录。如果两个页面的计数器都是0,我们只能在两个页面中随机选一个进行置换。实际上,有可能其中一个页面上次被访问是在9个时钟滴答以前,另一个页面是在1000个时钟滴答以前,而我们却无法看到这些。在实践中,如果时钟滴答是20ms, 8位一般是够用的。假如一个页面已经有160ms没有被访问过,那么它很可能并不重要。

3.5.7 工作集页面置换算法

在单纯的分页系统里,刚启动进程时,在内存中并没有页面。在CPU试图取第一条指令时就会产生一次缺页中断,使操作系统装入含有第一条指令的页面。其他由访问全局数据和堆栈引起的缺页中断通常会紧接着发生。一段时间以后,进程需要的大部分页面都已经在内存了,进程开始在较少缺页中断的情况下运行。这个策略称为请求调页(demand paging),这就是因为页面是在需要时被调入的,而不是预先装入。

编写一个测试程序很容易,在一个大的地址空间中系统地读所有的页面,将出现大量的缺页中断,因此会导致没有足够的内存来容纳这些页面。不过幸运的是,大部分进程不是这样工作的,它们都表现出了一种局部性访问行为,即在进程运行的任何阶段,它都只访问较少的一部分页面。

工作集

一个进程当前正在使用的页面的集合称为它的工作集。 如果整个工作集都被装入到了内存中,那么进程在运行到下一运行阶段(例如,编译器的下一遍扫描)之前,不会产生很多缺页中断。

若内存太小而无法容纳下整个工作集,那么进程的运行过程中会产生大量的缺页中断,导致运行速度也会变得很缓慢,因为通常只需要几个纳秒就能执行完一条指令,而通常需要十毫秒才能从磁盘上读入一个页面。如果一个程序每10ms只能执行一到两条指令,那么它将会需要很长时间才能运行完。若每执行几条指令程序就发生一次缺页中断,那么就称这个程序发生了颠簸。

在多道程序设计系统中,经常会把进程转移到磁盘上(即从内存中移走所有的页面),这样可以让其他的进程有机会占有CPU。有一个问题是,当该进程再次调回来以后应该怎样办?从技术的角度上讲,并不需要做什么。该进程会一直产生缺页中断直到它的工作集全部被装入内存。然而,每次装入一个进程时都要产生20、100甚至1000次缺页中断,速度显然太慢了,并且由于CPU需要几毫秒时间处理一个缺页中断,因此有相当多的CPU时间也被浪费了。

工作集模型

所以不少分页系统都会设法跟踪进程的工作集,以确保在让进程运行以前,它的工作集就已在内存中了。该方法称为工作集模型,其目的在于大大减少缺页中断率。在进程运行前预先装入其工作集页面也称为预先调页(prepaging)。 请注意工作集是随着时间变化的。

人们很早就发现大多数程序都不是均匀地访问它们的地址空间的,而访问往往是集中于一小部分页面。一次内存访问可能会取出一条指令,也可能会取数据,或者是存储数据。在任一时刻t,都存在一个集合,它包含所有最近k次内存访问所访问过的页面。这个集合w(k, t)就是工作集。因为最近k= 1次访问肯定会访问最近k>1次访问所访问过的页面,所以w(k, t)k的单调非递减函数。随着k的变大,w(k, t)是不会无限变大的,因为程序不可能访问比它的地址空间所能容纳的页面数目上限还多的页面,并且几乎没有程序会使用每个页面。下图描述了作为k的函数的工作集的大小:

事实上大多数程序会任意访问一小部分页面,但是这个集合会随着时间而缓慢变化,这个事实也解释了为什么一开始曲线快速地上升而k较大时上升会变慢。

因为这是个渐进的过程,k值的选择对工作集的内容影响不大。换句话说,k的值有一个很大的范围,它处在这个范围中时工作集不会变。因为工作集随时间变化很慢,那么当程序重新开始时,就有可能根据它上次结束时的工作集对要用到的页面做一个 合理的推测k,预先调页就是在程序继续运行之前预先装人推测出的工作集的页面。

为了实现工作集模型,操作系统必须跟踪哪些页面在工作集中。通过这些信息可以直接推导出一个合理的页面置换算法:

当发生缺页中断时,淘汰一个不在工作集中的页面。

为了实现该算法,就需要一种精确的方法来确定哪些页面在工作集中。根据定义,工作集就是最近k次内存访问(或页面访问)所使用过的页面的集合。为了实现工作集算法,必须预先选定k的值。一旦选定某个值,每次内存访问之后,最近k次内存访问所使用过的页面的集合就是唯一确定的了。

当然,我们需要想一种方法能够在程序运行期间计算出工作集。设想有一个长度为k的移位寄存器,每进行一次内存访问就把寄存器左移一位,然后在最右端插入刚才所访问过的页面号。移位寄存器中的k个页面号的集合就是工作集。理论上,当缺页中断发生时,只要读出移位寄存器中的内容并排序;然后删除重复的页面。结果就是工作集。

然而,维护移位寄存器并在缺页中断时处理它所需的开销很大,因此该技术从来没有被使用过。作为替代,可以使用几种近似的方法。

一种常见的近似方法就是,不是向后找最近k次的内存访问,而是考虑其执行时间。例如,按照以前的方法,定义工作集为前k次内存访问所使用过的页面的集合,那么现在就可以这样定义:工作集即是过去kms中的内存访问所用到的页面的集合。实际上,这样的模型很合适且更容易实现。一个进程从它开始执行到当前所实际使用的CPU时间总数通常称作当前实际运行时间。通过这个近似的方法,进程的工作集可以被称为在过去的秒实际运行时间中它所访问过的页面的集合。

注意:每个进程只计算它自己的执行时间。因此,如果一个进程在T时刻开始,在(T+100)ms的时刻使用了40ms CPU时间,对工作集而言,它的时间就是40ms。

现在让我们来看一下基于工作集的页面置换算法:基本思路就是找出一个不在工作集中的页面并淘汰它。

下图是某台机器的部分页表。因为只有那些在内存中的页面才可以作为候选者被淘汰,所以该算法忽略了那些不在内存中的页面。每个表项至少包含两条信息:上次使用该页面的近似时间和R(访问)位。空白的矩形表示该算法不需要的其他域,如页框号、保护位、M(修改)位。

该算法工作方式如下:

使用硬件来置R位和M位。同样,假定在每个时钟滴答中,有一个定期的时钟中断会用软件方法来清除R位。每当缺页中断发生时,扫描页表以找出一个合适的页面淘汰之。

  • 在处理每个表项时,都需要检查R位。如果它是1,就把当前实际时间写进页表项的“上次使用时间”域,以表示缺页中断发生时该页面正在被使用。既然该页面在当前时钟滴答中已经被访问过,那么很明显它应该出现在工作集中,并且不应该被删除(假定t横跨多个时钟滴答)。
  • 如果R是0,那么表示在当前时钟滴答中,该页面还没有被访问过,则它就可以作为候选者被置换。为了知道它是否应该被置换,需要计算它的生存时间(即当前实际运行时间减去上次使用时间),然后与r做比较。如果它的生存时间大于t,那么这个页面就不再在工作集中,而用新的页面置换它。扫描会继续进行以更新剩余的表项。
  • 然而,如果R是0同时生存时间小于或等于t,则该页面仍然在工作集中。这样就要把该页面临时保留下来,但是要记录生存时间最长(“上次使用时间”的最小值)的页面。如果扫描完整个页表却没有找到适合被淘汰的页面,也就意味着所有的页面都在工作集中。在这种情况下,如果找到了一个或者多个R = 0的页面,就淘汰生存时间最长的页面。在最坏情况下,在当前时间滴答中,所有的页面都被访问过了(也就是都有R=1),因此就随机选择一个页面淘汰,如果有的话最好选一个干净页面。
     

页面置换算法小结

我们已经考察了多种页面置换算法,现在对这些算法进行总结。

算法注释
最优算法不可实现,但可用作基准
NRU(最近未使用)算法LRU的很粗糙的近似
FIFO(先进先出)算法可能抛弃重要页面
第二次机会算法比FIFO有较大的改善
LRU(最近最少使用)算法很优秀,但很难实现
NFU(最不经常使用)算法LRU的相对粗略的近似
老化算法非常近似LRU的有效算法
工作集算法实现起来开销很大

最优算法

最优算法在当前页面中置换最后要访问到的页面。不幸的是,没有办法来判定哪个页面是最后一个要访问的,因此实际上该算法不能使用。然而,它可以作为衡量其他算法的基准。

NRU算法

NRU算法根据R位和M位的状态把页面分为四类。从编号最小的类中随机选择一个页面置换。该算法易于实现,但是性能不是很好,还存在更好的算法。

FIFO算法

FIFO算法通过维护一个页面的链表来记录它们装入内存的顺序。淘汰的是最老的页面,但是该页面可能仍在使用,因此FIFO算法不是一个好的选择。

第二次机会算法

第二次机会算法是对FIFO算法的改进,它在移出页面前先检查该页面是否正在被使用(判断R值)。如果该页面正在被使用,就保留该页面。这个改进大大提高了性能。

LRU算法

LRU算法是一种非常优秀的算法,但是只能通过特定的硬件来实现。如果机器中没有该硬件,那么也无法使用该算法。

NFU是一种近似于LRU的算法,它的性能不是非常好。还有一个主要问题是它从来不忘记任何事情,这可能反而使他置换有用的页面。

老化算法更近似于LRU并且可以更有效地实现,是一个很好的选择。

工作集算法

工作集算法有合理的性能,但它的实现开销较大。

总之,最好的两种算法是老化算法和工作集时钟算法(后面这个没有介绍),它们分别基于LRU和工作集。它们都具有良好的页面调度性能,可以有效地实现。也存在其他一些算法,但在实际应用中,这两种算法可能是最重要的。

  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值