虚拟内存管理器中,当您拥有大量空闲内存时,生活很简单。出现页面错误时,您会在空闲页面列表中找到一个空闲页面,并将其分配给错误页面。嘿,操作系统,恭喜你!你又做了一次。
不幸的是,当小内存是免费的时候,事情变得更有趣了。在这种情况下,这种内存压力迫使操作系统开始分页,为使用活动的页面腾出空间。决定将哪些页面(或页面)驱逐出去,封装在操作系统的替换策略中;从历史上看,这是早期虚拟内存系统所做的最重要的决定之一,因为旧系统几乎没有物理内存。
最低限度,这是一套有趣的政策,值得多了解一些。因此我们的问题:
关键是:如何决定驱逐哪一页。
操作系统如何决定将哪个页面(或页面)从内存中驱逐出去?这个决策是由系统的替换策略做出的,它通常遵循一些通用的原则(下文将讨论),但也包括某些调整以避免基本情况的行为。
22.1缓存管理
在深入研究政策之前,我们首先描述一下我们正在尝试的问题。 更详细地解决。由于主存保存了系统中所有页面的一部分,因此可以正确地将其视为系统中虚拟内存页的缓存。因此,我们为这个缓存选择替换策略的目标是最小化缓存遗漏的数量,即,为了最小化我们从磁盘取出一个页面的次数。另一方面,我们可以将我们的目标视为最大化缓存命中的数量,即。,可以在内存中找到访问页面的次数。知道缓存命中和错误的数量,让我们计算一个程序的平均内存访问时间(AMAT)(一个度量计算机架构师计算硬件缓存[HP06])。具体地说,考虑到这些 值,我们可以计算一个程序的AMAT:
当TM表示访问内存的成本时,TD访问磁盘的成本,并且PMiss没有在缓存中找到数据的概率(一个失误)。PMiss在0.0到1.0之间变化,有时我们指的是百分率,而不是概率(例如,10%的缺失率意味着PMiss = 0.10)。注意,您总是要支付访问内存中的数据的成本;但是,当您错过时,您必须额外支付从磁盘获取数据的成本。例如,让我们想象一个有一个(微小的)地址空间的机器:4KB,有256字节的页面。因此,虚拟地址有两个组件:一个4位的VPN(最重要的位)和8位偏移量(最不重要的位)。因此,这个示例中的流程可以访问24或16个总的虚拟页面。在本例中,流程生成以下内存引用(即:(虚拟地址):0x000, 0x100, 0x200, 0x300, 0x400, 0x500, 0x600, 0x700, 0x800, 0x900。这些虚拟地址是指地址空间前10页的第一个字节(页码是每个虚拟地址的第一个十六进制数字)。让我们进一步假设,除了virtual page 3之外的每个页面都已经在内存中了。因此,我们的内存引用序列将会遇到以下行为:命中、命中、命中、命中、命中、命中、命中、命中、命中、命中。我们可以计算命中率(在内存中找到的引用的百分比):90%,10个引用中有9个在内存中。因此,漏报率为10% (PMiss = 0.1)。一般来说,PHit + PMiss = 1.0;命中率加上漏网率为100%。要计算AMAT,我们需要知道访问内存的成本和访问磁盘的成本。假设访问内存(TM)的成本大约为100纳秒,而访问磁盘(TD)的成本大约为10毫秒,我们有以下AMAT: 100ns + 0.1·10ms,即100ns + 1ms,或1.0001毫秒,或大约1毫秒。如果我们的命中率是99.9% (Pmiss = 0.001),结果就大不相同了:AMAT是10.1微秒,大约是快100倍。当命中率接近100%时,AMAT接近100纳秒。不幸的是,正如您在本例中所看到的,在现代系统中磁盘访问的成本是如此之高,以至于即使是很小的失败率也会很快在运行程序的整个AMAT中占据主导地位。显然,我们需要避免尽可能多的遗漏,或者以磁盘的速度慢速运行。有一种方法可以帮助我们,如我们现在所做的那样,谨慎地制定一个明智的政策。
22.2最优替代政策:
为了更好地理解一个特定的替代策略是如何工作的,最好将其与最佳的替代策略进行比较。事实证明,这种最优的政策是多年前由Belady开发的[B66](他最初称之为MIN)。最优的替代政策导致总体上的失误数最少。Belady展示了一个简单的(但不幸的是,难以实现!)的方法,取代了未来将被访问最远的页面,这是最优的策略,导致了可能出现的最少的缓存丢失。希望,最优策略背后的直觉是有意义的。你可以这样想:如果你必须扔掉某一页,为什么不把最需要的那一页扔掉呢?通过这样做,您实际上是在说,缓存中的所有其他页面都比最远的页面更重要。原因很简单:您将在引用之前的其他页面。让我们通过一个简单的例子来了解最优策略的决策。假设一个程序访问下列虚拟页流:0、1、2、0、1、3、0、3、1、2、1。图22.1显示了最优的行为,假设缓存适合三页。
在图中,您可以看到以下操作。不足为奇的是,当缓存从一个空的状态开始时,前三个访问都是错误的;这样的失误有时被认为是一种冷启动的失误(或强制失误)。然后再次引用第0和第1页,它们都在缓存中命中。最后,我们到达另一个错误(到第3页),但是这次缓存已经满了;替换必须发生。这就引出了一个问题:我们应该替换哪个页面?使用最优策略,我们检查当前缓存中的每个页面(0,1和2)的未来,并且看到0几乎是立即被访问的,1是稍后访问的,2是在将来访问的最远的。因此,最优策略有一个简单的选择:驱逐第2页,在缓存中产生0、1和3页。接下来的三个引用都是命中的,但是我们到了第2页,我们很久以前就把它赶出去了,然后又一次错过了。
在最优策略再次检查缓存中的每个页面的未来(0、1和3),并看到只要它不驱逐第1页(即将被访问),我们就可以了。我们很久以前就把它赶出去了,又遭受了另一次的损失。在这里,最优策略再次检查每个页面的未来。 缓存(0,1,和3),并看到只要它不驱逐第1页(它)。 即将被访问),我们会没事的。这个例子显示第3页被驱逐,尽管0也是一个不错的选择。最后,我们点击第1页,跟踪完成。我们还可以计算出缓存的命中率:6次命中和5次失误,命中命中率为6+5 6或54.5%。您还可以计算命中率的强制缺失(即:,忽略第一个错过的页面),结果是85.7%的命中率。不幸的是,正如我们以前在制定调度策略时所看到的,未来是不为人所知的。不幸的是,正如我们以前在制定调度策略时所看到的,未来是不为人所知的。因此,在开发一个真正的可部署的策略时,我们将着重于找到其他方法来决定将哪个页面驱逐出去。因此,最优政策只能作为一个比较点,以了解我们离完美有多近。
22.3一个简单的政策:FIFO许多早期的系统避免了试图接近最优和采用非常简单的替换策略的复杂性。例如,一些系统使用FIFO(先入先出)替换,在这些系统进入系统时,页面被简单地放置在队列中;当替换发生时,队列尾部的页面(第一个页面)被逐出。FIFO有一个强大的优势:它的实现非常简单。让我们来看看FIFO在我们的示例引用流中是如何做的(图22.2,第5页)。我们再次开始跟踪,其中3个强制的错误是0、1和2,然后分别在0和1中进行攻击。接下来,第3页被引用,导致一个失误;替换的决定对FIFO很容易。选择页面的“第一个”(图中的缓存状态保存在FIFO秩序,与先进左边页),这是页面0。不幸的是,我们的下一个访问是第0页,导致另一个失误和替换(第1页)。然后我们点击第3页,但是在1和2上,最后点击3。
将FIFO与最佳值进行比较,FIFO的情况明显更糟:达到36.4%的命中率(或不包括强制失误的57.1%)。FIFO根本不能确定块的重要性:尽管第0页已经被访问了很多次,FIFO仍然将它踢出,仅仅因为它是第一个进入内存的
22.4另一个简单的政策:随机
另一个类似的替代策略是随机选择一个。 随机页面在内存压力下替换。随机有属性 类似于先进先出;实现起来很简单,但实际上并不是这样。 太聪明了,不能选择驱逐哪个街区。我们来看看随机事件。 在我们著名的示例引用流上(参见图22.3)。
当然,随机选择的随机性完全取决于随机选择的运气(或不幸运)。在上面的例子中,Random做的比FIFO好一点,比最优的稍微差一些。事实上,我们可以做随机实验数千次,并决定它的一般情况。图22.4显示了随机获得的随机种子有多少次,每一种都有不同的种子。正如您所看到的,有时(仅仅超过40%的时间),Random是最佳的,在示例跟踪中实现了6个点击;有时会更糟,达到2次或更少。抽奖的运气如何取决于抽奖的运气?
22.5使用历史:LRU。
不幸的是,任何像FIFO或Random这样简单的策略都可能有一个共同的问题:它可能会踢出一个重要的页面,一个即将被再次引用的页面。FIFO踢出了最先引入的页面;如果这恰好是一个具有重要代码或数据结构的页面,那么它就会被丢弃,尽管它很快就会被重新输入。因此,FIFO、Random和类似的策略不太可能接近最佳状态;聪明的东西是必要的。
就像我们在制定日程计划时所做的那样,为了提高我们对未来的猜测,我们再次依赖过去,将历史作为我们的向导。例如,如果一个程序最近访问了一个页面,它很可能在不久的将来再次访问它。
一种分页替换策略可以使用的一种历史信息是频率;如果一个页面被多次访问过,它可能不应该被替换,因为它显然具有一定的价值。页面更常用的属性是访问权限;最近一个页面被访问了,它可能更有可能被再次访问。
这个家庭的政策是基于人们所说的地方原则[D70],这基本上只是一个关于程序及其行为的观察。这个原则非常简单地说,程序倾向于访问某些代码序列(例如,在一个循环中)和数据结构(例如,一个由循环访问的数组);因此,我们应该试着用历史来找出哪些页面是重要的,并将这些页面留在内存中,当它被驱逐的时候。
因此,一个基于历史的简单算法家族诞生了。最不常用的(LFU)策略取代了最不经常使用的页面,当必须进行驱逐时。类似地,最近最少使用的(LRU)策略将替代最近使用的页面。这些算法很容易记住:一旦你知道了它的名字,你就会知道它的作用,这是一个很好的名字的属性。
为了更好地理解LRU,我们来看看LRU是如何在我们的考试中进行的。
:比如参考流。图22.5(第7页)显示了结果。从图中可以看出,LRU如何使用历史来比无状态策略(如Random或FIFO)更好。在这个例子中,LRU将第2页移除,因为它首先要替换一个页面,因为最近已经访问了0和1。然后它将替换第0页,因为最近已经访问了1和3。在这两种情况下,基于历史的LRU决定都是正确的,因此下一个引用就会被击中。因此,在我们的简单示例中,LRU可以尽可能地匹配最佳性能。我们还应该注意到,这些算法的对立性存在:最常用的(MFU)和最近使用的(MRU)。在大多数情况下(不是全部!),这些政策效果不佳,因为他们忽视了当地的大多数节目,而不是拥抱它。
22.6工作负载的例子。
让我们再看几个例子,以便更好地理解其中一些策略的行为。在这里,我们将检查更复杂的工作负载,而不是小的跟踪。然而,即使是这些工作负载也大大简化了;一个更好的研究将包括应用程序跟踪。
我们的第一个工作负载没有位置,这意味着每个引用都是在被访问的页面集合内的一个随机页面。在这个简单的示例中,工作负载在一段时间内访问100个惟一的页面,选择下一页以随机引用;总共访问了10,000个页面。在这个实验中,我们将缓存大小从很小的(1页)更改到足够容纳所有唯一的页面(100页),以便查看每个策略在缓存大小范围内的行为。
图22.6绘制了最优、LRU、Random和FIFO的实验结果。图的y轴显示每个策略达到的命中率;x轴会根据上面的描述改变缓存大小。
们可以从图中得出一些结论。首先,当工作负荷没有局部性时,你所使用的现实政策并不重要;LRU、FIFO和Random都执行相同的操作,命中率完全取决于缓存的大小。其次,当缓存足够大以适应整个工作负载时,它也不影响您使用哪个策略;所有的策略(甚至是随机的)在所有被引用的块都适合缓存时,会收敛到100%的命中率。最后,您可以看到,最优性能明显优于实际策略;瞥见了砰的一声。
我们检查的下一个工作负载称为80-20工作负载,它显示了位置:80%的引用被占到页面的20%(热页面);剩下的20%的参考资料都是留给剩下的80%的页面(冷页)。在我们的工作量中,总共有100个唯一的页面;因此,大部分时间都引用了热页面,剩下的则是冷页。图22.7(第10页)显示了这些策略如何执行此工作负载。从图中可以看出,虽然random和FIFO都做得很好,但LRU做得更好,因为它更有可能保持在热点页面上;由于这些页在过去经常被提及,它们很可能在不久的将来再次被提及。最优再一次做得更好,显示LRU的历史信息并不完美。
你现在可能会想:LRU是否比Random和FIFO的改进更大?答案和往常一样,这要视情况而定。如果每个失误都是非常昂贵的(并不少见),那么即使是命中率的一点点提高(降低率)也会对性能产生巨大的影响。如果失误不那么昂贵,那么当然LRU的好处也不那么重要。让我们看一看最后的工作量。我们将其称为循环顺序工作负载,就像在它中一样,我们引用了50页序列,从0开始,然后是1,…,直到第49页,然后我们循环,重复这些访问,总共有10,000个访问到50个唯一的页面。图22.8中的最后一个图显示了该工作负载下的策略的行为。这个工作负载在许多应用程序中都很常见(包括重要的商业应用程序,如数据库[CD85]),它代表了LRU和FIFO的一个worstcase。这些算法,在一个循环顺序工作负载下,将旧页面踢出;不幸的是,由于工作负载的循环特性,这些旧页面将比策略更倾向于保存在缓存中的页面更早地被访问。实际上,即使缓存大小为49,一个循环顺序的50页的工作负载也会导致0%的命中率。有趣的是,随机的票价明显好一些,不是很接近最优,但至少达到了非零的命中率。结果是随机有一些很好的性质;一个这样的性质是没有怪异的角落行为。
22.7实现历史算法
如您所见,像LRU这样的算法通常比FIFO或Random之类的简单策略做得更好,后者可能会抛出重要的页面。不幸的是,历史政策给我们带来了一个新的挑战:我们如何实现它们。举个例子,LRU。要实现完美,我们需要做大量的工作。具体地说,在每个页面访问(即:我们必须更新一些数据结构,将这一页移到列表的前面(即:个系统方面)。将此与FIFO形成对比,在这个FIFO中,只有当一个页面被清除(通过删除第一个页面)或将新页面添加到列表中时,才会访问到FIFO列表(在最后一个页面中)。为了记录哪些页面最少,以及最近使用的最频繁,系统需要做一些会计工作。
有一种方法可以帮助加快这个速度,那就是增加一点硬件支持。例如,机器可以在每个页面访问上更新内存中的一个时间字段(例如,这可能在每个进程页表中,或者在内存中的某个单独的数组中,在系统的每个物理页上有一个条目)。因此,当一个页面被访问时,时间字段将由硬件设置为当前时间。然后,在替换页面时,OS可以简单地扫描系统中的所有时间字段,查找最近使用的页面。
不幸的是,随着系统中页面数量的增长,扫描大量的时间来查找最近使用的页面是非常昂贵的。想象一下有4GB内存的现代机器,被切成4KB的页面。这台机器有100万页,因此即使在现代CPU速度下,找到LRU页面也需要很长时间。这就引出了一个问题:我们真的需要找到最古老的页面来替代吗?我们能以近似的方式生存吗?
22.8近似LRU
事实证明,答案是肯定的:从计算开销的角度来看,近似LRU更可行,而且确实是许多现代系统所做的。这个想法需要一些硬件支持,以使用位的形式(有时称为引用位),第一个系统是在第一个系统中实现的,其中有分页,Atlas onelevel store [KE+62]。在系统的每一页中有一个使用位,而使用位在内存中存在(例如,它们可能位于每个进程的页表中,或者只是在某个数组中)。每当引用一个页面时(即:,读或写),使用位由硬件设置为1。不过,硬件从来没有清理过。,设置为0);这是操作系统的责任。操作系统如何使用bit来近似LRU?嗯,可能有很多方法,但是有了时钟算法[C69],建议了一个简单的方法。想象一下,系统中所有的页面都排列在一个循环列表中。一个时钟指针指向某个特定的页面(它并不重要)。当必须进行替换时,操作系统会检查当前指向的页面P是否有一个使用点为1或0。如果1,这意味着页P最近被使用,因此不是一个合适的替代人选。因此,使用一些P设置为0(清除),而时钟手增加到下一个页面(P + 1)。该算法仍在继续,直到找到一个用位设置为0,这意味着这个页面没有被最近使用(或者,在最坏的情况下,所有页面,我们现在在整个组页面,清除所有的比特)。请注意,这种方法并不是使用bit来近似LRU的唯一方法。实际上,任何定期清除使用位的方法,然后区分哪些页面使用了1与0之间的位元来决定要替换哪些内容,都是可以的。Corbato s的时钟算法仅仅是一种早期的方法,它获得了一些成功,并且拥有了不重复扫描所有内存寻找未使用页面的良好特性。
时钟算法变体的行为如图22.9所示。这种变体在进行替换时随机扫描页面;当它遇到一个具有引用位设置为1的页面时,它会清除这个位(即。,设置为0);当它找到一个带有参考位设置为0的页面时,它选择它作为它的受害者。正如你所看到的,虽然它不像完美的LRU那样完美,但是它比那些不考虑历史的方法要好。
22.9考虑脏页
对时钟算法的一个小修改(也最初是由Corbato [C69]提出的),通常是在内存中对页面是否进行了修改的额外考虑。这样做的原因是:如果一个页面已经被修改了,而且是脏的,那么必须将它写回磁盘,以驱逐它,这是很昂贵的。如果它没有被修改(因此是干净的),驱逐是免费的;物理框架可以简单地用于其他目的,而不需要额外的I/O。因此,一些VM系统倾向于在脏页上清除干净的页面。为了支持这种行为,硬件应该包括一个修改的位(也就是脏位)。这个位被设置为任何时间一个页面被写入,因此可以被合并到页面替换算法中。例如,可以将时钟算法更改为对未使用和清除的分页进行扫描;如果没有找到这些,那么对于未使用的页面,它们是脏的,等等。
22.10其他VM政策
分页替换并不是VM子系统使用的唯一策略(尽管它可能是最重要的)。例如,操作系统还必须决定何时将页面带入内存。这个策略有时被称为页面选择策略(因为它是Denning [D70]调用的),它提供了一些不同选项的操作系统。对于大多数页面,操作系统只是使用了需求分页,这意味着操作系统在被访问时,按需要将页面带入内存。当然,操作系统可以猜测一个页面即将被使用,从而提前将其引入;这种行为被称为预取,只有在有合理的成功机会时才应该进行。例如,一些系统会假定,如果将代码页P引入内存,那么可能很快就会访问代码页P +1,因此也应该将其带入内存。另一个策略决定了操作系统如何将页面写入磁盘。当然,他们可以一次只写一个;然而,许多系统会在内存中收集许多挂起的写操作,并将它们写入磁盘(更高效)。这种行为通常被称为集群或简单地分组编写,并且由于磁盘驱动器的特性而有效,因为磁盘驱动器执行单个大的写比许多小的写效率更高。
22.11不足
在结束之前,我们要讨论一个最后的问题:当内存被简单地超额预订时,操作系统应该做什么,而运行过程的内存需求是否超过了可用的物理内存?在这种情况下,系统将不断地分页,这种情况有时称为抖动[D70]。一些早期的操作系统有一套相当复杂的机制来检测和处理它发生时的抖动。例如,给定一组流程,系统可以决定不运行流程的子集,希望简化的过程工作集(它们正在使用的页面)适合于内存,从而可以取得进展。这种方法,通常被称为“接纳控制”(admission control),它指出,有时做得不如做的好,而不是试着把每件事都做得很糟糕,这是我们在现实生活中经常遇到的情况,在现代计算机系统中也是如此(可惜)。一些当前的系统对内存过载采取了更严厉的方法。例如,某些版本的Linux在内存过多时运行内存不足的杀手;这个守护进程选择一个内存密集的进程并杀死它,从而以一种不太微妙的方式减少内存。虽然成功地降低了内存压力,但这种方法可能存在问题,例如,它会杀死X服务器,从而导致任何需要显示的应用程序无法使用。
22.12总结
我们已经看到了一些页面替换(和其他)策略的引入,这些策略是所有现代操作系统的VM子系统的一部分。现代系统增加了一些简单的LRU近似,如时钟;例如,扫描电阻是许多现代算法的重要组成部分,如ARC [MM03]。抗扫描算法通常是类似于LRU的,但也尽量避免LRU的最坏情况,这是我们看到的循环顺序工作负载。因此,页面置换算法的发展仍在继续。
然而,在许多情况下,由于内存访问和磁盘访问时间之间的差异增加,说算法的重要性已经降低。由于对磁盘的分页非常昂贵,因此频繁分页的成本非常高。因此,对过度分页的最佳解决方案通常是一个简单的(如果不满足):购买更多内存。