type
status
date
slug
summary
tags
category
icon
password
Property
Mar 28, 2023 12:55 AM
由于内存只包含系统中所有页的子集,因此可以将其视为系统中虚拟内存页的
缓存
(cache)。因此,在为这个缓存选择替换策略时,我们的目标是让缓存未命中
(cache miss)最少,即使得从磁盘获取页的次数最少。或者,可以将目标看成让缓存命中
(cache hit)最多,即在内存中找到待访问页的次数最多。这章讲的是 cache,就是加速硬盘读取,以页为单位,在内存中创建硬盘页的缓存。
缓存管理
将
物理内存页
作为硬盘内存页
的缓存cache
能直接从物理内存页中找到即为
缓存命中
,未找到即为缓存未命中
就可以计算程序的
平均内存访问时间
(Average Memory Access Time,AMAT):其中 表示访问
内存
的成本
, 表示访问磁盘
的成本
, 表示在缓存中找到数据的概率
(命中
), 表示在缓存中找不到数据的概率
(未命中
)。 和 从 0.0 变化到 1.0,并且 + = 1.0。在现代系统中,
磁盘
访问的成本非常高
,即使很小概率的未命中也会拉低正在运行的程序的总体 AMAT
。显然,我们必须尽可能地避免缓存未命中,避免程序以磁盘的速度运行。最优替换策略
为了更好地理解一个特定的替换策略是如何工作的,将它与最好的替换策略进行比较是很好的方法。事实证明,这样一个最优(optimal)策略是 Belady 多年前开发的(原来这个策略叫作 MIN)。最优替换策略能达到总体未命中数量最少。Belady 展示了一个简单的方法(但遗憾的是,很难实现)
最优策略:
替换内存中在
最远将来
才会被访问到的页,可以达到缓存未命中率最低。提示:与最优策略对比非常有用 虽然最优策略非常不切实际,但作为仿真或其他研究的比较者还是非常有用的。比如,单说你喜欢的新算法有 80%的命中率是没有意义的,但加上最优算法只有 82%的命中率(因此你的新方法非常接近最优),就会使得结果很有意义,并给出了它的上下文。因此,在你进行的任何研究中,知道最优策略可以方便进行对比,知道你的策略有多大的改进空间,也用于决定当策略已经非常接近最优策略时,停止做无谓的优化[AD03]。
遗憾的是,正如我们之前在开发调度策略时所看到的那样,未来的访问是
无法知道
的,你无法为通用操作系统实现最优策略。在开发一个真正的、可实现的策略时,我们将聚焦于寻找其他决定把哪个页面踢出的方法。因此,最优策略只能作为比较
,知道我们的策略有多接近“完美”
。简单策略:FIFO
页在进入系统时,简单地放入一个队列。当发生替换时,队列尾部的页(“先入”页)被踢出。
FIFO 有一个很大的优势:实现相当简单。
先进先出(FIFO)根本无法确定页的
重要性
:即使一个页已被多次访问,FIFO 仍然会将其踢出,因为它是第一个进入内存的。另一简单策略:随机
在内存满的时候它随机选择一个页进行替换。
随机具有类似于 FIFO 的属性。实现我来很简单,但是它在挑选替换哪个页时不够智能。
有些时候(仅仅 40%的概率),随机和最优策略一样好,有时候情况会更糟糕,随机策略取决于当时的运气。
利用历史数据:LRU
遗憾的是,任何像 FIFO 或随机这样简单的策略都可能会有一个共同的问题:它可能会踢出一个重要的页,而这个页马上要被引用。先进先出(FIFO)将先进入的页踢出。如果这恰好是一个包含重要代码或数据结构的页,它还是会被踢出,尽管它很快会被重新载入。因此,FIFO、Random 和类似的策略不太可能达到最优,需要更智能的策略。
正如在调度策略所做的那样,为了提高后续的命中率,我们再次通过
历史的访问情况
作为参考。例如,如果某个程序在过去访问过某个页,则很有可能在不久的将来会再次访问该页。
页替换策略可以使用的一个历史信息是频率
(frequency)。如果一个页被访问了很多次,也许它不应该被替换,因为它显然更有价值。页更常用的属性是访问的近期性
(recency),越近被访问过的页,也许再次访问的可能性也就越大。
这一系列的策略是基于人们所说的局部性原则
(principle of locality)[D70],基本上只是对程序及其行为的观察。这个原理简单地说就是程序倾向于频繁地访问某些代码(例如循环)和数据结构(例如循环访问的数组)。因此,我们应该尝试用历史数据
来确定哪些页面更重要,并在需要踢出页时将这些页保存在内存中。- 最不经常使用(Least-Frequently-Used,LFU)策略会替换
最不经常使用的页
。
- 最少最近使用(Least-Recently-Used,LRU)策略替换
最近最少
使用的页面。
工作负载示例
让我们再看几个例子,以便更好地理解这些策略。在这里,我们将查看更复杂的工作负载(workload),而不是追踪小例子。但是,这些工作负载也被大大简化了。更好的研究应该包含应用程序追踪。
图 22.2 展示了最优、LRU、随机和 FIFO 策略的实验结果。图 22.2 中的 y 轴显示了每个策略的命中率。如上所述,x 轴表示缓存大小的变化。
- 当工作负载不存在局部性时,使用的策略区别不大。LRU、FIFO 和随机都执行相同的操作,命中率完全由缓存的大小决定。
- 当缓存足够大到可以容纳所有的数据时,使用哪种策略也无关紧要,所有的策略(甚至是随机的)都有 100%的命中率。
- 你可以看到,最优策略的表现明显好于实际的策略。如果有可能的话,偷窥未来,就能做到更好的替换。
“80—20”负载场景
,它表现出局部性
:80%的引用是访问 20%的页(“热门”页)。剩下的 20%是对剩余的 80%的页(“冷门”页)访问。从图 22.3 中可以看出,尽管随机和 FIFO 都很好地运行,但 LRU 更好,因为它更可能保持热门页。由于这些页面过去经常被提及,它们很可能在不久的将来再次被提及。优化再次表现得更好,表明 LRU 的历史信息并不完美。
“循环顺序”工作负载
,其中依次引用 50 个页,从 0 开始,然后是 1,…,49,然后循环,重复访问。展示了 LRU 或者 FIFO 的最差情况。这种工作负载在许多应用程序(包括重要的商业应用,如数据库[CD85])中非常常见,展示了 LRU 或者 FIFO 的最差情况。这些算法,在循环顺序的工作负载下,踢出较旧的页。遗憾的是,由于工作负载的循环性质,这些
较旧的页
将比因为策略决定保存在缓存中的页更早被访问。事实上,即使缓存的大小是 49 页,50 个页面的循环连续工作负载也会导致 0%的命中率。有趣的是,随机策略明显更好,虽然距离最优策略还有距离,但至少达到了非零的命中率。可以看出随机策略有一些不错的属性,比如不会出现特殊情况下奇怪的结果。实现基于历史信息的算法
为了实现LRU,我们需要做很多工作。
具体地说,在每次页访问(即每次内存访问,不管是取指令还是加载指令还是存储指令)时,我们都必须更新一些数据,从而将该页移动到
列表的前面
(即 MRU 侧)。为了记录哪些页是最少和最近被使用,系统必须对每次内存引用做一些记录工作。显然,如果不十分小心,这样的记录反而会极大地影响性能。硬件可以在
每个页访问
时更新内存中的时间字段
(时间字段可以在每个进程的页表中,或者在内存的某个单独的数组中,每个物理页有一个)。因此,当页被访问时,时间字段将被硬件设置为当前时间。然后,在需要替换页时,操作系统可以简单地扫描系统中所有页的时间字段以找到最近最少使用的页。遗憾的是,随着系统中页数量的增长,扫描所有页的时间字段只是为了找到最精确最少使用的页,这个代价太昂贵。
近似 LRU
从计算开销的角度来看,近似 LRU 更为可行,实际上这也是许多现代系统的做法。
这个想法需要硬件增加一个
使用位
(use bit,有时称为引用位,reference bit),这种做法在第一个支持分页的系统 Atlas one-level store
[KE + 62]中实现。系统的每个页有一个使用位,然后这些使用位存储在某个地方(例如,它们可能在每个进程的页表中,或者只在某个数组中)。每当页被引用(即读或写)时,硬件将使用位设置为1
。但是,硬件不会清除该位(即将其设置为 0),这由操作系统负责。时钟算法(clock algorithm)
系统中的所有页都放在一个
循环列表
中。时钟指针
(clock hand)开始时指向某个特定的页(哪个页不重要)。当必须进行页替换时,操作系统检查当前指向的页 P 的
使用位是 1 还是 0
。如果是 1,则意味着页面 P 最近被使用,因此不适合被替换。然后,P 的使用位
设置为 0
,时钟指针递增到下一页(P + 1)。该算法一直持续到找到一个使用位为 0
的页,使用位为 0 意味着这个页最近没有被使用过(在最坏的情况下,所有的页都已经被使用了,那么就将所有页的使用位都设置为 0)。时钟算法的一个变种的行为。该变种在需要进行页替换时
随机扫描
各页,如果遇到一个页的引用位为 1,就清除该位(即将它设置为 0)。直到找到一个使用位为 0的页,将这个页进行替换。如你所见,虽然时钟算法不如完美的 LRU 做得好,但它比不考虑历史访问的方法要好。考虑脏页
时钟算法的一个小修改(最初也由 Corbato [C69]提出),是对内存中的页
是否被修改
的额外考虑。这样做的原因是:如果页已被修改(modified)并因此变脏(dirty),则踢出它就必须将它写回磁盘,这很昂贵。如果它没有被修改(因此是干净的,clean),踢出就没成本。物理帧可以简单地重用于其他目的而无须额外的 I/O。因此,一些虚拟机系统更倾向于踢出干净页,而不是脏页。
为了支持这种行为,硬件应该包括一个
修改位
(modified bit,又名脏位,dirty bit)。每次写入页时都会设置此位,因此可以将其合并到页面替换算法中。例如,时钟算法可以被改变,以扫描既未使用又干净的页先踢出。无法找到这种页时,再查找脏的未使用页面,等等。其他虚拟内存策略
页面替换不是虚拟内存子系统采用的唯一策略(尽管它可能是最重要的)。
操作系统还必须决定何时将页载入内存。该策略有时称为
页选择
(page selection)策略,它向操作系统提供了一些不同的选项。
对于大多数页而言,操作系统只是使用按需分页(demand paging),这意味着操作系统在页被访问时将页载入内存中,“按需”即可操作系统可能会猜测一个页面即将被使用,从而提前载入。这种行为被称为
预取
(prefetching),只有在有合理的成功机会时才应该这样做。例如,一些系统将假设如果代码页 P 被载入内存,那么代码页 P + 1 很可能很快被访问,因此也应该被载入内存。另一个策略决定了操作系统如何将
页面写入磁盘
。以一种(更高效)的写入方式将它们写入硬盘:这种行为通常称为聚集
(clustering)写入,或者就是分组写入
(grouping),这样做有效是因为硬盘驱动器的性质,执行单次大的写操作,比许多小的写操作更有效。抖动
当内存就是被超额请求时,这组正在运行的进程的内存需求超出了可用物理内存?在这种情况下,系统将不断地进行换页,这种情况有时被称为抖动(thrashing)。
一些早期的操作系统有一组相当复杂的机制,以便在抖动发生时检测并应对。例如,给定一组进程,系统可以决定
不运行
部分进程,希望减少的进程工作集(它们活跃使用的页面)能放入内存,从而能够取得进展。这种方法通常被称为准入控制
(admission control)。目前的一些系统采用更严格的方法处理内存过载。例如,当内存超额请求时,某些版本的 Linux 会运行“
内存不足的杀手程序
(out-of-memory killer)”。这个守护进程选择一个内存密集型进程
并杀死它,从而以不怎么委婉的方式减少内存。小结
现代系统增加了对时钟等简单 LRU 近似值的一些调整。例如,扫描抗性(scan resistance)是许多现代算法的重要组成部分,如 ARC [MM03]。扫描抗性算法通常是类似 LRU 的,但也试图避免 LRU 的最坏情况行为,我们曾在循环顺序工作负载中看到这种情况。因此,页替换算法的发展仍在继续。
然而,在许多情况下,由于
内存访问和磁盘访问时间之间的差异增加
,这些算法的重要性降低了。由于分页到硬盘非常昂贵,因此频繁分页的成本太高。过度分页的最佳解决方案往往很简单:购买更多的内存。
参考
上一篇
《Operating System:Three Easy Pieces》第二十三章 VAX/VMS虚拟内存系统
下一篇
《Operating System:Three Easy Pieces》第二十一章 超越物理内存:机制
- 作者:GJJ
- 链接:https://blog.gaojj.cn/article/blog-37
- 声明:本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。