这次的实验大体上可以分为6个模块,分别是:最佳置换算法模块,先进先出算法模块,最近最久未使用算法模块,改进clock算法模块,页面缓冲算法模块,访问序列生成模块。
功能:实现最佳置换算法,即在最久时间不使用或永不使用的页面被淘汰 接口:访问序列中的一个值 操作流程:
功能:实现先进先出算法,即最先进入内存的页面被淘汰 接口:访问序列中的一个值 操作流程:
功能: 实现最久未使用置换算法,即将已经进入内存的页面中,最久未使用的哪一个淘汰 接口:访问序列中的一个值 操作流程:
功能: 实现改进型clock算法 接口:页面访问数组中的某个值 操作流程:
功能:实现页面缓冲算法 接口:访问序列中的一个值 操作流程
功能:运用生成的随机序列,实现对以上置换算法的检测 接口:工作集起始位置p,工作集包含页数e,工作集移动率m 操作流程:
选择永不使用或是在最长时间内不再被访问(即距现在最长时间才会被访问)的页面淘汰出内存。
程序运行时,先将7,0,1三个页面装入内存。 之后,当进程要访问页面2的时候,将会产生缺页中断。此时根据最佳置换算法,因为页面7要在第18次才能访问,页面0在第5次访问,页面1在第14次访问,页面7最久不被使用,所以将页面7淘汰; 当进程0要访问时,因为它已存在在内存所以不必产生缺页中断; 当页面3要访问时,又引起缺页中断淘汰1; 依次类推直到最后一个页面访问完。下图为采用最佳置换算法的置换图。由图可得,采用最佳置换算法发生了6次缺页中断。
最佳置换算法的代码主要分为两部分,其中opt函数为函数主体,用于实现最佳置换,函数find_num用于寻找内存页面中距离下次使用,间隔最久的页面。
int find_num(int a)//距离下一次使用间隔 { int i; for (i = que_num; i < L; i++) { if (queue[i] == a) return i - que_num; } return MAX;//返回最大值 } void opt(int num) //最佳置换算法,num是访问序列中的一个值 { int i, max = 0, rep_num, temp = 0; if (count != N)//如果内存页面存在空位置,则加入至此 { list[count] = num; count++; flag++; } else { for (i = 0; i < N; i++) { if (list[i] == num) return; } for(i=0;i<N;i++) { { temp = find_num(list[i]); if (temp > max) { max = temp; rep_num = i; } } } list[rep_num] = num; flag++; } }淘汰最先进入内存的页面,即选择在页面待的时间最长的页面淘汰。
依旧是上一个算法的例子 程序运行时,先将7,0,1三个页面装入内存。 之后,当进程要访问页面2的时候,将会产生缺页中断。此时根据先进先出置换算法,因为页面7是最先进入内存的,所以将页面7换出; 当进程0要访问时,因为它已存在在内存所以不必产生缺页中断; 在进程要访问页面3的时候,因为页面0是最早进入内存的,所以将页面0换出; 依次类推直到最后一个页面访问完。下图为采用先进先出置换算法的置换图。由图可得,采用最佳置换算法发生了12次缺页中断。先进先出的页面置换比最佳置换算法的页面置换正好多了一倍;
先进先出算法的代码同样分为两部分,其中fifo函数为代码主体,用于实现先进先出置换算法的主要功能,而find_first函数,主要用于查找最先进入内存页面的那一个,返回它的位置,在fifo中将其替换。
int find_first()//找到最先进入内存页面的那一个 { int i,result=0,min=first_come[0]; for (i = 0; i < N; i++) { if (first_come[i] < min) { min = first_come[i]; result = i; } } return result; } void fifo(int num)//先进先出置换算法 { int i,rep_num,min=MAX; if (count != N)//如果内存页面存在空位置,则加入至此 { list[count] = num; first_come[count] = count; count++; flag++; } else { for (i = 0; i < N; i++) { if (list[i] == num) return; } rep_num = find_first(); list[rep_num] = num; first_come[rep_num] = que_num; flag++; } }以“最近的过去”作为“最近的将来”的近似,选择最近一段时间最长时间未被访问的页面淘汰出内存
依旧是上一个算法的例子
程序运行时,先将7,0,1三个页面装入内存。 之后,当进程要访问页面2的时候,将会产生缺页中断。此时根据最近最久未使用置换算法,因为页面7是最近最久未被使用的的,所以将页面7淘汰; 当进程0要访问时,因为它已存在在内存所以不必产生缺页中断; 在进程要访问页面3的时候,因为页面1是最近最久未被使用的,所以将页面1淘汰; 依次类推直到最后一个页面访问完。下图为采用最近最久未使用的置换算法的置换图。由图可得,采用最近最久未使用置换算法发生了9次缺页中断。
最近最久未使用算法的代码同样也是两部分,其中lru函数为算法主体,通过find_last函数查找内存页表中最久未使用的那一个,找到它的位置,将其替换。
int find_last()//找到最久未使用的那一个 { int i, result = 0, min = last_use[0]; for (i = 0; i < N; i++) { if (last_use[i] < min) { min = last_use[i]; result = i; } } return result; } void lru(int num)//最近最久未使用算法 { int i, rep_num = 0; if (count != N)//如果内存页面存在空位置,则加入至此 { list[count] = num; last_use[count] = count; count++; flag++; } else { for (i = 0; i < N; i++) { if (list[i] == num) { last_use[i] = que_num; return; } } rep_num = find_last(); list[rep_num] = num; last_use[rep_num] = que_num; flag++; } }在之前的CLOCK算法上面除了使用位之外,还增加了一个修改位,现在每一页有两个状态,分别是使用位,修改位,可分为以下四种情况考虑: (0,0):最近没有使用使用也没有修改,最佳状态! (0,1):修改过但最近没有使用,将会被写 (1,0):使用过但没有被修改,下一轮将再次被用 (1,1):使用过也修改过,下一轮页面置换最后的选择
基本思想 ① 从查寻指针当前位置起扫描内存分页循环队列,选择A=0且M=0的第一个页面淘汰;若未找到,转②
② 开始第二轮扫描,选择A=0且M=1的第一个页面淘汰,同时将经过的所有页面访问位置0;若不能找到,则转①
以下面替换的流程为例:
1.当页面0来时,Frame0空闲,所以换入页面0,修改状态为(1,0),同时发生缺页中断。
2.当访问页面1时,由于页面1,将要被修改,其状态设置为(1,1),同时发生缺页中断。
3.同理对于接下来的页面3,6,将其状态设置为(1,0),,同时发生缺页中断。
4.对于接下来的页面2,按照之前的页面置换算法的顺序,他现在主存中找状态为(0,0)的页面,发现没有…然后执行算法的第二步,找状态为(0,1)的页面,发现还是没有…这时候把主存里面所有页面的used bit清零,再重复执行算法的第一步,此时由于页面0的状态已经变成(0,0),页面2把页面0替换出主存。同时由于在之前的设计中页面2属于将要被修改(modify)的页面,故将其状态设置为(1,1)。由于经历了两轮查找,所以"Fault ?"对应的查找次数为2*4+1=9。同理可类推访问其他页面的情况。最后可知缺页次数为13次。
同样,改进型clock置换算法的代码也是两部分,其中clock_pro函数是算法的主体部分,它通过find_clock()函数返回的页号,实现页面的替换。而find_clock函数,就是实现了我们上面刚刚说的功能:进行第一轮查找,第二轮查找。 需要注意的是,在这里我们每次进行页面的替换都需要同时修改访问位和状态位两个参数,而同时也要根据第一轮第二轮查询的规则,单独的修改状态位。
int find_clock() { int i = clock_num; while (true)//如果没有找到第二类页面,则重新开始寻找第一类页面 { while(true)//寻找第一类页面 { if (state[i][0] == 0 && state[i][1] == 0) return i; i = (i + 1) % N; if (i == clock_num) break; } while(true)//如果没有找到第一类页面,则开始找第二类页面 { if (state[i][0] == 0 && state[i][1] == 1) return i; state[i][0] = 0; i = (i + 1) % N; if (i == clock_num) break; } } } void clock_pro(int num) { int i,rep_num; for (i = 0; i < N; i++) { if (list[i] == num) { state[i][0] = 1; state[i][1] = 0; clock_num = i; return; } } rep_num = find_clock(); clock_num = (rep_num+1)%N; list[rep_num] = num; state[rep_num][0] = 1; state[rep_num][1] = 1; flag++; }在基本FIFO算法基础上改进。 策略:全局置换,动态分配(大部分操作系统采用:高效,易于实现) 设立空闲页面链表和已修改页面链表,缺页选中需要置换的页面,如未修改:插入空闲页表尾部,该页已修改:已修改页面链表尾部。当已修改页面链表达到一定长度(如64个页面)时或者没有空闲页面时,一起将所有已修改页面写回磁盘,并将这些页面插入空闲页表队列。故可显著减少磁盘I/O操作次数
页面缓冲置换算法比较复杂,实现起来也比较麻烦。由于涉及到了链表,所以我们首先需要建立一个结构体。通过刚刚的实验原理,我们可以了解到PBA算法需要两个链表,一个空闲页面链表,一个已修改页面链表。
typedef struct QNode //链队列结点的定义 { int val;//页面号 int num;//进入内存页面的时间 struct QNode *next; }QNode, *Linklist; Linklist p, q;//p代表已修改页面链表,q代表空闲页面链表首先是create_linklist函数,用于创建一个队头为空的队列。
void create_linklist(Linklist &p, int num) { p = (Linklist)malloc(sizeof(QNode)); if (!p) exit (- 1); p->val = 0; p->next = p; }接下来是Insert_LNode函数,用于在链表中插入新的节点,ps,插入位置在链表最后。
void Insert_LNode(Linklist &m, int e,int f)//在循环链表中插入新的结点,从L头结点开始依次向后插入 { Linklist p, q; p = (Linklist)malloc(sizeof(QNode)); q = (Linklist)malloc(sizeof(QNode)); q->val = e; q->num = f; p = m; while (p->next != m) { p = p->next; } p->next = q; q->next = m; }Exchange_LNode函数,用于将链表L中序号为i的结点替换为val为e,num为f的结点
void Exchange_LNode(Linklist &m, int e, int f,int i)//将链表L中序号为i的结点替换为val为e,num为f的结点 { if (m->next == m) exit(-1); Linklist p, q; int j = 0; p = (Linklist)malloc(sizeof(QNode)); q = (Linklist)malloc(sizeof(QNode)); q->val = e; q->num = f; p = m; for (j = 0; j < i; j++)//使p为待更换结点的前一个结点,故应保证,删除第一个非头结点时i=0,以此类推 p = p->next; q->next = p->next->next; p->next = q; }pba函数是这个算法的主体,具体的设计思想可以在上面的概要设计中看到,在此不做过多解释。
void pba(int number)//p代表已修改页面链表,q代表空闲页面链表 { int i,min=MAX,rep_num=0,j=0; QNode *t = p->next; QNode *r = q->next; if (count != N)//如果内存页面存在空位置,则加入至此 { Insert_LNode(p, number,que_num); flag++; count++; } else { for (i = 0; i < N; i++)//如果numer已经存在修改队列p中 { if (t->val == number) return; t = t->next; } for (i = 0; i < 2; i++)//如果空闲队列中有num { if (r->val == number) { Insert_LNode(p, number, r->num);;//将其从q中加入p的队尾 QNode *s = q;//将其从q中移除 for (int ii = 0; ii < 2; ii++) { if (s->next->val == number) { s->next = s->next->next; break; } s = s->next; } //将p中最早进入的移除 t = p->next; for (i = 0; i < N; i++)//找到最先进入的那一个 { if (t->num < min) min = t->num; t = t->next; } t = p; for (i = 0; i < N; i++) { if (t->next->num == min) { t->next = t->next->next; break; } t = t->next; } count2--; return; } r = r->next; } //如果空闲队列中没有number t=p->next; for (i = 0; i < N; i++)//找到最先进入的那一个 { if (t->num < min) min = t->num; t = t->next; } t = p->next; for (i = 0; i < N; i++)//将最先进入的拿一个页面淘汰,加入到空闲页面链表 { if (t->num == min) { Insert_LNode(q, t->val,min);//将其加入空闲队列q的队尾 count2++; if(count2==2) q->next = q->next->next;//移除q中的第一个链表 Exchange_LNode(p, number, que_num,j);//在修改队列p中对应位置替换 flag++; break; } t = t->next; j++; } } }确定虚拟内存的尺寸N,工作集的起始位置p,工作集中包含的页数e,工作集移动率m(每处理m个页面访问则将起始位置p +1),以及一个范围在0和1之间的值t; 生成m个取值范围在p和p + e间的随机数,并记录到页面访问序列串中; 生成一个随机数r,0 ≤ r ≤ 1; 如果r < t,则为p生成一个新值,否则p = (p + 1) mod N; 如果想继续加大页面访问序列串的长度,请返回第2步,否则结束。
sequence_generate就是将上面的生成规则转换成代码,没有什么其他的东西,故不再做详细介绍。
void sequence_generate(int p,int e,double t,int m)//序列随机生成,其中p为工作集起始位置,e为工作集包含页数,m为工作集移动率 { int choice; double r; srand(time(0)); for (int i = 0; i < m; i++,que_num++)//生成L个取值范围在p和p+e之间的随机数作为访问序列 queue[que_num] = rand() % e + p; m_count++; printf("序列生成成功,访问序列为:\n"); for (int i = 0; i < m*m_count; i++) printf("%d ",queue[i]); printf("\n"); printf("是否继续增加访问序列长度\n1.是\n2.否\n"); scanf_s("%d",&choice); if (choice == 1) { if (L < m_count*m) { printf("已超出访问序列最大长度,自动返回\n"); return; } else { r = (rand() % 100)*0.01; if (r < t) { printf("请重新输入p的值\n"); scanf_s("%d", &p); } else p = (p + 1) % N; sequence_generate(p, e, t, m); } } if(choice==2) return; }通过设置不同的访问序列、不同的虚拟内存尺寸可以发现,在这五种页面置换算法中,opt最佳置换算法确实是最好的,在大多数情况下其缺页率都是最少的,但从时间开销上而言,opt的时间开销比较大,不出意外,先进先出算法是时间开销最小的,但也因此,其缺页率也比较高。lru算法同opt差不多,一个是往前看,一个是往后看,因此lru也是缺页率低时间开销大。而对于改进clock置换算法,由于他要对内存页面进行多次扫描,因此开销比较大。对于页面缓冲算法pab,由于其可以一起将所有已修改页面写回磁盘,因此显著减少了磁盘I./O操作次数,降低了开销。
最佳置换算法:理想化算法,具有最好性能(对于固定分配页面方式,本法可保证获得最低的缺页率),但实际上却难于实现
先进先出置换算法:简单直观,但不符合进程实际运行规律,性能较差,故实际应用极少
最近最久未使用置换算法:适用于各种类型的程序,性能较好,但需要较多的硬件支持
改进CLOCK置换算法:与简单Clock算法相比,可减少磁盘的I/O操作次数,但淘汰页的选择可能经历多次扫描,故实现算法自身的开销增大
页面缓冲算法:空闲页面链表同时用于物理块分配,当已修改页面链表达到一定长度如Z个页面时,一起将所有已修改页面写回磁盘,故可显著减少磁盘I/O操作次数
github地址: https://github.com/16281307/OS/tree/master/lab4 实验参考: 【1】三种页面置换算法 https://blog.csdn.net/qq_39290490/article/details/82251421 【2】CLOCK置换改进版算法 https://blog.csdn.net/zhuixun_/article/details/85336417