目录
- 局部页面置换算法
- 最优页面置换算法
- FIFO先进先出
- LRU最近最久未使用算法
- Clock时钟页面置换算法
- 二次机会法Enhanced Clock
- Belady现象
- FIFO/LRU/Clcok的比较
正文
局部页面置换算法
功能:当缺页中断发生,需要调入新的页但是物理内存已满,此时需要把当前一部分页换出去,空出空间。选择内存中哪一个页被替换
目标:
1.尽可能减少换入和换出的次数。具体来说,把未来不再使用或者近期不会使用/较少使用的页面换出,通常只能在局部性原理指导下依据过去的统计数据预测。
2.页面锁定。 有一些物理页是需要常驻内存的,比如OS相关的代码和数据,这些随时都有可能访问,那么这些页我们需要常驻访问。我们用页面锁定技术,把相关的页放在内存里,使得这些页不在页面置换算法的考虑范围内。
例子:
(3,0)这个序列的意思是访问了第三个页面的第0个便宜。也就说(页序号,偏移量object)。我们这里可以忽略他的偏移量,只考虑页序号。因为只有当一个页不存在的时候,才会产生缺页中断,才需要考虑页面置换。在同一个页里面有多次访问,只有第一次访问才有可能产生缺页中断,所以我们可以忽略偏移量只考虑页号。
最优页面置换算法
这个算法可以说是一个不太实际的算法,但是我们可以把它作为一个评价标准。这个算法根据未来的情况进行推算,一般来说缺页次数产生最少。一般我们用其他页面置换算法运行后的结果与最优页面置换算法比较,比较缺页次数,只要能够尽量逼近最优选法,我们就可以认为这是一种比较好的算法。
根据最优页面置换算法,由于访问之前物理内存中已经存储了abcd,所以前四次访问不会改变物理内存中的页。但是当第五次访问发生时,根据换出距离下次访问时间最长的页,得应该换出d置入e,则此时物理内存中是abce,没有d。则当下次访问d时,应该换出最长等待时间的c。第十次访问后,物理内存的页编号是abde
FIFO先进先出
链表(List),典例是程序中的Queue。
产生缺页中断次数比较多。
Belady现象:当我们给一个程序分配更多的物理页时,它的缺页次数应该减少,但是FIFO可能会导致分配的物理页越多,缺页次数增加的现象。
先入先出原则,替换list中滞留时间最久的那一个。
内容 | 内容 | 内容 | 内容 | 时间点 |
a | b | c | d | 0 |
a | b | c | d | 1 |
a | b | c | d | 2 |
a | b | c | d | |
a | b | c | d | |
e | b | c | d | |
e | b | c | d | |
e | a | c | d | |
e | a | b | d | |
e | a | b | c | |
d | a | b | c |
LRU最近最久未使用算法
和最长滞留时间不同,最长滞留时间是在物理内存中驻留时间很久,但是他可能在近期被访问过;这个算法是换出在队列中最长时间没有被访问的页。根据程序局部性原理,在最近的一小段时间内,程序访问的代码和数据未来还有可能会继续访问,所以LRU就是合理的,因为反推局部性原理,一段时间内没用,那么接下来一段时间也可能不会用到。
缺页中断:3次
*:较久访问 标记越多,程序内优先级越高
粗体:置换/更新访问,当内存内数据被访问时,其替换优先级清零
访问请求 | c | a | d | b | e | b | a | b | c | d | 最终 |
默认a | a | a | a* | a** | a*** | a**** | a | a* | a** | a*** | a |
默认b | b | b | b | b | b* | b | b* | b | b* | b** | b |
默认c | c | c* | c** | c*** | e | e* | e** | e*** | e**** | d | d |
默认d | d | d | d | d* | d** | d*** | d**** | d***** | c | c | c |
LRU需要记录各个页面使用时间的先后顺序,开销比较大。目前有两种可能的实现方法。
1.使用链表[List]。每当一个页面被访问时,如果内存中有这个页,则将其放在首,最久访问自然会在不断换头的过程中排序放在尾部。没有的数据则淘汰链表尾部,然后插入首部。
例子:
头 | 内容 | 内容 | 尾 | 更新 |
a | b | c | d | default |
c | a | b | d | c |
e | c | a | b | e |
a | e | c | b | a |
2.使用堆栈。
页面置入栈顶后,查找是否有相同页号这个过程开销比较大。栈底一定是最久未被使用,所以需要替换时,直接替换栈底。
总结:LRU算法,结果虽然不错,但是他的开销比较大。他不是一个合适的实现方法,OS设计讲究高效且简洁,为了实现效果而花费很大代价,则得不偿失。需要一个平衡。
Clock时钟页面置换算法
LRU的近似,FIFO的改进。没有LRU的精确,但是能够近似的表明访问的先后顺序状态的算法,开销自然小很多。
页表项中的acess位,访问后0变成1,这个过程由硬件完成,不需要软件参与。但是我们的软件可以对其进行清零操作和置1操作。所以,当CPU访问到这个页的时候,会把acess bit置1。因此我们可以粗略估计,如果这个页的acess bit 是1,我们就认为他被访问了,是0则认为他没有被访问。只用了一个比特表示时间信息,虽然不精确但是有效。然后我们把程序访问的所有页面组成一个环形列表,并通过一个指针指向当前页,并且探测他是否是近似最老的页。老的判断就是:因为OS会定期把访问位清零,你一旦访问页后CPU又会置1,然后看这个访问位01状态,0是老页,就可以把这个页换出去。
抽象成时钟后,如图所示,指针指向某一物理内存时,检测其访问为是否为1,为1则将其置0且指针走向下一项。为0则其页帧就需替换出去。如果内存中有这一项且其访问位为0,则不需要指针转动检测,只需要将这一项置1即可。且这一操作不会导致指针转动到下一个位置【指针位置不变】。
解析:
阶段 | 变化 |
1~4 | 物理内存中有这一项,所以访问位0=>1,指针不转动,默认停在首位 |
5 | 由于物理内存中所有项访问位均为1,指针空转一圈。此时所有访问位置0,指针处于首位,指向a。此时更新页帧(a->e)且将访问位置1 |
6 | b已存在于内存,更新访问位为1。指针不变,仍指b |
7 | 指针指向b,将b置0后指向下一位。由于c访问位为0,则将c替换为a并置1,指针向下一位移动。 |
8 | 更新b,指针不变,指向d |
9 | 更新d为c并置1.此时内存中所有访问位均为1 |
10 | 进行空转一周,访问位全部清0,此时指针将从头开始旋转,指向首部,替换第一个页帧号。e=>d并置1 |
也就说当内存中访问位全为1时,该算法退化为FIFO
二次机会法Enhanced Clock
Clock算法是对访问进行标示,访问其实就是读写。这一过程并不能区分读和写,只能区分大致是否访问。因此对脏页的处理代价就比较大。写操作进行后,硬件会将脏位置1
脏位(dirty bit):如果页是写操作,置1;读操作,置0
在访问过程中,如果全是读操作,就意味着内存中与硬盘中的内容是一致的。所以如果替换这一页的时候,我们不需要写回硬盘,只需要释放内存空间(开销小);但是如果这一页在内存访问过程中,是对这一页进行写操作,也就说内存与硬盘中内容可能不一致,我们替换这一页时,需要把这一页的内容写会硬盘,来确保数据一致。
所以用上脏位,配合Clock算法可以来减少对硬盘的访问(写回操作的次数)
算法解析:
根据图示:
只有脏位(图中修改位)和访问位(图中使用位)均为0时,才会置换指针指向的页,然后将其访问位置1。当脏位和访问位仅有一个1时,将这一个1置0,指针指向下一位。均为1时,访问位置0,脏位不变,指针下移。
也就说如果某一个页他的两个校验位均为1,指针经过他两次才会将其全部清零,也就说第三次才会被置换。
通过这种算法,可以吧脏位(经常使用的页)有更多的机会留在内存中,而不会被频繁换入换出,而只读页会更优先的换出,使得被写过的页换出的概率减少,对硬盘的访问次数也就随之减少。这种算法修改了脏位,也就说每次替换都要写回主存。
理解hit】相比较访问位相同的页,脏位为1的页将赋予它多一次机会,优先将读操作的页换出去。
图示:aw中,w代表是这次访问是写操作。
阶段 | 变化 |
1~4 | 物理内存中有这一项,所以访问位0=>1,指针不转动,默认停在首位 |
5 | 指针空转一轮,然后从上到下分别变为01 01 00 00,指针回到首部,开始扫,数据变为00 00 c替换e 10 00 abed |
6 | b已存在于内存,更新访问位为1。00 10 10 00 指针不变 |
7 | a的写操作。11 10 10 00 abed |
8 | 与7相同 |
9 | 更新d为c并置1。 11 10 10 10 abec |
10 | 进行空转一周,访问位全部清0,此时指针将从头开始旋转,指向首部,替换第一个页帧号。d=>a并置1 01 10 00 00 adec |
Belady现象
当我们给正在运行的程序分配的物理页越多,它产生缺页的次数应该越少。但是当采取了某些页面置换算法后,物理内存分配的越多缺页现象反而增加的现象。典例是FIFO。因为FIFO替换的页面不是当前程序近期不会访问或者不常用的页面,它置换出去的页很可能接下来要使用。
如上图所示,FIFO的页面置换情况下出现的缺页现象。
阶段 | 变化 |
1~7 | 满足FIFO条件,先进先出,发生缺页后头部替换出去且剩余内容顺位移动,在尾部加入当前要访问的页帧号 |
8~9 |
第七次访问后,此时链表中的数据是1-2-5,此时再次访问1和2这个已经存在于链表中的页帧,这里不会产生缺页中断但是整个链表没有体现出时间相关的信息,此时1和2虽然是链表中放的比较久的页,但是这里并不能体现他们的驻留时间。它只体现了它存在的先后时间却没有体现动态访问中访问时间先后。 |
10~12 | 未命中,缺页中断,FIFO先入先出替换 |
上面是给链表设置了三个页的空间,那么如果我们设置多一个空间又会怎样呢?理论上应该是缺页次数更少才对。
如图,缺页次数反而比之前更多了。这就是Belady现象。
反观LRU算法
同样的访问序列,当在内存分三个页帧时,产生了10次缺页,当内存中页帧分配量增加1时,只产了8次缺页。也就说随着物理页帧空间的增加,缺页次数减少了,LRU没有Belady现象。
LRU算法符合栈算法的特点,栈算法的特点之一就是给的物理页帧越多,它的缺页次数越少。
FIFO/LRU/Clcok的比较
LRU和FIFO都是先进先出的思路,所以可以用链表或者栈来表示它的访问次序和驻留时间,但是LRU算法除了滞留时间还考虑了最近访问时间,所以当访问到内存中已存在的页帧时,他会将其置于head其他页顺序推移。这就是FIFO和LRU的最大区别。程序局部性特点越好,LRU算法就会产生越少的缺页次数。但是如果程序没有很好的局部性特点,则LRU会退化为FIFO算法,甚至完全等价。
Clock算法是LRU算法的一种近似,用1-2位bit来模拟访问时间,所以他有效逼近LRU算法且开销小。
LRU算法开销过大,FIFO性能不佳,所以折中我们会偏爱Clock算法。每一次访问时,不必动态的去调整该页面在链表中的顺序,而仅需要做一个标记,然后等待缺页中断发生时,再把它移动到链表尾部。对于内存中未被访问的页面,Clock与LRU一样好。而对于访问过的页面,则比LRU算法差。