Fork me on GitHub

操作系统(三)—— 操作系统内存管理(下)

概述

  上一篇文章把分段和分页介绍了一下,以及两者的寻址方式和优化方法都介绍了,这节主要介绍另一个重要问题,页面置换算法,当内存空间不足时,需要把内存中的页放入磁盘,那到底选择哪些页放到磁盘上比较好呢?这篇文章会回答这个问题。

如何解决内存不够用

  早期的解决办法是采用覆盖技术,但是覆盖技术需要程序员做大量的工作,非常麻烦且低效,后来就发明了交换技术,这里注意,这里的交换是把整个程序在内存和磁盘之间交换并不是页之间的交换,其缺点就是开销太大,所以后来就发明了虚存技术,这种技术解决了上面两种技术的痛点。下面就分别介绍一下这三种技术。

覆盖技术

把程序按照其自身逻辑结构,划分为若干个功能上相对独立的程序模块,那些不会同时执行的模块共享同一块内存区域,按时间先后来运行。

把程序分成如下几个部分:

  • 常驻部分:这部分代码和数据需要常驻内存,比如一个程序的main函数。
  • 可选部分:需要运行时从磁盘加载到内存,平时就放在磁盘。

如果可选部分,两个不同的模块之间没有相互调用关系,那么这两者就可以共享同一片内存区域,只是执行的先后不同而已,下面举个例子。

public class Test {

    public static void module1(){
        System.out.println("i am module1");
    }
    
    public static void module2(){
        System.out.println("i am module2");
    }


    public static void main(String[] args) {
        module1();
        module2();
    }
}

上面的代码是一个简单的java程序,其中main函数就是上面介绍的常驻部分,而module1和module2就是可选部分,并且两者没有互相调用关系,所以module1和module2可以共享同一片内存区域,当module1运行的时候,module2没有必要加载到内存中,当module1运行结束之后,module2再加载到内存中占用module1使用的内存区域。

缺点:

  需要程序员自己来界定模块,并且确定哪些模块之间没有互相调用关系,对于程序的设计一点也不友好。

交换技术

  当有多个程序在内存中运行的时候,如果有一个新的程序要进来执行,发现内存不足了,这个时候有一种解决办法就是把内存中正在运行的某个程序保存到磁盘上,然后把空间腾出来让新的程序使用。

缺点:

  把整个程序从内存搬到磁盘,开销是很大的,如果频繁做这个操作,会非常影响程序的执行效率。

虚存技术

  虚存技术基本原理如下

  • 在程序装载到内存的时候不把全部内容一次性全部装入内存,只是部分载入
  • 当程序执行过程中发现有些数据或指令不在内存中,就把需要的数据或指令加载到内存中
  • 把内存中没有使用的页放到磁盘上,以腾出内存空间

和交换技术相比,他的粒度更低,是以页为单位(上节把分页已经介绍了,不懂的朋友可以看上一篇文章^_^),而不像交换技术一次性把整个程序交换。与覆盖技术相比,他也是把部分指令和数据加载到内存中,但是不用考虑把程序分成互相不调用的模块,所以虚存技术综合了覆盖技术和交换技术的优点,并且解决了交换技术和覆盖技术的缺点。

虚存技术理论基础

  其实也说不上是理论基础,只是一个观察的结果,该结果就是程序在执行的过程中,当前访问的指令和接下来要访问的指令,当前访问的数据和接下来要访问的数据都集中在一个较小的区域内。这个就是“局部性原理”,不要看这只是一个简单的结果,其实使用非常广泛。

 

ok,上面把三种技术都介绍完了,下面的部分讲解虚存技术使用的页面置换算法

最优页面置换算法

  当发生缺页中断,并且内存不足时,需要把内存中的页和磁盘中的页进行交换。所谓最优页面置换算法就是内存中的页在下一次被访问之前等待的时间最长的页,那问题来了,怎么预知哪个页需要等待的时间最长呢?事实上是无法预知的,所以这个算法是无法实现的,但是无法实现并不是没有意义,当一个程序跑完之后,访问的页的序列确定下来之后,自然知道在每次缺页中断时应该交换哪个页,所以这个算法可以用来评估别的页面置换算法的好坏,如果有一种页面置换算法的结果和最优页面置换算法的结果很接近,那这种算法就ok,反之,就不行。

                         

            图片来源:清华大学操作系统公开课

图片内容说明:a,b,c,d表示要访问的页,最上面1,2,3,4,5,,,表示时间序列,内存中最多保存4页,而且a,b,c,d都已经加载到内存,在1,2,3,4时刻分别访问c,a,d,b,这个是没有问题的,因为内存中已经存在,当访问到e的时候就会发生缺页中断,这个时候由于内存最多保存4页,那就需要把其中的一页放到磁盘上,腾出来空间供e使用,根据最优页面置换算法,我们可以发现d在第10时刻才会访问,等待的时间最长,所以在第5时刻,把d页换成e页是最优的。

先进先出页面置换算法

  这种算法其实就是我们常说的队列的方式(FIFO),最先进去的页面,最先被置换出去,这种方式思路确实很简单,但是并不是简单的就是有效的,这种算法只考虑页面在内存中的存活时间,并没有考虑这个页面是不是经常被访问的页面,有时候会把经常访问的页面给置换到磁盘上,这就很尴尬,所以这个算法很少被单独使用。

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

  简称LRU算法,这个算法应用太广泛了,redis,mysql中都可以看到他的身影,以至于现在找工作,很多面试官都变态的要求现场手写一个LRU算法,于我个人看来,没必要,除非面试的岗位确实需要很多牛逼的算法实现,不然进去拧螺丝,搞那么复杂干啥。下面就介绍一下这个算法的思想,需要维护一个列表,记录每个页面被访问的时间,当发生缺页中断时,把最久没有被访问的页面给置换掉,这个其实是对最优页面置换算法的一个逼近,因为根据局部性原理,最近被频繁访问的,在未来的一小段时间内还会频繁被访问。反过来说,很久未被访问的,将来也会很长时间不会被访问。

实现方法

  可以采用链表实现,把刚刚访问的页面作为链表的首节点,当访问页面在链表中存在,就把那个节点移动至首节点,当发生缺页中断的时候,把链表的尾节点淘汰。

时钟页面置换算法

    

                    图片来源:清华大学操作系统公开课

在图片开头第一句话,时钟页面置换算法是LRU的近似,是对FIFO的改进,下面就解释一下为什么这么说,LRU算法是需要详细记录每个页面的访问时间的,但是这个算法并没有记录详细的访问时间,只是使用一个标志位,也就是之前介绍过的页表中的访问位,如果被访问过,无论是读还是写,就把这个标志位改为1,否则为0,这个算法淘汰的时候,优先选择访问标志位为0的页面,这样淘汰的页面可能并不是最久未被访问的,因为这个是采用一个指针类似于钟表一样来旋转,找到那个标志为0的就淘汰,所以说是LRU的一种近似。

  为什么说是FIFO(先进先出)的改进呢?如果所有的页面都没有被访问过,标志位都是0,如果这个时候发生了缺页中断,就把最先放入内存的给淘汰掉。如果所有的页面都被访问过,标志位都是1,如果在标志位变成1的过程中没有发生缺页中断,那淘汰的依然是最先进入的页面。但是这个算法考虑到最近是否被访问,而不像FIFO直接淘汰最早访问的页面,所以说是FIFO的改进。

算法过程如下:

  • 如果指针指向的页面访问位是0,直接淘汰
  • 如果是1,就把访问位改成0,指向下一个
  • 如果遍历一遍发现都是1,就把最开始的那个页淘汰

下面举个例子来说明这个指针旋转的过程。

    

                       图片来源:清华大学操作系统公开课

图中过程如下:

  • t = 1,t = 2,t = 3,t = 4,连续访问a,b,c,d,把每个页的访问位都修改为1
  • t = 5,指针旋转一周,发现都是1,把所有的1都改成0,然后把开始的那个页给淘汰,把e页加入进来
  • t = 6,命中,把b页访问位改成1
  • t = 7,缺页中断,此时指针指向b页,但是b页访问位为1,所以寻找下一个c,发现访问位为0,淘汰掉
  • ...
  • 后面的分析都是如此

二次机会法

  二次机会法,其实对时钟算法的优化,因为时钟置换算法在淘汰页面的时候只考虑了页面是否被访问过,如果所有的页面都被访问过,那让一个未被写过的页面淘汰比一个已经被写过的页面淘汰成本低很多,因为被写过的页面是脏页,还要把这个页面的内容先写到磁盘,如果一个页面没有被写过,可以直接丢弃。所以二次机会法,就是使用了两个标志位,第一个标志位是访问位,第二个标志位修改位,当发生缺页中断时,具体的淘汰流程如下:

  • 如果访问位和修改位都是0,就直接淘汰
  • 如果访问位是1,修改位是0,把访问位的1改成0,继续查看下一页
  • 如果访问位和修改位都是1,先把访问位修改为0,修改位不变,下一轮循环如果又访问到这一页,把脏页刷新会磁盘,同时把修改位设置为0(这个页会有两次存活的机会,所以叫二次机会法),继续查看下一页

通过上面的分析,大家可以发现,被修改过的页有两次机会存活下来,而被淘汰的一定是访问位和修改位都是0的。

最不常用算法

  这个算法的核心思想是使用一个计数器记录每个页面被访问的次数,然后把访问次数最少的淘汰。这个算法有一个很明显的缺点就是,如果某一个页在程序的初始阶段访问非常频繁,如果之后执行过程中再也没有访问,但是根据这个算法,这个页会一直留在内存中,占用空间。

LRU,FIFO,Clock的比较

在比较这几个算法之前,先介绍一个现象,在使用FIFO算法时,有时会出现分配的物理页数增多,缺页率反而上升的异常现象,这个现象叫做belady现象。

看下面的例子:

 

 在图中,使用的是FIFO页面置换算法,给程序分配的物理页的个数是3页,从图中可以看出总共命中了3次。上图第一行是访问序列,下面三行是队列中页。

再看下面的图

 

在图中,依然是FIFO算法,但是这次使用的物理页是4页,可以看出总共就命中了2次,所以缺页率反而提高了。

FIFO算法发生belady现象的原因 

页面置换的一个原则就是要把最不经常使用的页给淘汰掉,而FIFO只是简单的按照访问时间先后进行淘汰,完全没有考虑访问频率和最近有没有被使用,所以可能会把一些经常被使用的页给换出去。

LRU算法没有belady现象,但是LRU算法实现起来的开销比较大,所以CLOCK算法是比较理想的实现方法,开销相对较小,因为只需要修改特定的标志位,而且是硬件实现的修改,同时又可以把最近访问的页保留下来。

工作集

为了介绍和工作集相关的算法,需要先了解一些有关工作集和常住集的概念,同时要了解一下在程序的运行过程中工作集变化的特征。

 工作集定义如下:

  1. 从t时刻开始
  2. 经过一个小的时间间隔Δ
  3. 在这个小的时间间隔Δ中访问的页为W(t,Δ),就是工作集

注意工作集是一个集合,集合有三个特性,1、确定性 2、互异性 3、无序性,所以工作集中的页面不能重复,下面举个例子说明一下什么是工作集。

    

上图中,第一个工作集是从 t开始,经过时间间隔Δ,工作集为W(t1, Δ) = {1, 2, 5, 6, 7},第二个图类似就不说了。

 

程序运行过程中,工作集变化符合什么特征呢?看下图

    

在图的最上方的解释其实已经很清晰了,如果程序符合局部性原理,那就会进入一段非常平稳的曲线,因为这段时间工作集发生变化很小,一旦程序局部性区域改变,工作集就会有一个剧烈的变化。

 

常驻集定义:

  当前时刻,内存中实际驻留的物理页面的个数。简单来说就是给某个进程分配的物理页框的多少,举个例子,比如给进程A限制在内存中最多存在10页,如果达到10页,又有新的页要放入内存,就需要把内存中某个页给淘汰掉,那常住集的大小就是10。注意,常住集不是越大越好,当常住集达到一定的大小之后,再增大也不会使缺页率明显下降。

 

ok,上面把工作集和常住集的概念介绍完了,同时介绍了程序运行过程中工作集的特性,下面就介绍一个与工作集相关的算法。

工作集页面置换算法

学过计算机网络的都知道,在TCP协议接收和发送报文的时候会使用滑动窗口,这个也是类似,也有一个窗口大小,至于这个窗口是什么,看下面的例子

 

在上图中,窗口为时间窗口τ = 4

  • 在t = -2时刻,把e页加入内存
  • 在t = -1时刻,把d页加入内存
  • 在t = 0时刻,把a页加入内存
  • 在t = 1时刻,访问c页,c不在内存中,把c加入进来,由于窗口的开始时-2,当前时刻是1,窗口大小刚好为4,可以在t = 1时刻看到4个笑脸,不用淘汰。
  • 在t = 2时刻,访问c页,但是c在内存中,命中,但是由于在t = 2时刻,窗口大小变成5 > 4,所以要淘汰掉一个页面,把最开始放进入的e淘汰掉。
  • ....
  • 下面的都是按照这个思路分析

缺页率页面置换算法

首先看一下缺页率的定义

    缺页率 = 缺页次数/内存访问次数

有了上面的公式,看一下这个算法

 

 这个图的纵轴表示缺页率,横轴表示内存中工作集的大小,图中有两个横线,上面那条表示缺页率过高,需要增加工作集大小,下面那条线表示缺页率很低,可以减小工作集大小。

 

这个算法也有窗口的概念,但是和上面工作集页面置换算法不同,这个算法的窗口是两次发生缺页异常中间间隔的页面个数叫做窗口(其实这样叙述并不准确,还是看下面的例子吧)

 

 

图中,window size = 2,表明,两次缺页中断间隔大于2,就要把在两次间隔没有访问的页面淘汰掉。如果小于等于2就继续增大工作集大小。

  • 在t = 0时刻,内存中已经放入了e,d,a三个页面
  • 在t = 1时刻,放入c,ok窗口大小从这里开始记录(这里有朋友可能会觉得应该从第一个记录,其实不对,因为前4个页都发生了缺页中断,所以要从这里几录),也就是tlast
  • 在t = 2时刻,命中,此时tcurrent - tlast = 1 ,小于窗口大小,继续
  • 在t = 3时刻,访问d,命中,此时tcurrent - tlast = 2,等于窗口大小,继续
  • 在t = 4时刻,访问b,缺页中断,此时tcurrent - tlast > 2,把两次中断中没有访问到的页面都淘汰,两次中断中访问的页面为c,d,b,而e没有访问到,淘汰
  • ...
  • 在t = 6时刻,访问e,缺页中断,此时tcurrent - tlast  < 2,不做处理,说明缺页很频繁,将t = 6设置为 tlast
  • ...
  • 在t = 9时刻,访问a,缺页中断,此时tcurrent - tlast > 2,干掉在t = 6到t = 9中间没有访问的页面
  • ...

最后总结

  FIFO通过维护一个队列,淘汰最老的页面,但是该页面可能正在被使用,所以FIFO不是一个很好的算法。

  时钟页面置换算法和二次机会法算是对FIFO的一种改进,增加了访问位控制,用来检测页面是否被使用,优化了FIFO算法。

  LRU是一种非常优秀的算法,但是只能通过特定的硬件实现,如果硬件不支持,则无法实现,而最不常用算法是一种近似LRU算法,但是性能不是非常好。

  最后两个算法是工作集相关的算法,性能还可以,但是实现开销太大,在清华大学的操作系统公开课中,这两种算法都被归类为全局页面置换算法,但是在《现代操作系统》中把工作集页面置换算法归类为局部页面置换算法,原话如下:

 

 至于哪个是正确的,大家自己思考吧。。。

         

 

 

 可能有些胖友不太了解全局页面置换算法和局部置换算法有什么区别,这里就补充一下。

局部页面置换算法:针对单个进程的页面置换算法,页的换入和换出都是当前进程相关的。

全局页面置换算法:针对计算机中的全部进程,比如进程A发生缺页中断,可以把进程B的某个页淘汰,用于腾出空间。

posted @ 2020-08-14 12:08  猿起缘灭  阅读(364)  评论(0编辑  收藏  举报