天天看点

内存管理之页面置换算法

       前面我们提到了关于内存管理的一些知识,交换技术和虚拟内存是两种常用的处理内存过载的办法。对于虚拟内存,进行换入换出的基本单位是页面。当进程访问的页面没有被映射到内存时,操作系统必须在内存中选择一个页面换出内存,以便为即将要换入的页面提供空间。并且如果被换出的页面被修改过,还需要重写到磁盘上。然后把需要映射的页面换入到内存中,并修改进程的页表,然后再重新执行失败的指令。那么在操作系统决定换出那个页面是有讲究的,这就是我们接下来要讨论主题。

       当发生缺页中断时,我们可以随机挑选一个页面进行置换,但如果每次都选择不经常使用的页面会提升系统的性能。如果选择一个经常被访问的页面来进行置换,可能​​在不久后进程又访问了该页面,该页面就又需要从磁盘中被置换回内存中,因此置换经常访问页面会造成一些不必要的性能开销(我们完全可以通过妥善的安排操作系统来避免这种不必要的开销)。接下来我们就展开讨论集中目前比较成熟的页面置换算法。需要指出的是,“页面置换"问题在计算机设计的其他领域也有应用。在多数计算机中会把最近使用的32字节或者64字节缓存在高速缓存中来提升性能。当高速缓存存满之后就会出现”页面置换“。在Web服务器中会缓存最近被访问的页面,数据库服务器会缓存最近访问的数据库记录所在的页面,当这些页面存满缓存空间后,如果还需要存入新的页面,就会出现”页面置换“ 。

  • 最优页面置换算法

       很容易想象的一种高效的页面置换算法是最优页面置换算法,虽然这个页面不可能实现。该算法是这样工作的:有些页面可能在接下来的几个指令就会被访问,而有些页面可能会经过1000/10000条指令后才会被访问,每个页面都有一个在下一次被访问时需要执行的指令数作为标记。很显然,标记数越大说明该页面越晚被访问到。当发生缺页中断,选择标记数最大的页面进行置换,因为这样的页面最晚被使用到。这个算法的想法是十分出色的,但不足在于无法实现,因为在发生缺页中断时,操作系统无法得知每个页面在多少条指令后会被访问到(在进程调度算法时也遇到类似的难题,对于最短作业优先调度算法,操作系统无法那个进程的执行时间最短)。虽然这个页面置换算法在实际系统中无法使用,但是用它作为参照来评价其他的页面置换算法的优劣是很有用的。

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

       在前面介绍页表表项的结构时我们讲到有两个状态位对于操作系统进行页面置换时非常有帮助:访问位(R)和修改位(M)。当页面被访问时(读或写)修改访问位,当页面被修改(写)设置修改位。每次访问内存时都由硬件更新这些位。因此当某位被设置为1时,他就一直保持为1直到操作系统将它复位。可以利用M位(修改位)和R位(访问位)来构造一个简单的页面置换算法。当一个进程启动时,起初所有的页面都没有被访问,所有页面的M位和R位都为0。R位被周期性的清零(每个时钟周期清零)来区分最近一个时钟周期被访问的页面和未被访问的页面。当发生缺页中断时,操作系统检查页面的R位和M位的值,并把它们分为四类:

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

       第1类,看起来不太可能,但是它是确实存在的,在每个时钟周期对R位进行清零时,第3类就变为了第1类。每个时钟周期到来时,清零R位用于区分开最近一个时钟周期被访问的页面和未被访问的页面,但是不会清零M位,因为M位决定了页面被换出时是否需要写回到磁盘交换区。

       NRU (Not Recently Used,最近未使用)算法 随机从类编号最小的非空类中挑选一个页面进行页面置换。算法思路:在最近一个时钟滴答中(典型的时间为20ms),淘汰一个既没有被访问也没有被修改的页面比淘汰一个频繁使用的页面好。

优点:易于理解并且易于实现,虽然该算法不是性能最好的,但已经能够满足大多数场景。

  • 先进先出页面置换算法

       一种比较简单的且易实现的算法是FIFO(First-In First-out,先进先出)。由系统维护一个页面进入内存的顺序链表,可以是单独某个进程一个链表,也可以是整个系统一个链表。最新进入内存的页面放到链表的尾部,最早进入内存的页面放在链表的头部。当发生缺页中断时,淘汰链表头部的页面,并把新调入的页面加入到链表尾部。

缺点: FIFO算法会置换掉一些经常使用的页面,因此在系统中很少使用纯粹的FIFO算法。

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

        FIFO算法可能会把经常访问的页面换出,为了避免这种情况,可以对FIFO算法进行改进:当发生缺页中断时,检查链表头部的页面,如果该页面的R位为0,表示该页面进入内存时间最长并且在最近时间周期内未被访问,直接选择该页面进行页面置换。如果链表表头页面的R位为1,那么将该页面的R位清零同时把该页面由链表表头移动到链表尾部,并继续考察下一个链表头部页面。这种算法称之为二次机会(second chance)算法。第二次机会算法就是在寻找最近一个时钟间隔没有被访问的页面,如果所有的页面都被访问了,那么该算法就简化成了FIFO算法。

  • 时钟页面置换算法

       考虑第二次机会页面置换算法,它虽然是一个合理的算法,但是需要在链表中移动大量的页面,既降低了效率又不是很必要。因此对于第二次机会页面置换算法进行改进:把所有的链表元素都保持在类似于钟面的环形链表中,用一个指针指向最老的页面。当发生缺页中断时,首先检查指针指向的页面。如果该页面的R位为0,那么直接淘汰该页面,并把新加入的页面放入到该位置,指针指向下一个页面。如果R位为1,那么将R位清零,同时指针移向下一个页面。

内存管理之页面置换算法
  • 最近最少使用页面置换算法

        LRU(Least Recently Used,最近最少使用)页面置换算法 是对最优页面置换算法的一种近似的实现,该算法是基于如下的一种观察得出的:在前面几条指令频发访问的页面很可能在后面的若干指令也可能会访问到(与转换检测缓冲区TLB相似)。反过来,最近几条指令没有访问的页面可能在接下来很长一段时间内也不会被访问。基于这种观察,我们可以在发生缺页中断时,置换最近最少使用的页面。虽然这种LRU算法在理论上很好,但是实现的代价非常大,通常需要特殊的硬件支持,但大多数计算机却并不包含这样的硬件,因此我们可以使用一种软件的解决方案。NFU(Not Frequently Used,最不常用)算法,该算法将每个页面和一个软件计数器相关联,计数器的初始值为0。每当时钟中断时,由操作系统扫描内存中的所有页面,并把每个页面的R位加到计数器上,计数器大体上反映了页面的访问频率。每当发生缺页中断时,淘汰计数器较小的页面。NFU这种直接将R位的值加到计数器上的做法是不妥当的,需要使用老化(aging)算法 对NFU算法进行改进:首先在将页面的R位加到计数器之前现将计数器向右移一位 ,然后再把R位的值加到计数器的最左端而不是最右端。下图说明了老化算法是如何工作的。假设第一个时钟滴答后,页面0~5的R值为1、0 、1 、0 、1 、1。也就是说在时钟滴答0到时钟滴答1之间访问了页面0、 2 、4、 5,这些页面的R位被设置为1,其他页面的R位被设置为0。6个页面各自对应的计数器初始状态都为0,在第一个时钟滴答后各个计数器通过老化算法后的值如(a)所示,后面的(b),(c)......分别为第2,3,4......个时钟周期的计数器值。当发生缺页中断时,选择值最小的计数器对应的页面淘汰。

内存管理之页面置换算法

       该算法和LRU存在些许不同:在图(e)中的页面3和页面5,他们都已经两个时钟滴答没有被访问,而在两个时钟滴答前,两个页面都被访问了过。根据LRU,如果必须置换一个页面,就需要从页面3和页面5中选择一个。现在的问题是我们不知道时钟滴答1间和时钟滴答2间那个页面别访问了,因为每个时钟滴答只记录了一位,所以无法从一个时钟滴答中区分那个页面在较早的时间被访问和那个页面在较晚的时间被访问。而使用老化算法就可以知道应该置换页面3而不是页面5,因为页面5在更往前的几个时钟滴答间(时钟滴答1间)也被访问了而页面3却没有,对应到计数器的体现是页面5的计数器比页面3的计数器更大。同时老化算法的计数器是有限位的(如果计数器为8位,那就只能记录8个时钟滴答间的访问情况),这就限制了对以往的页面使用情况的记录。

  • 工作集页面置换算法

       一个进程当前正在使用的页面集合称之为它的工作集。如果整个工作集都被转入到内存,那么进程运行将不会产生太多的缺页中断。若内存太小不能容纳进程的的整个工作集,那么进程运行时会产生很多的缺页中断,原本可能只需要几纳秒就能执行完的指令现在可能需要10毫秒才能完成。如果程序执行几条指令就发生一次缺页中断,称程序发生了颠簸。有不少的分页系统都试图跟踪进程的工作集,确保再进程运行时,它的工作集页面就已经在内存中了。这种方法称之为工作集模型,其目的在于大大减少缺页中断率。在进程运行前预先将进程的工作集装入到内存的方法称之为预先调用,另一种方式是进程运行前工作集没有在内存中,当进程运行时再调入工作集到内存中,称之为请求调页。

       为了实现工作集模型,操作系统必须跟踪那些页面在工作集中,并可根据这些信息推导出一个合理的页面置换算法:在发生缺页中断时,淘汰一个不在工作集中的页面。为了实现该算法,就需要一种方法来确定那些页面在工作集中。根据定义,工作集就是最近k次内存访问使用过的页面集合。为了实现该算法,就必须事先确定k的值,一旦k的值确定了,每次访问内存后,最近k次内存访问访问过的页面也就确定了。但是不是有了工作集的定义就能够在程序运行期间高效的计算出工作集,理论上,可以通过特殊的寄存器记录过去k次内存访问使用过的页面,在发生缺页中断时,读取寄存器中记录的页面号,进行排序去重得到页面工作集,然而这种方式的开销极大,因此该技术几乎没有被采用过。作为一种替代,不再考虑最近k次的内存访问,而是考虑页面的执行时间。过去定义的工作集可能是:在过去的1000w次内存访问所使用的页面集合,而现在的定义可能是:在过去的10ms内存访问使用过的页面集合。例如对于某个进程从Tms时刻到T+100ms期间真正使用CPU的时间为40ms,对于工作集而言,这40ms称之为当前实际运行时间,因此我们在进行工作集模拟时也应该考虑在过去的τ秒内进程在当前实际运行时间使用过的页面。

       现在考虑该算法,该算法的基本思想是找出不在工作集中的页面并进行淘汰。因为只有在内存中的页面才可以作为被淘汰的候选者,因此该算法不考虑不在内存中的页面。对于进程的页表,每个表项包含一个R位、M位和一个上次使用该页面的时间,可以使用硬件来设置R位和M位,并且在每个时钟周期清零R位。算法进行如下的工作,当发生缺页中断时,扫描页表寻找一个合适的页面进行淘汰。对于每一个表项,如果R位为1,那么就把当前实际时间写入到表项的“上次使用时间”域,以表示缺页中断发生时该页面正在被使用。该页面在当前时钟滴答中被使用过,表面该页面应在工作集中,不应该被淘汰。当表项的R位为0时,那么就表明该表项对应的页面在当前的时钟滴答中没有被访问,可以作为被淘汰的候选页面。为了确定该页面是否能够被淘汰,需要计算页面的生存时间(当前实际时间-上次被访问的时间)并和τ进行比较,如果生存时间大于τ,表示该页面不在工作集中,该页面可以被淘汰。同时,扫描整个进程页表表项的的工作继续进行以更新剩余的表项。还有一种情况是生存时间小于等于τ,则说明该页面仍然在工作集中,这样的页面应该被保存起来,但是要记录生存时间最长的页面号。在扫描完进程页表的所有表项后,如果发现所有页面的生存时间都小于等于τ(所有页面都在工作集中),那么此时就应该淘汰生存时间最长的页面。更极端的情况,在当前时钟滴答中 ,所有的页面都被访问了(R位都为1),因此就随机选择页面进行淘汰,如果有可能的话,淘汰一个干净的页面。

  • 工作集时钟页面置换算法

       上面介绍的工作集页面置换算法中,当发生缺页中断时,需要扫描进程的整个页表一遍后才能决定淘汰那个页面,因此工作集算法是比较费时的。有一中改进的算法,它是一种时钟算法,同时又使用了工作集信息,称为WSClock算法,该算法实现简单,性能好,在实际的工作中有广泛的应用,接下来一起探讨该算法。

       与时钟算法一样,所需要的数据结构是一个包含页框信息的循环表。起初该表是空表,随着许多页面被访问,渐渐地,循环表中加入了许多表项,最终形成了一个环。每个表项包含来自工作集算法的上次访问时间、R位、M位等信息。该算法的工作流程:当发生缺页中断时,首先检测指针指向的表项。如果R位被设置为1,表明在过去的这个时钟滴答中该页面被访问了,该页面在工作集中。将该表项的R位置为0,指针指向下一个页面,并重复该过程。如果发现指针指向的表项的R位为0,同样需要计算该页面的生成时间,如果生存时间大于τ,表明该页面不出在工作集中,可以被被淘汰,然后再根据M位判断是否需要将页面写回到磁盘交换区中。如果M位为0,可以淘汰该页面,直接用新调入的页面覆盖该页面所对应的页框。如果M位为1,表示该页面被修改过,因此此页面在磁盘上没有有效的副本。为了避免由于写磁盘操作而引起进程的切换,指针移动到下一个表项,算法对下一个 表项所对应的页面进行检查,毕竟后面可能存在一个旧的且干净的页面可以使用。

  • 页面置换算法小结

算法 注释
最优算法 不能实现,可作为其他算法性能的参照标准
NRU算法 LRU的粗糙的近似实现
FIFO算法 可能会淘汰频繁访问的页面
第二次机会算法 在FIFO基础上改进,有很大的提升
时钟算法 是对第二次算法的改进,一种现实可行的算法
LRU算法 很优秀,缺点是难以实现
NFU算法 对于LRU相对粗糙的近似实现
老化算法 非常近似于LRU的有效算法
工作集算法 实现起来开销很大
工作集时钟算法 在工作集算法中引入时钟的概念,一种有效的算法

       在所有算法中,最优算法无疑是最完美的,在缺页中断时它选择最晚被访问的页面作为被淘汰的页面。但该算法却难以实现,因为操作系统无法知道进程在内存中的页面将在什么时候被访问。

        NRU(Not Recently Used,最近未使用)算法:使用页表项中的M位和R位作为算法的支撑,并根据R位和M位的值将进程的页面划分为四类,在发生缺页中断时淘汰分类编号最小且不为空的分类中的页面。该算法的优点在于简单且容易实现(虽然性能不一定最优)。

        FIFO算法 / 第二次机会算法 / 时钟算法:这三中算法有着一些联系,第二次机会算法是在FIFO算法的基础上进行改进的,时钟算法又是在第二次机会算法的基础上进行改进,因此时钟算法经过一些改进后是一个现实可行的算法。FIFO算法中,操作系统维护一个链表,链表记录了页面进入内存的顺序。当发生缺页中断时就直接淘汰最先进入内存的页面。FIFO算法的优缺点十分明显:该算法容易实现,但同时该页面也会淘汰掉经常被访问的页面,带来不必要的性能开销。

       第二次机会算法对FIFO算法提出改进:在发生缺页中断时,检查链表头部的页面的R位的值。如果R位为1表示该页面访问过,将该页面的R位清零,并把页面从链表头部摘除移动到链表尾部。如果链表头部的页面的R位为0,则可以直接淘汰掉该页面。第二次机会算法算法相对于FIFO算法能够避免一些被经常访问的页面被淘汰。但如果在发生缺页中断时,所有的页面的R位都为1,那么此时第二次机会算法就退化成了FIFO算法。

        时钟算法是在第二次机会算法进行改进,提出使用指针指向页面而不是在链表中移动页面节点的方式来减少性能开销。

        LRU算法 / NFU算法 / 老化算法:LRU(Least Recently Used)算法是指最近最少使用的算法,当发生缺页中断时置换最近最长时间未被使用的页面。但该算法实现起来代价很高。

        NFU(Not Frequently  Used)算法作为LRU算法的一种软件实现,该算法将每个页面和一个计数器相关联,每次时钟中断时,操作系统扫描所有页面的R位(0 or 1)并把它们累加到计数器,该计数器就大致体现了页面的被访问的频率程度。在发生缺页中断时就置换计数器值最小的算法。NFU算法的缺点:对于一些页面,在过去的几个时钟周期内(0-T1)可能被频繁访问,而在最近几个时钟周期内(T1-T2)可能很少被访问,那么在发生缺页中断时,这些页面的计数器值就不能反映在最近几个时钟周期内(T1-T2)页面访问的频繁程度,因为计数器反映的是过去整个时间的访问频率(0-T2)而非某个时间段的访问频率(T1-T2)。

       老化(aging)算法:作为对NFU算法的改进,在每次扫描页面的R位并累计到计数器时做出一些改动。在将页面的R位的值加到计数器之前现将页面的计数器向右移一位,然后把页面的R位的值加到计数器的最左一位上。这样,通过改动后的计数器就能够反映过去n(n的值取决于计数器使用多少位进行表示,如果计数器使用16bit进行表示,则n=16)个时钟周期里页面的访问频率。计数器值越大表明页面在最近n个时钟周期内访问的频繁程度越高,计数器值越小表明页面在最近n个时钟周期内访问的频繁程度越低。当发生缺页中断时,比较各个页面的计数器的值,然后将计数器值最小的页面淘汰。

       工作集算法/工作集时钟算法:工作集算法引入了工作集模型,一个进程当前正在使用的页面集合称之为工作集。该算法在发生缺页中断时,淘汰不在进程工作集中的页面。为了实现该算法需要通过一种方式确定到底哪些页面在工作集中。一种可以采用的方法是在过去执行的k次内存访问中,被访问到的页面就是工作集。当k的值确定了,每次发生内存访问时,过去k次内存访问的页面也就随即确定了。另一种方式是考虑考虑页面执行时间,例如工作集定义为在过去10ms内访问过的页面。在该算法的实现中,每个页表表项都包含M位、R位和上次使用该页面时间等信息。在发生缺页中断时,以此扫描每个表项,如果R位为1,那么表示在该页面在当前时钟周期内被访问了,该页面应该在进程的工作集中,不应被淘汰,因此将当前时间写入到表项的页面上次使用时间域。如果R位为0,则计算页面的生存时间(当前时间时间-上次页面使用时间),并与时间间隔τ进行比较。如果生存时间大于τ则可以淘汰该页面,如果所有页面的生存时间都小于等于τ则选择生存时间最长的页面进行淘汰。工作集算法的缺点是需要扫描完整个页表才能确定要被淘汰的页面。                           

       工作集时钟算法是在工作集算法的基础上加入了时钟的概念,该算法使用了循环表,并用指针指向表中的元素。当发生缺页中断时检测指针指向的元素是否满足被淘汰的条件,如果满足则淘汰指针执行的页面,指针移动到下一个表项。如果不满足淘汰条件,指针移动到下一个表项并重复该工作。

继续阅读