操作系统-页面置换算法

学习背景

在前面的章节,我们学习了什么是虚拟内存,了解到了虚拟内存的结构。但是还有个问题我们没回答,就是内存调入换出时的策略/算法是怎么样的,本章节就来讲解。虚拟内存的知识在面试里问的也不多(字节除外),但是如果问到了一般都会以页面置换算法为考点提问,所以还是比较重要,当然,如果是纯JAVA开发问到这块的可能性比较小,大家酌情掌握。

页面置换算法

当发生缺页中断时,就需要将一个页面从内存换出,来腾出空间将磁盘的数据调入,如果换出的页面是脏页,那么还需要先写入磁盘。而如果不是脏页,则直接用调入的内存覆盖即可。我们的目标是选择在未来时间中最少使用到的页面,以便减少页面换出的次数从而提升性能。

最优页面置换算法

最优页面置换算法思路很简单,就是寻找到一个在接下来时间中最少被使用的页面,比如一个页面在800w条指令后被访问,另一个页面在600W条指令后被访问,那么很明显应该置换800w后被访问的页面。但是实际上操作系统不可能预知未来哪个页面会被问到,虽然可以通过仿真实验,记录下所有页面的访问情况,然后再发生换出时,以上一次记录的页面访问情况来作为根据判断最少使用的页面,但是所有仿真都是只针对一个程序的和输入数据的,而操作系统总是运行着各种各样的程序。所以这种算法实际上无法实现。

最近未使用页面置换算法(not recently used)

操作系统为一个页面有设置个状态为M(修改)位,R(读或者写位又叫访问位)位,注意这里只要写入了,也会修改R位,且R位会周期性(比如每次时钟中断)的被被设置为0,不设置M位是因为需要知道那个页面被修改了,将修改的信息换出到磁盘。那么一个页面有4种状态编号

  1. 没有访问,没有修改,R=M=0

  2. 没有访问,有修改,R=0,M=1

  3. 没有修改,有访问,R=1,M=0

  4. 有修改,有访问,R=M=1

看起来2似乎是不可能的,但是当R位被时钟周期置为0之后就会出现情况2。NRU算法随机选择一个最小的编号页面淘汰。比如在最近一个时钟滴答(大约20ms)内,淘汰一个没有非访问的页面,实际上淘汰一个被修改的但是没有被访问页面要比淘汰一个被访问但是没被修改的页面效果要更好。

先进先出页面置换算法(FIFO)

操作系统维护一个当前内存的链表,将新进入的页面放在队尾,最早进入的放在队头,当发生缺页中断时,淘汰表头,并发新调入的页面追加到队尾。这个算法的效果其实并不好,因为有可能把经常使用的页面置换出去,实际上很少有操作系统存粹使用这种算法

FIFO页面置换算法

第二次机会页面置换算法

二次机会法是对FIFO的改进,检查队头的页面的R位,如果R位是0,那么换出这个页面,如果是1,将R位清0,并放入队尾。然后向下搜索。很明显,如果操作系统在最近对所有页面都访问过,那么这个算法效果和FIFO一样。

二次机会法

时钟页面置换算法(clock)

其实是对二次机会法的改进,因为二次机会法需要经常移动页面,为了避免移动页面带来的效率问题,我们把所有页面组成一个环形链表,然后还是使用第二次机会法,只是当发现页面的R位是0时,将这个页面换出,然后新调入的页面插入到换出的位置,同时指针向下一个页面移动。因为环形链表看起来像是时钟,所以这个算法叫做时钟算法。(这个圆形的时钟真的难画,大家有好的画图方法可以教我一下就好了,怎么快速的画出来一个时钟的结构)

时钟页面置换算法

最近最少使用算法(LRU)

对最优页面置换算法的一个近视是,在前面几条指令中频繁被使用的页面,在接下来的指令中也可能被频繁的使用。相反的,在前面几条指令中不怎么使用的页面,在未来的指令中也不太可能被使用。这就是LRU的策略。当缺页中断时置换未使用时间最长的页面。但这个算法代价很高,因为要维护一个关于所有页的链表,使用频繁的在表头,使用较少的在表尾。有的使用特殊硬件实现,比如特殊计数器C,每当一页页面被访问后将页面中一个值+1,当发生缺页中断时检查所有页面的值,淘汰值最小的页面。

实际上很少有计算机有这种硬件,所以还需要软件实现。常用的方案是NFU(not frequently used),每次时钟中断时将页面的R位取出来加到页面的计数器上,这样这个计数器就大致跟踪了各个页面的访问频繁程度。但是这么做还是不完美,因为假设在两次扫描时间间隔中间,有一个页面被频繁访问,但是在第二次扫描时,还是只给这个页面+1,假设那么这个页面是刚申请的页面,那么很有可能这个页面就会被淘汰掉。实际上第一次扫描出的计数器较大的页面,在第二次扫描时,值也总是还是很大。

解决方式是老化(aging)算法,比如说,计数器是8位,每次还是读取页面的R位,然后在加上R为之前,将计数器右移一位,然后加到计数器上,但是加到计数器的最左边(也就是最高位),比如下面的图,经过4个时钟滴答,4个页面每个页面的计数器变化如下。很显然的在发生中断时我们应该淘汰掉值最小的页面,因为这个页面在最近的时间里并没有被访问到。但老化算法和LRU是不一样的,它有两个不同点

  1. 老化算法不能区分在同一个时钟滴答里,两个页面的先后访问顺序

  2. 加入两个页面在老化算法里的时钟滴答都是0,但是一个是在9个时钟滴答前被访问过,另一个是在100个时钟滴答前被访问过,也区分不出来,只能随机选择一个页面置换。

最近最少使用算法

在实践中,计数器往往设置为8位,而时钟滴答设置为20ms,这个设置一般是够用的。一般一个页面经过160MS还没被访问过,说明这个页面也没那么重要

工作集页面置换算法

之前说的页面置换算法都是局部页面置换算法,局部页面置换算法没有考虑进程内存访问的差异性,也就是进程在不同阶段的内存需求是不一样的,而且物理页面就这么多,不管怎么置换,对于全局的缺页次数并不能减少。而全局页面置换算法则是从所有进程的角度去考虑,选择出对于进程而言可以换出的物理页面。

一个进程在一段时间内正在使用的逻辑页面集合被称为工作集(working set),而实际驻留在物理内存的页面被称为常驻集,当常驻集包含较多的工作集时,缺页较少,工作集剧烈收缩或者扩张时,缺页较多。如果程序在运行阶段发生了大量的缺页中断导致程序运行很慢,就说明程序发生了颠簸(denning)。最理想的目标是在程序运行之前就已经把程序所需的工作集全部调入内存中,这叫做预先调页(preparing)。注意工作集是随着时间变化的。很多系统都会射法跟踪进程的工作集,以确保程序在运行之前,工作集已经在内存中了。该方法被称为工作模型。

一个进程从它开始执行到当前时刻,这期间使用cpu的时间称为实际运行时间。假设一个进程在T时间开始运行,在T+100ms的时刻使用了40ms的cpu时间,那么它的工作时间就是40ms(每个进程的工作时间不一样,一般是设置为∏),工作集定义就是40ms内这个进程使用过的页面。基于工作集的页面置换算法就是要找出一个不在工作集的页面并淘汰他(注意并不是发生缺页中断时才会扫描该进程的所有工作集,随着程序的执行工作集会被更新)。每个表项需要两个信息,页面最近使用时间和R,扫描所有页面

  1. r = 1

    设置上次使用时间为当前时间,表示缺页中断时该页面正在被使用

  2. r = 0,且 生存时间 > ∏ 换出这个页面,用新的页面置换他(如果需要)

  3. r = 0,且生存时间 < ∏ 保留页面,不设置生存时间,但是但记住最长生存时间,当扫描完所有页面还没找到一个被淘汰的页面,就淘汰掉生存时间最长的页面

最坏的情况下,所有页面r都为1,那么就随机选择一个干净的页面淘汰。

工作集页面置换算法

可以看到,没次置换出去的页面就是距离上次访问超过4的页面,红点页面表示换入的页面。

工作集时钟页面置换算法(wsclock)

因为工作集算法需要扫描所有页面,所以这种做法效率并不高。时钟工作集的工作方式和时钟算法很像,将页框组成一个循环表,每个表项包含来自基本工作集算法的上次使用时间,以及R位和M位,当发生缺页中断时,如果R=1,说明该页面被使用过,将R位置为0,指针向下移动。当R=0时,查看该页面的生存时间π,如果π大于生存时间,且页面是干净的,就换出该页面,如果不是干净的就先写入磁盘,然后换出。此时,算法不会停止,而是继续往下走,因为在前方可能存在一个干净的未使用的页面。这么做是为了避免在时钟周期中被IO阻塞,实际上为了避免被过多的写磁盘引起的进程切换,能够换出的页面个数是有限制的,不超过n个,一旦达到就不允许新的写调度操作。

如果指针经过一圈返回了起点,那么有可能已经发生过一次或多次换出,也可能一次都没发生。如果发生了那么肯定会有某个页面写操作被完成,该页面是干净的,可以被换出。如果没有发生过换出,说明所有页面都在工作集中,那么此时只能选择一个干净的页面来换出,如果不存在干净页面就找一个页面写磁盘,然后换出。

缺页率页面置换算法(Page Fault Frequency)

缺页率=缺页次数/内存访问次数 这是标准计算公式,实际上这个公式并不好用,更多时候使用平均缺页时间间隔的倒数。影响缺页率的因素有,页面置换算法,分配给进程的物理页数,页面大小,程序局部性特征

缺页率页面置换算法通过跟踪进程的缺页率,来将每个进程的缺页率控制在一个合理的范围,如果缺页率高,就增加工作集以分配更多的物理页面,如果缺页率过低,就减少工作集,将一些页面置换到外存。

缺页率页面置换算法

举这个例子,假设时间间隔为2,即窗口大小

  • 在第4时刻,发生缺页中断,距离上次缺页中断1间隔为3,大于规定的缺页间隔,说明程序运行的比较好,可以把一些页面换出,那么在时间间隔为2的时间内,程序访问了c,d,b的页面(即2,3,4时刻访问的页面)。所以从当前工作集这个呢换出a,e页面

  • 在时刻6,又发生了中断,此时因为间隔<=2,不需要换出,直接将缺页调入内存

  • 在时刻9又发生了缺页,间隔为3,那么将缺页调入。7-9的时间内访问了c,e,a页面所以换出b,d页面

做个总结

上面介绍了很多的算法,实际上这些算法并不是独立存在的,在操作系统里是多个算法互相配合完成页面置换。我们对他们做个总结。

算法总结

页面设计

以下讲解在页面设计时需要注意的一些点,因为不同的页面特点也会影响页面置换算法的工作效率。

  • 页面大小 一般来说更大的页面能使用更小的页表,且更能利用TLB表项,因为TLB是很稀缺的资源,用较少的TLB表项表示更大的页面内存就能够提供更多的页表缓存。比如一个进程使用64K内存,一个页4K就需要16个页表项,一个页4M则只需要1个页表项。内存与磁盘之间的传输一般是一次传输一页,而且传输时间大部分都花在了寻到和旋转延迟上,所以传输一个小页面和大页面的耗时是基本相同的。更少的页面写入也代表更少的io延迟。

  • 分离指令空间和数据空间 将指令存储在I空间,数据存储在D空间,每个空间存在独立的页表,这样能使用的地址空间可以加倍。因为其他进程可以复用I空间和D空间,行程他们自己的页表。

  • 共享页面 一般适合共享的都是只读的程序代码,而数据一般不适合共享,而且在I空间和D空间分开后,共享页面变得比较简单。共享数据则必共享数据麻烦,一种解决办法是如果两个进程只是读数据,则不复制,如果写数据,责复制写的页面,为每个进程生成一个单独的页副本,这种机制叫做写时复制。

  • 共享库 windows中称为DDL或者动态链接库。一些大型公用的库基本都是通过链接来做的,即将执行函数(如print)保存在磁盘,真正执行时链接器会将可执行文件加载进内存。而动态链接则是不加载需要的可执行文件,而是加载一段能够在运行时绑定被调用函数的存根例程(stub routine),而且共享库不是一次加载进内存,而是根据需要以页面单位装载。

  • 内存映射(memory mapped file) 共享库是内存映射的一种页数机制。内存映射的思想是,进程发起的系统调用会被映射到虚拟地址空间,而被映射的页面不会实际读入内容而是使用到的时候才以页为单位从磁盘读入。很显然的,进程可以使用这种方式来完成通信。

  • 清除策略 分页系统一般存在一个分页守护进程(paging demon),它被定期唤醒,如果空闲页框过少,则使用页面置换算法将脏页写会磁盘,并且保证系统中有足够多干净的页面。一种是先策略叫做双指针时钟,即前一个指针指向前移动,遇到脏页就写磁盘,后面的指针则做页面置换,因为前一个指针的存在后一个指针找到干净页面的概率会增加。

缺页中断处理

这里介绍一下发生缺页时操作系统的工作过程是怎么样的,当然具体的细节很复杂,大家掌握一个大概的工作流程即可。

  1. 硬件陷入内核,将当先指令的各种状态信息保存在特殊的CPU寄存器中

  2. 启动一个汇编例程保存通用寄存器和其他的易失信息,以免被操作系统破坏。

  3. 当操作系统发现一个缺页中断时,会尝试发现需要哪个虚拟页面,通常一个硬件寄存器的包含了这一个信息,如果没有的话,操作系统会检索程序寄存器,取出这条指令,并用软件分析这个指令,看它在缺页中断时在做什么

  4. 一旦知道了缺页中断的虚拟地址,检查这个地址是否有效,检查存取与保护是否一致,不一致就发出信号或者杀掉进程。否则检查是否有空闲页框,没有就执行页面置换算法。

  5. 如果选择的页框脏了,那么回写磁盘,这个过程会发生线程上下文切换,挂起产生缺页中断的进程,让其他进程运行直到磁盘传输结束,该页框会被标记为忙,防止其他进程占用

  6. 回写完毕后将从磁盘查找页面的内容,调入内存,这时进程仍然挂起

  7. 当磁盘中断发生时,说明该页面已经被装入,表示可以更新它的位置,页框也被标记为正常状态

  8. 恢复缺页中断指令以前的状态,程序计数器指向这条指令

  9. 调度引发缺页中断的进程,操作系统会返回它的汇编语言例程

  10. 恢复该例程的寄存器和其他状态信息,返回到用户空间继续执行,就像没发生过缺页中断一样。

分段和分页结合

为什么要分段或分页,假设虚拟地址时一维的,也就是地址从0到最大值增长,一个地址接着另一个地址,有两个独立的地址会比只有一个要好得多。比如编译器在编译是需要建立很多表,有的表保存程序源文件,有的保存符号变量名字和属性,有的保存整形或者浮点的常量,以及程序内部调用堆栈。其中程序内部调用堆栈会不断增长或者缩小,那么这5个(有个没写)表的排列假设如下。但是很明显的调用堆栈一般比其他表更大,而常量表则比较小,有可能常量表还有空间,而调用堆栈已经用完了。甚至两个表地址互相增长发生碰撞。

分段和分页结合

一种比较好的解决方式就是分段,即将地址空间分为多个独立的地址空间,每个地址空间从0到最大地址序列,且段的长度不同,可以在运行时动态改变。因为段地址之间互相独立,所以怎么增长缩短都不会互相影响。但是段地址是可能被填满的,一般程序需要提供两个部分,一个是段号,一个是段内地址。注意段是逻辑实体,而且可以包含不同的内容,针对这些内容我们可以做不同的设置,比如只包含程序过程的段是只读可执行的,而一个浮点值的段则是可读写,不可执行的。

分页和分段的区别则是,段是大小可变的,而分页则是大小不可变的,因为一个很大的段是很难管理的,所以又需要在段内进行分页。分页机制因为是代销固定的内存块,更加适合管理管理内存,而分段机制则更加适合复杂系统,段表存储在线性地址空间,而页表则保存在物理地址空间。段的定义需要三个参数,段基地址,段长,段属性。

END

到这里,虚拟内存的知识我们基本学习完毕了,大家可以将两个文档串起来仔细消化一下这里的内容,虽然不是面试的重点,但是里面的很多设计思想都很好。以后我们讲到一些数据结构的时候大家就会有感觉,比如redis内存淘汰算法,布隆过滤器等

  • 33
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
页面置换算法操作系统中的一个重要概念,用于解决虚拟内存中的页面置换问题。常见的页面置换算法包括最佳置换算法、先进先出页面置换算法、最近最久未使用置换算法、改进型Clock置换算法和页面缓冲算法等。 最佳置换算法是一种理论上的算法,它总是选择最长时间内不再被访问的页面进行置换,以保证最小化缺页率。但是,由于需要预测未来的页面访问情况,因此在实际应用中很难实现。 先进先出页面置换算法是一种简单的算法,它总是选择最先进入内存的页面进行置换。这种算法容易实现,但是可能会导致“老旧页面”长时间占用内存,从而增加缺页率。 最近最久未使用置换算法是一种基于时间局部性原理的算法,它总是选择最长时间未被访问的页面进行置换。这种算法相对于先进先出算法能够更好地利用时间局部性,但是需要维护一个访问时间戳,因此实现起来比较复杂。 改进型Clock置换算法是一种基于时钟算法的改进算法,它通过维护一个环形链表和一个访问位来实现页面置换。这种算法相对于最近最久未使用算法能够更好地平衡页面的访问频率和时间,但是需要更多的硬件支持。 页面缓冲算法是一种基于缓存的算法,它通过将热点数据缓存到内存中来减少缺页率。这种算法相对于其他算法能够更好地利用空间局部性,但是需要更多的内存空间。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值