一、实验内容
二、源码分析
1. 理解nachos单线程地址映射机制
Machine::Run()
中调用Machine::OneInstruction(Instruction *instr)
逐条执行可执行文件中的指令,执行指令过程中和获取下一条指令时如果访问内存,通过machine->ReadMem(…)/WriteMem(…)
完成,这个函数先用Translate(addr, &physicalAddress, size, FALSE)
测试是否会发生异常,如有异常则获取异常类型,在Translate(…)外调用machine->RaiseException(…)
处理异常;如无异常,则完成虚拟地址到物理地址的转换,把物理地址放到指针参数中传出,并在Translate(…)
外用物理地址直接读mainMemory内容。Translate(…)
中有两种地址互斥的转换工具,一个是pageTable,一个是TLB(正常操作系统中应该是二者共存,pageTable中是完整的页表,TLB中是部分pageTable的缓存),在使用pageTable情况下,如果addr对应的逻辑页的表项valid==FALSE
,就会返回PageFault,而且有且仅有这一种情况会返回PageFault。pageTable中保存了逻辑页和物理页的对应关系,由此完成虚拟地址到物理地址转换。在使用TLB情况下,会对整个TLB进行遍历来比对给出的逻辑页和TLB中存的每个逻辑页,然后找到对应页的物理页,如果找不到就返回PageFault。我们只需要做pageTable这种就可以。
现在问题在于,这个addr是从指令中读出的地址,这个地址肯定是逻辑地址,但它是相对于本程序开始位置的地址还是它在整个内存空间中的逻辑地址呢?编写用户程序的人写程序的时候肯定不知道自己的程序运行时会被加载到哪个位置,但问题在于加载器是否会去改动汇编程序中的地址,来让它变成一个全局的地址呢?网上资料众说纷纭。问了老师,老师说是一种“粗粒度”的位置无关代码,即这是相对于程序开始位置的地址。
这个观点在AddrSpace中得到证实。AddrSpace是用executable初始化的,也就是跟当前进程直接相关,并且在AddrSpace初始化的时候才new了一个新的页表,即不同进程有不同的页表,而且查看system文件中也没有存全局的页表,所以用户程序中的地址只能是相对于本程序起始位置的地址。也就是说,nachos直接跳过了全局逻辑地址这一步,直接完成了从相对于程序起始位置的逻辑地址到主存中实际的物理地址的映射。(这和我理解的正常操作系统也不一样,《操作系统》教材中讲的是,首先有个操作系统管理的全局的页表,每个进程获取这个全局页表中本进程部分的副本,最后交给机器执行的程序中的内存地址是全局的逻辑地址,所以可能出现访问到其它进程表项的危险,所以才有各种保护措施,所以多进程情况下可能出现因为访问了没有权限访问的内存位置而出现的异常)。PageTable每个页表项中有virtualPage, physicalPage, valid, use, dirty, readOnly六项,在AddrSpace构造器中初始化,其中物理页与虚拟页相同(我很不理解为什么要有虚拟页这一项,因为对于所有进程和所有情况,包括多道程序,virtualPage都等于该项的索引,并且没有任何机会修改它。单进程并使用pageTable情况下,初始化AddrSpace时,先计算了该可执行程序的代码段长度、已初始化数据段长度和用户栈长度(1024)之和,如果这个总长度大于主存大小,就根本不让加载这个程序。所以可以断定,在nachos本来支持的单进程方式中,一定不会出现PageFault。
另外很有趣的一点是,Nachos没有PCB,是AddrSpace充当了PCB来描述进程。进程切换时的上下文是在它所在的内核线程中保存的。另外Scheduler没有提供迫使某个Thread放弃CPU的操作,即Nachos是非抢占的。
2. 多进程问题与解决方案
核心是两个问题:如何让多个进程在主存中共存,并在执行指令时完成相对于本进程的逻辑地址到主存中物理地址的转换;如何完成进程的创建和切换。
对于第一个问题的前半部分,能带起来exec.noff的最简单的方案是连续分配内存,即先跑起来的进程装载在前面,后跑起来的进程装载在后面,但这样对于更多进程的情况就会出现问题,首先是,新装载的进程的起始物理地址是哪里?这个可以通过维护一个系统变量记录上一个进程的占用截至位置解决。但如果这个位置之后的空内存不够了怎么办?以及如何判断它不够了?光维护一个记录占用帧数的系统变量,是无法应对由于位置靠前的进程结束、撤出而带来的内存孔洞问题的。所以我的解决方案是,使用bitmap数据结构,每占用一个物理帧,就把它在bitmap中的相应位置置1,当某个进程撤出系统,就把它占用的所有物理帧在bitmap中的相应位置置0,给新进程分配物理帧时,使用bitmap中的Find函数找到一个空帧给它,并存在该进程的pageTable中。逻辑地址到物理地址的映射和nachos原本提供的单进程情况完全一样(就因为没有到全局逻辑地址这一层映射)。
第二个问题涉及系统调用中的操作。我们需要讨论两种初始化进程的方式,一种是如同main函数创建用户进程,它直接调用了progtest.cc中的StartProcess,运行命令行中提到的用户程序,StartProcess中是用新创建的进程的地址空间直接挂到了当前Thread的地址空间指针上,相当于就把自己替换掉了,虽然它和新进程共存于内存中,但它就永远无法再被执行了,还占着一块内存。形象来说,startProcess的做法中,currentThread就像一个出租车,用户进程就像是乘客,只有乘客A下车了,乘客B才能上车,上下车之后车还是那个车。但我们想要的不是这样,我们想要的是乘客A和乘客B能一起走在路上,这样就需要两辆出租车一起开。乘客B上车的时候,乘客A不能下车,那么乘客B就应该坐另一辆出租车,也就是另外Fork一个内核Thread,并将子进程的AddrSpace挂在新的Thread上。尽管在非抢占的情况下,而且针对于我们测试用的exec.noff,前一种方式的结果和正确的结果是差不多的,但前一种方案明显非常不合理。
还有一个小问题,AddrSpace的初始化应该由当前thread完成,还是交给新Fork的Thread?我觉得是后者。在exception.cc中Fork新thread,并且把ProcessStub作为要执行的函数、从r4寄存器中拿到的filename地址作为参数给新的thread。已经注意到,对于做系统调用的进程,它会在异常处理结束后回到RaiseException函数,这个函数调用ExceptionHandler(which)
之前把系统模态设为SystemMode,调用之后设为UserMode,所以不用担心。
三、实现方案
对于第一个问题,我修改了AddrSpace构造器中初始化物理页为通过machine->phymem_bitmap->Find()
来获取一个空物理帧,这个phymem_bitmap是我新增的machine的一个成员变量(事实上它应该属于system更好一些,因为machine是对机器硬件的模拟,system是管理者),并且用code和initData填充物理帧时,也通过页表进行从逻辑地址到物理地址转换(以应对不连续分配的问题)。并且维护一个系统变量remainVirMemPage计算剩余的物理帧(因为要为lab7做准备,所以命名为Vir…),每占用一个物理帧就-1,每释放一个物理帧就+1(析构AddrSpace时逐个地放一批)。
对于第二个问题,解决方案上文已经讲明。实现中,由于创建线程时只能传一个int类型参数,所以不能把待运行程序的文件名直接作为参数传给新线程,而且创建线程也需要运行一个函数,而不是运行一个文件。所以我的ExceptionHandler先从寄存器4中获取待运行程序的文件名;然后创建一个新线程,把这个地址和一个ProcessStub函数交给新线程;本线程调用advancePC()
完成PC+1的操作(这是因为mipssim.cc中,当涉及访存操作时,处理完都是直接return而非break,使得它没有执行函数末尾的PC+1,因为有些异常需要重新执行当前指令,比如pageFault);然后让当前线程放弃CPU而引发调度让其它(新)进程执行(这里应该不放弃也行,就是当前进程结束后自然放弃CPU让其它(新)进程执行)。ProcessStub中,我先根据传进来的文件名所在内存的起始地址获取文件名(如果遇到\0就结束读取字符串),并从文件系统中获取这个可执行文件,然后用这个可执行文件初始化AddrSpace,并把这个addrSpace挂载到“当前线程”上,然后删掉一些过程中的变量,初始化寄存器和状态,然后开始运行。需要特别注意的是,这个“当前进程”并非父进程,尽管把ProcessStub交给新线程的时候本线程还没有Yield,但这只是将这个新线程加入到了就绪队列,本线程只要不放弃CPU新线程是不会执行的。所以上文中的“当前进程”其实是这个新线程,是新进程的“出租车”。
另外一个小问题,就是Exec()
系统调用需要返回一个SpaceId(是个int),所以我在addrspace.h中加上一个int id成员变量,在addrSpace初始化时获取一个值赋给id。并且在machine中维护一个int pidCounter
。每调用一次Exec()
,pidCounter都要+1。那么在哪里做这个+1:① 在抢占式的策略中,不可以在ExceptionHandler中Fork之后做pidCounter++的工作,因为不一定新进程和原进程谁先执行。② Fork之前完成,这样pidCounter是从1开始的。③ 在AddrSpace初始化的时候,用machine->pidCounter给id赋值之后,就给它++。我使用了第三种,因为第一种不稳妥,第二种可能出现没有Fork成功,但pidCounter已经++的情况。
新的问题是,获取pidCounter和pidCounter++之间是否应该关中断。我认为这里没必要,因为如果两个进程都在获取pidCounter,而其中一个获取到pidCounter,这时发生了interrupt,nachos处理interrupt的时候甚至完全没有离开当前线程,直接把interrupt从队列中拿出来扔掉了(nachos是非抢占式调度,即使离开当前线程去处理interrupt,处理完毕之后也会回来的)。但在抢占式调度中,必须关中断,否则就可能出现两个进程获取到相同的pidCounter的情况。nachos源码中的关中断操作,一般是因为中断处理的过程中可能改变当前的操作关注的机器属性。而对于pidCounter,没有来自外部的中断对这个值感兴趣。而内部对它感兴趣的中断没有机会被执行,所以它很安全。
另外考虑到一个问题,关中断期间出现的程序性中断是否可能造成中断打不开?结论是不可能。int是32位的,即pidCounter的最大值不能超过2^32,在极端情况下,关中断期间如果发生的溢出,将在开中断后的第一条用户指令后被处理。
四、关键代码
void ExceptionHandler(ExceptionType which){……case SC_Exec:{DEBUG('a', "Execute a new user process.\n");int addr=machine->ReadRegister(4);//rent a car for the new processThread *t = new Thread("NewUserProcess");t->Fork(ProcessStub, addr);//only responsible for putting new thread in ready queue, and will go back.advancePC();currentThread->Yield();break;} default:{DEBUG('a',"wrong type!!!!!!!!\n");break;}……
}void advancePC(){machine->WriteRegister(PCReg,machine->ReadRegister(PCReg)+4);machine->WriteRegister(NextPCReg,machine->ReadRegister(NextPCReg)+4);
}void ProcessStub(int filenameAddr){char * filename=new char[20];for(int i=0;i<20;i++){int value;machine->ReadMem(filenameAddr+i,1,&value);if(value=='\0')break;filename[i]= value;}filename=%c%c%c%c%c%c%c%c\n",filename_chars[0],filename_chars[1],filename_chars[2],filename_chars[3],filename_chars[4],filename_chars[5],filename_chars[6],filename_chars[7]);OpenFile *executable = fileSystem->Open(filename);AddrSpace *space;if (executable == NULL) {return;}space = new AddrSpace(executable); currentThread->space = space; //say goodbye to the stubdelete executable; // close filespace->InitRegisters(); // set the initial register valuesspace->RestoreState(); // load page table registermachine->Run(); // jump to the user progamASSERT(FALSE); // machine->Run never returns;
}AddrSpace(OpenFile *executable){……ASSERT(numPages <= machine->remainVirMemPage);……pageTable[i].physicalPage = machine->phymem_bitmap->Find();//可能不连续……if (noffH.code.size > 0) {DEBUG('a', "Initializing code segment, at 0x%x, size %d\n",noffH.code.virtualAddr, noffH.code.size);int next_to_load_virtualAddr=noffH.code.virtualAddr;int remain_code_bytes=noffH.code.size;while(remain_code_bytes>0){int load_bytes=remain_code_bytes<PageSize?remain_code_bytes:PageSize; executable->ReadAt(&(machine->mainMemory[virAddr2phyAddr(next_to_load_virtualAddr)]),load_bytes,noffH.code.inFileAddr+next_to_load_virtualAddr-noffH.code.virtualAddr);remain_code_bytes-=load_bytes;next_to_load_virtualAddr+=load_bytes;}}if (noffH.initData.size > 0) {DEBUG('a', "Initializing data segment, at 0x%x, size %d\n",noffH.initData.virtualAddr, noffH.initData.size);int next_to_load_virtualAddr=noffH.initData.virtualAddr;int remain_initData_bytes=noffH.initData.size;while(remain_initData_bytes>0){int load_bytes=remain_initData_bytes<PageSize?remain_initData_bytes:PageSize;
executable->ReadAt(&(machine->mainMemory[virAddr2phyAddr(next_to_load_virtualAddr)]),load_bytes, noffH.initData.inFileAddr+next_to_load_virtualAddr-noffH.initData.virtualAddr);remain_initData_bytes-=load_bytes;next_to_load_virtualAddr+=load_bytes;}}id=machine->pidCounter;machine->pidCounter++;machine->remainVirMemPage-=numPages;//newly added. it's save at anywhere unless use preemptive strategyprintf("spaceId:%d\n",id);Print();
}
五、实验结果
查看全文
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.dgrt.cn/a/248001.html
如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!
相关文章:
山东大学软件学院操作系统课程设计(2021秋季,nachos)实验6
一、实验内容 二、源码分析
1. 理解nachos单线程地址映射机制
Machine::Run()中调用Machine::OneInstruction(Instruction *instr)逐条执行可执行文件中的指令,执行指令过程中和获取下一条指令时如果访问内存,通过machine->ReadMem(…)/WriteMem(………
在复苏与重塑之路上,同程旅行为旅游业价值回归交出答卷
若论对疫情感受最深刻的行业,旅游业必然榜上有名。也许这个产业链上的每个玩家在这两年都思考过这样两个问题:客观上,旅游业恢复的基础条件有哪些?主观上,又该用什么措施、什么方法应对现在的局面?
尽管疫……
Spring Boot Auto-Configuration
Spring 自定义Auto-Configuration
Spring Boot 可以根据classpath中依赖关系自动装配应用程序。通过自动装配机制,可以使开发更快、更简单。今天,学习下如何在Spring Boot 中创建自定义 auto-configuration。
代码运行环境
JDK17MYSQL8源码地址
Mave……
Android 10.0 系统禁用深色主题背景功能
目录
1.概述
2.系统禁用深色主题背景功能相关核心代码…
数据结构初阶:队列
目录
一、队列的概念和结构
二、队列的实现 定义队列结构 初始化队列 销毁队列 检测队列是否为空 入队列 出队列 获取队列头部元素 获取队列队尾元素 获取队列中有效元素个数 优化
三、测试 四、优化后的全部代码 一、队列的概念和结构 队列:只允许在一端进行插入数据操作……
服务器优化
文章目录服务器负载分析CPU 使用率内存使用率磁盘 I/O平均负载网络使用情况服务器内核参数调优单个进程最大打开文件数TCP 相关设置服务器负载分析
在性能调优时,需要先对服务器负载进行分析,通常而言,我们主要分析 CPU 使用率、内存使用率、……
知识引擎藏经阁天花板——高性能Java架构核心原理手册
开场 本书是按照程序设计与架构的顺序编写的,共13章。
第1章介绍学习高性能Java应了解的核心知识,为前置内容。
第2章和第3章讲解在编写代码之前,如何高效地为My SQL填充亿级数据,并对My SQL进行基准测试,以便在之后……
如何测试 esp-matter_example_light 例程
此例程支持三种配网方式:
苹果手机扫码配网chip-tool 命令配网Matter 指令配网 1 使用苹果手机扫码配网 说明文档:Apple Matter 测试方法 所需设备:
苹果手机(最新版本 IOS 系统)苹果音响(Apple-Matter&a……
旅游定制服务|基于SSM实现旅游个性化定制网站平台
旅游定制订单管理 旅游订单管理 作者主页:编程千纸鹤 作者简介:Java、前端、Pythone开发多年,做过高程,项目经理,架构师 主要内容:Java项目开发、毕业设计开发、面试技术整理、最新技术分享 收藏点赞不迷路……
Linux-sed
sed
sed是一种几乎包括所有UNIX平台(包括Linux)的轻量级流编辑器。sed主要用来将数据进行选取、替换、删除、新增的命令
sed [选项] ‘[动作]’ 文件名 选项: -n:一般sed命令会把所有数据都输出到屏幕,如果加入此选项……
反序列化渗透与攻防(五)之shiro反序列化漏洞
Shiro反序列化漏洞
Shiro介绍
Apache Shiro是一款开源安全框架,提供身份验证、授权、密码学和会话管理。Shiro框架直观、易用,同时也能提供健壮的安全性
Apache Shiro 1.2.4及以前版本中,加密的用户信息序列化后存储在名为remember-me的Cookie中。攻击者可以使用Shiro的默……
vue2+vue3
vue2vue3尚硅谷vue2vue2 课程简介【02:24】vue2 Vue简介【17:59】vue2 Vue官网使用指南【14:07】vue2 搭建Vue开发环境【13:54】vue2 Hello小案例【22:25】了解: 不常用常用:id 更常用 简单class差值总结vue 实例vue 模板 : 先 取 ࿰……
【hello Linux】环境变量
目录 1. 环境变量的概念 2. 常见的环境变量 3. 查看环境变量 4. 和环境变量相关的命令 5. 环境变量的组织方式 6. 通过代码获取环境变量 7. 通过系统调用获取环境变量 Linux🌷 在开始今天的内容之前,先来看一幅图片吧! 不知道你们是否和我一……
【Linux基础】常用命令整理
ls命令
-a选项,可以展示隐藏的文件和文件夹-l选项,以列表形式展示内容-h,需要和-l搭配使用,可以展示文件的大小单位ls -lah等同于la -a -l -h
cd命令(change directory)
语法:cd [Linux路径]……
客快物流大数据项目(一百一十二):初识Spring Cloud
文章目录
初识Spring Cloud
一、Spring Cloud简介
二、SpringCloud 基础架构图…
C和C++中的struct有什么区别
区别一: C语言中: Struct是用户自定义数据类型(UDT)。 C语言中: Struct是抽象数据类型(ADT),支持成员函数的定义。
区别二:
C中的struct是没有权限设置的,……
docker的数据卷详解
数据卷 数据卷是宿主机中的一个目录或文件,当容器目录和数据卷目录绑定后,对方修改会立即同步
一个数据卷可以同时被多个容器同时挂载,一个容器也可以被挂载多个数据卷
数据卷作用:容器数据持久化 /外部机器和容器间接通信 /容器……
13、Qt生成dll-QLibrary方式使用
Qt创建dll,使用QLibrary类方式调用dll
一、创建项目
1、新建项目->其他项目->Empty qmake Project->Choose 2、输入项目名,选择项目位置,下一步 3、选择MinGW,下一步 4、完成 5、.pro中添加TEMPLATE subdirsÿ……
基于mapreduce 的 minHash 矩阵压缩
Minhash作用: 对大矩阵进行降维处理,在进行计算俩个用户之间的相似度。
比如: 俩个用户手机下载的APP的相似度,在一个矩阵中会有很多很多的用户要比较没俩个用户之间的相似度是一个很大的计算任务 如果首先对这个矩阵降维处理&am……
关于hashmap使用迭代器的问题
keySet获得的只是key值的集合,valueSet获得的是value集合,entryset获得的是键值对的集合。 package com.test2.test;import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;public class mapiterator……
编程日记2023/4/16 14:50:37