实验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,需要对哪些数据结构以及算法进行改进?
2:如果采用TLB快表,则需如何改进方案?
TLB快表用于加速虚拟地址到物理地址的转换过程。
3:如果虚拟地址大小、物理地址大小、虚页大小、cache大小、cache块大小、相联度等参数可以通过用户输入的情况,需要对哪些部分进行改进?
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:段式和页式的区别,如果改成段式存储,则如何实现段式首地址和偏移量?
段式和页式的区别:
页式存储:页机械地划分为大小相同的块。页式管理是以定长页面进行存储管理的方式。
段式存储:段会按程序逻辑划分成的相对独立可变长的块。段式管理是把主存按段分配的存储管理方式。
整体区别如下图所示。
如果改为段式存储,如何实现段式首地址和偏移量:
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 #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<<"插入"< 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; |