实验7:虚拟存储器以及虚拟变换
一、实验目的
1:加深对虚拟存储器基本概念、基本组织结构以及基本工作原理的理解。
2:掌握页式、段式,段页式存储的原理以及地址变换的方法。
3:理解LRU与随机替换的基本思想。
二、实验平台
在Dev-C++软件上,运行或修改VA-Converting.cpp文件。
三、实验内容与步骤
3.1 学习虚拟地址变换的基本操作,了解基本工作原理
1:启动虚拟地址变化模拟器。
2:运行程序,设计测试地址,思考模拟器地址变化的原理。
在上图中,测试的逻辑地址是300。物理地址的计算公式为:逻辑地址的块号 * 页面大小 + 页内地址。其中,逻辑地址块号的计算公式为:逻辑地址块号 = 逻辑地址 / 页面大小。页内地址的计算公式为:页内地址 = 逻辑地址 % 页面大小。
综上所述,模拟器地址变化主要依靠逻辑地址和物理地址之间的映射。
3:思考物理地址和虚拟地址空间大小。
由下图打印的信息可知,pa对应虚拟地址的页号,d对应虚拟地址的页内地址,n对应所映射的物理地址的页号,m对应所映射的物理地址。
物理地址的空间大小 = 物理页号最大值 * 页面大小 + 最大页面地址。其中,物理页号的最大值为25,页面大小为1KB(1024B),最大页面地址为1023B(页面大小减去1)。
虚拟地址的空间大小 = 虚拟页号最大值 * 页面大小 + 最大页面地址。其中,虚拟页号的最大值为1024(即字母l对应的大小),页面大小为1KB(1024B),最大页面地址为1023B(页面大小减去1)。
3.2 打开源代码
1:分析示例程序的源码中,页表用什么数据结构实现的?替换算法用的什么?
页面是用结构体搭建的哈希表实现的。Page中包含一个虚拟地址的页号和一个所映射的物理地
址页号,且总共设置了15个虚拟页。
替换算法采用了LRU算法,是用栈实现的。
2:分析虚拟变换过程是如何实现的,替换算法是如何实现的?
虚拟变化过程的实现:利用页表进行实现。首先对用户输入的逻辑地址进行分解和转换,分别得到虚拟页号pa、页内地址d。再利用locate函数,通过传入的虚拟页号得到物理页号。
其中,locate函数的实现如下所示。首先遍历所有虚拟页,对比当前位置虚拟页号和函数传入的虚拟页号是否相等。如果相等,则返回结构体中所保存的物理页号,否则继续下一次遍历。如果最终没有对应的物理页号,则返回-1,表示函数传入的虚拟页号和当前系统中的物理页号没有映射关系。
替换算法的实现:利用栈进行实现。如果栈内是非满状态的话(即存在空闲的页表),首先判断该页是否在栈内,如果不在栈内则进行入栈操作,如果在栈内则调用其位置于栈顶(即最近最常访问内容的位置往前靠)。如果栈内是满状态的话(即不存在空闲的页表),首先判断该页是否在栈内,如果不在栈内则进行入栈操作,并将栈底的内容弹出(即替换掉最近最不常访问的内容),如果在栈内则调用其位置于栈顶(即最近最常访问内容的位置往前靠)。
3:分析cache的大小,cache的地址变换用什么数据结构实现?
本程序中没有设置cache。
3.3 自行设计地址变换模拟器
1:用C、C++或者熟悉任意语言编程实现虚拟地址变换过程。
2:虚拟地址32位,物理地址16位,每页大小2K、Cache大小2K、直接映像、块大小32B。
3:输入采用16进制(或10进制)的地址,通过随机方法进行替换。(可自行设计一个初始的页表数据)
4:采用一级页表方式。
5:运行需要输出虚拟地址变换后的物理地址,以及Cache块的索引和块内地址、是否命中。
【分析】
1:页式结构分析
页面大小2K = 2^11
虚拟地址32位,空间大小为2^32,页面数量为2^32 / 2^11 = 2^21个
物理地址16位,空间大小为2^16,分块数量为2^16 / 2^11 = 2^5 = 32个
2:Cache结构分析
Cache大小2K = 2^11,块大小32B = 2^5,Cache块数为2^11 / 2^5 = 2^6 = 64块
Cache是直接映像,每组对应1块,所以Cache的行数和组数都是64
3:地址映射分析
页面号 = 虚拟地址 / 页面大小
页内地址 = 虚拟地址 % 页面大小
主存块号:计算出的页面号在页表内进行一一映射
物理地址 = 主存块号 * 页面大小 + 页内地址
4:Cache命中分析
Cache索引 = 物理地址 / Cache块大小 % Cache块数
Cache块内地址 = 物理地址 % Cache块大小
其中,Cache块大小 = 32B
【测试用例】
1:执行随机页面置换算法
使用【功能1】,输入页面总数为10,页面号从0~9随机输入(此处的序列为0、1、2、3、6、5、4、7、9、8)。当存储页面的栈处于满状态时,栈内执行随机替换算法,即新插入的页面替换掉栈中的某页面。最终的结果如上图所示。
2:地址变换
使用【功能2】,输入逻辑地址为300。程序通过计算逻辑页面和页内地址,将逻辑页面在页表中找到其对应的物理块号后,可以计算出逻辑地址所映射的物理地址为2048 * 7 + 300 = 14636。Cache再对物理地址进行检索,本测试中Cache命中,对应的Cache行是14636 / 32 % 64 = 9,块内地址是14636 % 32 = 12。最终的结果如上图所示。
当用户输入的逻辑地址过大时,程序会提示【地址越界】,结果如上图所示。
3:退出程序系统
使用【功能3】,打印“结束使用!”字样,程序运行完毕。结果如上图所示。
【源代码】
import random # 栈类,用于管理页面号的栈 class SqStack: def __init__(self): self.stack = [] # 栈的存储结构 self.max_size = 5 # 栈的最大容量!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
def push(self, item): # 将一个元素推入栈中 if len(self.stack) < self.max_size: self.stack.append(item) def is_full(self): # 判断栈是否已满 return len(self.stack) == self.max_size def is_empty(self): # 判断栈是否为空 return len(self.stack) == 0 def find(self, item): # 查找栈中是否含有某个元素 return item in self.stack def print_stack(self): # 打印栈的内容 print(self.stack) # 页面类,用于管理页面和内存 class Page: def __init__(self): self.page_table = {} # 页面表 self.page_len = 65536 # 页面总数 self.l = 2048 # 页面大小 # 初始化页面表 for i in range(self.page_len): j = random.randint(0, 10) self.page_table[i] = i + j def locate(self, n): # 根据页面号查找对应的块号 return self.page_table.get(n, -1) # 地址转换函数 def address_translation(page, address): # 计算页面号和偏移量 page_number = address // page.l if not (0 <= page_number < page.page_len): print("地址越界") return -1 offset = address % page.l # 获取块号并计算物理地址 block_number = page.locate(page_number) if block_number != -1: physical_address = block_number * page.l + offset print(f"页号: {page_number}, 块号: {block_number}, 偏移量: {offset}") print(f"物理地址 = {physical_address}") return physical_address else: print("此地址没有对应的条目") return -1 # 随机页面置换函数 def random_page_replacement(stack, page_number): # 若栈未满且页面号不存在于栈中,则入栈 if not stack.is_full(): if not stack.find(page_number): stack.push(page_number) return True # 若栈已满且页面号不存在于栈中,则随机替换一个元素 else: if not stack.find(page_number): replace_index = random.randint(0, stack.max_size - 1) stack.stack[replace_index] = page_number return True return False def main(): page = Page() stack = SqStack() cache = [None] * 64 # 初始化缓存 cache_v = 32 while True: print("**********菜单***********") print("1 - 执行随机页面置换算法") print("2 - 地址变换") print("3 - 退出") print("*************************") choice = input("请输入菜单号:") if choice == "1": while True: try: n = int(input("请输入页面数:")) if n > 100: print("输入的数值太大!!") continue break except ValueError: print("请输入一个有效的整数!") page_sequence = [] for _ in range(n): while True: try: page_number = int(input("请输入页面号 (0~~9):")) if 0 <= page_number <= 9: page_sequence.append(page_number) break else: print("请输入一个介于 0 和 9 之间的整数!") except ValueError: print("请输入一个有效的整数!") for page_number in page_sequence: random_page_replacement(stack, page_number) stack.print_stack() elif choice == "2": # 执行地址变换 print("页面大小是 2kb") print("物理地址 = 其所对块号 * 页面大小 + 页内地址") adr = int(input("请输入逻辑地址:")) physical_address = address_translation(page, adr) if physical_address != -1: cache_index = physical_address // cache_v % len(cache) offset = physical_address % cache_v cache[cache_index] = physical_address // cache_v print(f"物理地址 {physical_address} 对应的 cache 索引为:{cache_index}") print(f"物理地址 {physical_address} 对应的 cache 块内地址为:{offset}") elif choice == "3": # 退出程序 print("结束使用!") break if __name__ == "__main__": main() |
3.4 思考问题并简要回答
1:如果替换算法改为FIFO,需要对哪些数据结构以及算法进行改进?
- 数据结构:使用队列或列表,用于追踪页面的加载顺序。
- 算法改进:修改页面置换函数,使其按照 FIFO 的逻辑运行。即当缓存满的时候,移除最早加载的页面。
2:如果采用TLB快表,则需如何改进方案?
TLB快表用于加速虚拟地址到物理地址的转换过程。
- 数据结构:使用数组或列表,用于存储最近的地址转换。
- 算法改进:在进行地址转换之前,先在 TLB 快表中查找。如果在快表中找到该地址,则直接使用;如果在快表中没有找到该地址,进行常规地址转换,并将结果存入快表中。
3:如果虚拟地址大小、物理地址大小、虚页大小、cache大小、cache块大小、相联度等参数可以通过用户输入的情况,需要对哪些部分进行改进?
- 用户输入改进:设置允许用户输入各种参数的模块,并设置相应的变量进行保存,以便于函数调用时进行传参操作。
- 数据结构调整:
(1)页面和页大小:根据用户输入的虚拟页大小和物理地址大小调整页面的数据结构。
(2)Cache 结构:基于用户输入的 cache 大小和块大小调整 cache 的数据结构。如果实现了相联 cache(直接映射、全相联、组相联),则需要根据相联度来组织 cache 的存储方式。 - 算法调整:
(1)地址转换:修改地址转换逻辑以适应新的虚拟地址和物理地址大小。计算页号和偏移量的方式需要根据新的页面大小进行调整。
(2)Cache 管理:根据新的 cache 参数调整 cache 的管理策略,例如数据的存储、检索、替换等。
(3)页面置换算法:页面置换算法需要根据新的页面大小进行调整,例如使用FIFO、LRU、随机替换算法等。 - 参考代码如下所示:
def get_user_input(): virtual_address_size = int(input("请输入虚拟地址大小:")) physical_address_size = int(input("请输入物理地址大小:")) page_size = int(input("请输入虚页大小:")) cache_size = int(input("请输入cache大小:")) cache_block_size = int(input("请输入cache块大小:")) associativity = int(input("请输入相联度:")) # ... 其他参数 # 进行参数验证和处理 return (virtual_address_size, physical_address_size, page_size, cache_size, cache_block_size, associativity) # 在程序的主要部分中调用此函数 def main(): params = get_user_input() # 根据输入的参数设置数据结构和算法 # ... if __name__ == "__main__": main() |
4:段式和页式的区别,如果改成段式存储,则如何实现段式首地址和偏移量?
段式和页式的区别:
页式存储:页机械地划分为大小相同的块。页式管理是以定长页面进行存储管理的方式。
段式存储:段会按程序逻辑划分成的相对独立可变长的块。段式管理是把主存按段分配的存储管理方式。
整体区别如下图所示。
如果改为段式存储,如何实现段式首地址和偏移量:
- 段表的构建:
段表可以用数组或者列表实现,数组的索引或列表的位置可以用作段号。
每个表项 = 段基址 + 段长度。
(1)段基址:段在内存中的起始物理地址。
(2)段长度:段在内存中的长度。 - 地址转换:
虚拟地址由两部分组成:段号和段内偏移量。
地址转换过程需要三个步骤,分别是:定位段、检查越界、计算物理地址。
(1)定位段:使用段号从段表中查找到相应的段基址和段长度。
(2)检查越界:检查偏移量是否超过了段长度。
(3)计算物理地址:将段基址和偏移量相加,得到物理地址。 - 参考代码如下所示:
class Segment: def __init__(self, base, length): self.base = base self.length = length def create_segment_table(): # 示例:创建包含几个段的段表 return [Segment(1000, 300), Segment(2000, 400), Segment(3000, 500)] def translate_address(segment_table, segment_number, offset): if segment_number >= len(segment_table): raise ValueError("无效的段号")
segment = segment_table[segment_number] if offset >= segment.length: raise ValueError("偏移量越界") return segment.base + offset # 示例使用 segment_table = create_segment_table() physical_address = translate_address(segment_table, 1, 50) # 段号1,偏移量50 print("物理地址:", physical_address) |
四、实验总结
1:虚拟存储器是存储器的逻辑模型,借助于磁盘等辅助存储器来扩大主存容量,为更大或更多的程序所使用。
2:物理地址由CPU地址引脚送出,用于访问主存的地址。
3:虚拟地址由编译程序生成,是程序的逻辑地址,其地址空间的大小受到辅助存储器容量的限制。
4:【主存-外存层次】和【cache-主存层次】用的地址变换映射方法和替换策略是相同的,都基于程序局部性原理(时间局部性 + 空间局部性)。
5:虚拟存储器的实现,基于基本信息传送单位、替换算法、地址映射、一致性问题。
6:替换算法主要分为以下几种:
(1)LRU:近期最少使用算法。当需要替换一个页面时,选择最近最久未被使用的页面进行替换。它维护一个页面访问历史记录,并将最近被访问的页面置于队列的前面,而最久未被访问的页面在队列的末尾。当需要替换页面时,选择队列末尾的页面进行替换。
(2)LFU:最不经常使用算法。当需要替换一个页面时,选择被访问次数最少的页面进行替换。它维护一个计数器来跟踪每个页面的访问次数,并选择访问次数最少的页面进行替换。
(3)FIFO:先进先出算法。最早进入内存的页面会被最早替换出去。它使用一个队列来维护内存中的页面顺序,当需要替换页面时,选择队列中的最早进入的页面进行替换。
(4)随机替换算法。当需要替换一个页面时,随机选择集合中的一个页面进行替换。
(5)LFU + FIFO。
7:有效的页面置换算法可以显著减少缺页中断,提高系统效率。
五、代码修改
基于VA-Converting.cpp代码进行修改,实现【自行设计地址变换模拟器】。
#include "stdio.h" #include "math.h" #include"malloc.h" #include "stdlib.h" #include<iostream> #include "math.h" #define OK 1 #define OVERFLOW -1 #define ERROR -1 #define Max 5 typedef int status; typedef int SElemType; using namespace std; int k=0;//记录缺页次数 /*--------------------栈及其操作---------------------*/ typedef struct { SElemType *base; //栈底指针 SElemType *top; //栈顶指针 int count; //栈的大小 }SqStack; //构造空栈 status InitStack (SqStack &S){ S.base=(SElemType *)malloc(Max * sizeof(SElemType)); if (!S.base) return(OVERFLOW); S.count = 0; S.top = S.base; return(OK); } //入栈 status Push(SqStack &s,SElemType e) { *s.top++=e; s.count++; // cout<<"插入"<<e<<endl; return OK; } //销毁栈 status DestroyStack(SqStack &S) {
S.top=NULL; S.base=NULL; delete[] S.base; S.count=0; return OK; } //判断栈是否为空 bool EmptyStack(SqStack s) { if(s.count==0) return true; else return false; } //是否已满 bool full(SqStack s) { if(s.count==5) return true; return false; } //判断是否已经存在 int equeal(SqStack s,SElemType e) { int num=s.count; if(EmptyStack(s)) return -1; for (int i=1;i<=num;i++) { if(*(s.top-i)==e) return i; } return -1; } //输出 void print(SqStack s){ int a,i,num=s.count; //cout<<"有"<<num<<"个数"<<endl; for (i=0;i<num;i++) { a=*(s.base+i); cout<<a<<" "; } cout<<endl; } /*----------------页表---------------------*/ int pageLen=65536;//页面数 int l=2048; //页面大小 struct page{ int pageNum; int memNum; }p[70000]; int initiate(){ int i,j; for (i=0;i<pageLen;i++) { j=rand()%11; p[i].pageNum=i; p[i].memNum=i+j; } return 1; } int locate(int n){ for (int i=0;i<pageLen;i++) { if (p[i].pageNum==n) { return p[i].memNum; } } return -1; } //物理地址 int adress(){ int pa,d,adr; cout<<"请输入逻辑地址:"; cin>>adr; cout<<endl; pa=(int)adr/l; if (pa<0||pa>=l) { cout<<"越界"<<endl; return -1; } d=adr%l; int n=locate(pa); if (n!=-1) { int m=n*l+d; cout<<"其对应的页号是:"<<pa<<endl; cout<<"其对应的页内地址是:"<<d<<endl; cout<<"此页号地址对应的块号是:"<<n<<endl; cout<<"物理地址="<<n<<"*"<<l<<"+"<<d<<endl; cout<<"其物理地址为:"<<m<<endl; return m; } else{ cout<<"此地址无对应项"<<endl; return -1; } } //随机替换算法 int randReplace(SqStack& s, SElemType e){ int i; int num=equeal(s, e); cout<<"访问页面"<<e; if(!full(s)) { if (num==-1){ cout<<",不存在此页号,"<<e<<"入栈"<<endl; k++; Push(s,e); } } else{ if(num == -1){ k++; cout<<",此页号不存在且栈满,随机替换栈中页面,并"<<e<<"入栈"<<endl; int randomIndex=rand()%Max; s.base[randomIndex]=e; } } return 1; } //执行随即替换算法 int begin(){ int i,n,m; SElemType a[100]; SqStack s; InitStack(s); cout<<"请输入页面数:"; cin>>n; if (n>100){ cout<<"输入的数值太大!!"<<endl; return 0; } cout<<"请输入页面号序列(0~~9):"<<endl; for(i=0;i<n;i++){ cin>>m; if(m<0||m>9){ cout<<"输入错误,请重新输入!!"<<endl; i--; continue; } a[i]=m; } for (i=0;i<n;i++) { randReplace(s,a[i]); cout<<"结果为:"; print(s); } DestroyStack(s); cout<<"一共缺页"<<k<<"次"<<endl; return 1; } int Cachenum=64; int CacheV=32; int CACHE[65]; void cacheadress(int pad){ int Index=pad/CacheV%Cachenum; int Offset=pad%CacheV; CACHE[Index]=pad/CacheV; cout<<"物理地址"<<pad<<"对应的cache索引为:"<<Index<<endl; cout<<"物理地址"<<pad<<"对应的cache块内地址为:"<<Offset<<endl; } int CacheHit(int pad){ for(int i=0;i<Cachenum;++i){ if(CACHE[i]==pad/CacheV){ return 1; } } return -1; } int x; int main(){ int ok=0; initiate(); while (ok!=3){ cout<<" **********菜单***********"<<endl; cout<<" 1--执行随即变换算法"<<endl; cout<<" 2--地址变换 "<<endl; cout<<" 3--退出 "<<endl; cout<<" *************************"<<endl; cout<<"请输入菜单号:"; cin>>ok; switch (ok){ case 1: begin(); break; case 2: cout<<"页面大小是2kb"<<endl; cout<<"物理地址=其所对块号*页面大小+页内地址"<<endl; x=adress(); if(CacheHit(x)==1) cout<<"cache成功命中"<<endl; else cout<<"cache命中失败"<<endl; cacheadress(x); break; default: break; } } return 1; } |