OS复习笔记ch8-2

引言

页面置换算法是考试的重点,算法实现不难,但是要能完全理解算法的思想才能很好地掌握。
常见的算法有FIFO、LRU、OPT、Clock四种,王道课上还讲解了一种改进的Clock算法,比较复杂,实际考察不多,这里主要讲解提到的四种,并给出对应的代码实现。

算法

FIFO

FIFO,即fist-in,first-out,常见于我们数据结构中的队列,表示先进先出的关系。
在页面置换里面的应用就是说,页框满的时候置换掉最先来的页面,或者说淘汰掉在内存中最久的页面,原理很好理解。
接下来,让我们举一个例子具体说一下
假设我们的页面编号是1-5,实际访问顺序是2 3 2 1 5 2 4 5 3 2 5 2,有三个页框
前四次访问情况如下图
image.png

前两次将2、3页面依次装入页框,第三次访问2号页面命中,第四次将1号页面装入页框

剩下的请小伙伴们自己动笔画一下,看看有几次缺页异常。


image.png
如果不考虑一开始内存中没有缓存的缺页异常,后续总共发生了6次。
比如,第五次页面5需要装入页框,但是页框已经满了,就需要发生页面置换。这里就是将第一次装入页框的2号页面置换出去了,装入了5号页面,同理后面的5次缺页异常。

实现代码如下,其中PAGE_SIZE=3pageArr=[2,3,2,1,5,2,4,5,3,2,5,2]

int FIFO(int pageArr[], int n) {

    int pageFault = 0;

    int frame[PAGE_SIZE];

    memset(frame, -1, sizeof(frame));

    int frameIndex = 0;

    printf("FIFO: \n");

    for (int i = 0; i < n; i++) {

        int j;

        for (j = 0; j < PAGE_SIZE; j++) {

            if (frame[j] == pageArr[i]) {

                break;

            }

        }

        if (j == PAGE_SIZE) { // page fault

            // page_string(frame);

            pageFault++;

            frame[frameIndex] = pageArr[i];

            frameIndex = (frameIndex + 1) % PAGE_SIZE;


        }

    }

    int pf_FIFO = pageFault;

    printf("FIFO page faults: %d\n", pf_FIFO);

    printf("FIFO hit ratio: %.3f\n", HitRatio(pf_FIFO, n));

  

    return pageFault;

}

注:这里使用取模运算间接实现了队列的功能,每次淘汰的页面编号都是framIndex+1取模PAGE_SIZE的结果,小伙伴们可以自行验证。

优缺点

  • 优点:实现简单,执行效率高。
  • 缺点:可能导致较高的页面置换率,特别是当旧页面仍然频繁使用时。

LRU

接下来,我们来讲比较著名的LRU算法,也就是Least Recently Used,最近最久未使用。
这个算法我们之前在组原的课程上讲Cache也提到过。LRU 算法基于“局部性原理”,即最近被访问的页面在不久的将来可能再次被访问,是一种比较高效的Cache更新算法。

同理,小伙伴们在纸上写一下LRU算法的页面置换示意图


image.png
我们直接看到第一次缺页异常这里,第五次需要访问页面5,没有命中需要发生页面置换。
LRU是说最近最久未使用,前序访问的序列是2 3 2 1,很明显页面3是最近最久未使用,页面2和1在其后,所以淘汰3号页面,装入5号页面。同理可得其他情况,最后总共是发生了4次页面置换。

实现代码如下

int LRU(int pageArr[], int n) {

    int pageFault = 0;

    int frame[PAGE_SIZE];

    memset(frame, -1, sizeof(frame));

    int frameIndex = 0;

    int stack[PAGE_SIZE]; // 记录每个页面对应编号最近一次访问的时间

    memset(stack, -1, sizeof(stack)); // -1表示未访问过

  

    printf("LRU: \n");

    for (int i = 0; i < n; i++) {

        int j;

        for (j = 0; j < PAGE_SIZE; j++) {

            if (frame[j] == pageArr[i]) {

                stack[j] = i;

                break;

            }

  

        }

        if (j == PAGE_SIZE) { // page fault

  

            pageFault++;

            // page_string(frame);

            int min = 0x7fffffff;

            int minIndex = -1;

            for (int k = 0; k < PAGE_SIZE; k++) {

                if (stack[k] < min) {

                    min = stack[k];

                    minIndex = k;

                }

            }

            frame[minIndex] = pageArr[i];

            stack[minIndex] = i;

            // printf("-> ");

            // page_string(frame);

            // printf("\n");

        }

    }

    int pf_LRU = pageFault;

    printf("LRU page faults: %d\n", pf_LRU);

    printf("LRU hit ratio: %.3f\n", HitRatio(pf_LRU, n));

    return pageFault;

}

实现LRU比较重要的一点就是要记录当前内存中页面的出现频率,如果是最近最久未使用,那么出现频率肯定是最低的,具体怎么实现,可以使用一个栈stack去记录。
每当内存中的页面被命中,就更新stack中对应编号的出现频率 stack[j] = i(频率用下标i表示,即当前页面和第一个页面的距离),i越小,说明距离第一个页面越近,反之距离当前越远,即越久没使用。最后遍历stack,找到最小值对应的下标就是要淘汰的页面编号。

优缺点

  • 优点:相比 FIFO,通常能更好地预测哪些页面将不再被需要,从而减少缺页率。
  • 缺点:实现复杂,需要更多的CPU资源来维护页面的访问记录。

OPT

Optimal policy,即最优页面置换算法。
核心思想是选择未来不再使用的或最远的未来才访问的页面被置换。
由于该算法的核心涉及未来的页面访问,所以应用前提必须是页面访问序列是确定的

我们直接来看访问图示
image.png
如图所示,第一个被置换的页面是1号页面,装入5号页面。与LRU的思路相反,在发生页面置换的时刻,看的是后续访问队列2 4 5 3 2 5 2,很明显1号页面是不会在访问到了,留着也没用,所以及时地替换掉。而第二个被置换的事2号页面,此时的后续访问队列是5 3 2 5 2,对于2号页面是最远的未来才访问到,所以替换2号页面,以此类推。
最后,我们发现opt的页面置换次数是3,迄今为止最好的算法,确实是最优的。

实现代码如下:

int OPT(int pageArr[], int n) {

    int pageFault = 0;

    int frame[PAGE_SIZE];

    memset(frame, -1, sizeof(frame));

  

    printf("OPT: \n");

    int frameIndex = 0;

    for (int i = 0; i < n; i++) {

        int j;

        for (j = 0; j < PAGE_SIZE; j++) {

            if (frame[j] == pageArr[i]) {

                break;

            }

        }

        if (j == PAGE_SIZE) {

  

            pageFault++;

            // page_string(frame);

            int k;

            int max = -1;

            int maxIndex = -1;

  
            for (k = 0; k < PAGE_SIZE; k++) {

                int l;

                for (l = i + 1; l < n; l++) {

                    if (frame[k] == pageArr[l]) {

                        break;

                    }

                }

                if (l == n) {   // page not used in the future

                    maxIndex = k;

                    break;

                }

                if (l > max) {  // find the page that will be used last

                    max = l;

                    maxIndex = k;

                }

            }
  

            frame[maxIndex] = pageArr[i];

            // printf("-> ");

            // page_string(frame);

            // printf("\n");

        }

    }

    int pf_OPT = pageFault;

    printf("OPT page faults: %d\n", pf_OPT);

    printf("OPT hit ratio: %.3f\n", HitRatio(pf_OPT, n));

    return pageFault;

}

实现上,最重要的是对后续队列的审查,这里对于内存中已有页面逐个去找它后续使用的频率,对最远的或者不使用的页面下标进行标记,最后淘汰。

优缺点

  • 优点:如果访问序列已知,它可以提供最低的缺页率。
  • 缺点:在真实的操作系统环境中几乎不可能实现,因为未来的页面访问序列通常是未知的。

CLOCK

CLOCK算法,也被称为时钟算法,是一种用于管理计算机系统中页面替换的常用方法。这种算法可以视为最近最少使用(LRU)算法的一个近似实现,它试图以较低的计算成本模拟LRU的行为。由于设计复杂,我们将花大量的篇幅去详细叙述其思想,代码不再给出。

该算法的几个要点如下

  • The set of frames be a circular buffer, with which a pointer is associated
  • Use an additional bit called a“use bit ”
  • When a page is first loaded in memory or referenced, the use bit is set to 1
  • When it is time to replace a page, the OS scans the set flipping all 1’s to 0
  • The first frame encountered with the use bit already set to 0 is replaced.

中文即

  • 使用循环缓冲,有一个指针可以指向缓冲的任意位置
  • 对每一个页面标记一个"使用位"。
  • 若该页首次加载到内存或被访问则user bit置1
  • 置换时操作系统扫描页面集合,为1的页面user bit置0
  • 第一个user bit=0的页面被置换

这里强调一下,u=1表示最近有使用,u=0则表示该页面最近没有被使用,应该被逐出。
我们先举一个简单例子,假设按照1-2-3-4的顺序访问页面,则缓冲池会以这样的一种顺序被填满:

图片.png

注意中间的指针,就像是时钟的指针一样在移动,这样的访问结束后,缓冲池里现在已经被填满了。此时如果要按照1-5的顺序访问,那么在访问1的时候是可以直接命中缓存返回的,但是访问5的时候,因为缓冲池已经满了,所以要进行一次逐出操作,其操作示意图如下:

图片.png

由于需要置换,最初要经过一轮遍历,每次遍历到一个节点发现u=1的,将该标记位置为0,然后遍历下一个页面。一轮遍历完后,发现没有可以被逐出的页面,则进行下一轮遍历。第二次遍历发现原先1号页面的标记位u=0,则将该页面逐出,置换为页面5,并将指针指向下一个页面。

image.png

假设我们接下来会访问2-1-2-5页面。

  • 第一次二号页面可以直接命中,将2号页面的u置为1,指针不移动。
  • 接下来访问1号页面没有命中,从二号页面开始遍历,先将2号页面的u置0,然后发现3号页面的使用位是0,淘汰更新为1号页面,使用位初始为1,指针转动指向页面4。
  • 接下来访问2号页面,再次命中,将2号页面的u置为1,指针不移动。
  • 最后访问5号页面命中,五号页面u置1,指针不移动。

同理,我们根据上述思想详细分析课上一开始的例子
访问页面为:2,3,2,1,5,2,4,5,3,2,5,2
image.png
首先,我们有一个指针指向循环缓冲区,下移到底会回溯置顶
然后,让我们来注意分析一下这一整个过程:

  1. 第一个页面2放入页框,使用位初始为1(有星号),指针下移
  2. 第二个页面3放入页框,使用位初始为1,指针下移
  3. 第三个页面2命中,使用位u置1,指针不变
  4. 第四个页面1放入页框,使用位初始为1,指针下移回溯置顶指向2号页面
  5. 第五个页面5未命中,需要替换页面,从指针开始处遍历数组将原先2、3、1号页面u置0,再次遍历将第一个u=0的2号页面淘汰,更换成5号页面,u初始化为1。
  6. 第六个页面2未命中,需要替换页面,从指针开始处遍历数组,发现第一个u=0的页面3,将其更换之,初始化u=1,指针下移。
  7. 第七个页面4未命中,同理替换1号,指针下移回溯置顶
  8. 第八个页面5命中,5号页面u置1,指针不移动
  9. 第九个页面3未命中,同第一次缺页异常,替换5号页面,指针指向2号页面
  10. 第十次页面2命中,2号页面u置1,指针不移动
  11. 第十一个页面5未命中,需要替换页面,指针从2号页面开始遍历,2号页面u置0,然后找到第一个u=0的4号页面,替换成5号页面,u初始为1,指针下移回溯置顶
  12. 第十二个页面3命中,3号页面u=1,指针不移动。

优缺点

  • 优点:效率高,实现简单,开销较小。
  • 缺点:不是完美的LRU,对工作集的适应性较差,性能不稳定。

总的来说,Clock算法是一个在实际操作系统中广泛使用的页面替换算法,它在实现简单和运行高效之间取得了较好的平衡。然而,它并不是在所有情况下都是最优的,其性能可以根据系统的具体需求和工作负载的特性进行评估。

比较

image.png
这些结果是基于运行一个使用256字页面大小的FORTRAN程序进行的测试,其中进行了约25万次页面引用。这张图表比较了四种页面替换算法——先进先出(FIFO)、时钟(CLOCK)、最近最少使用(LRU)以及最优(OPT)算法——在不同的内存帧数配置下的缺页率表现。图表显示了随着分配给程序的内存帧数的增加,每种算法的缺页率都逐渐降低。

  • FIFO算法显示了最高的缺页率,这表明它在页面管理效率上通常低于其他算法。
  • CLOCK算法的表现位于FIFO和LRU之间,提供了比FIFO更低的缺页率,但通常高于LRU。
  • LRU算法的缺页率低于FIFO和CLOCK,显示了较好的性能,因为它能较好地近似实际程序的访问模式。
  • OPT算法提供了最低的缺页率,因为它是基于未来信息做出决策,理论上是最优的。

这种类型的分析有助于理解不同页面替换策略在特定条件下的效能,并可以指导操作系统的设计和内存管理策略的选择。比如,当次数足够多的时候,CLOCK算法和LRU算法的性能相差无几,然而维护LRU算法的开销很大,此时使用CLOCK算法不失为一种好的选择。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值