关于逻辑内存和物理内存转换的过程在lab6报告中已经详细讲解,异常处理也在lab1报告中解释了,在此我们只专注于虚拟内存相关事情。
没有虚拟内存时,能在程序运行中被访问的内存空间只有物理内存空间那么大(32*128),因此加载一个可执行文件时即使内存空空如也,这个文件的code和initData和userStack加在一起的大小要是超过物理内存大小,就无法运行。有虚拟内存情况下,程序空间可以超过物理内存空间(但不可以超过虚拟内存空间),如果机器执行某条命令时访问的内存位置所在的页不在物理内存中,则需要从交换区调入一页进入物理内存,其中,如果物理内存中有空页,就直接调入该空页;如果没有空页,则从所有物理内存中选出一个“不太重要的页”放回交换区,再将我们想要的页读入物理内存。
上述过程中,有几个可讨论的地方。
第一是加载进程时是否需要往已有物理内存里面填一部分初始化数据和代码。填与不填都可以,填的话,加载时有空位的时候就把当页页表项的物理页号设为分给他的物理页,并把valid设为1,如果空位用光了,就把剩下内容的物理页号设为-1,valid设为0。然后加载code和initData时加载到它们的物理地址上。不填的话,就是把所有页表项的物理页号都设为-1,valid设为0,然后等待程序运行时第一次触及到某个地址的时候再把这个地址所在的页从交换区读出来。这两种在实现上难度没有太大差别,我选择了第二种纯换页策略。
第二个讨论点是,交换区应该放在哪,放多大。正常的操作系统的交换区是磁盘中一块单独的区域,nachos中,由于没有“磁盘哪部分应该专门为什么准备”的协议,所以其它文件(比如可执行文件)很有可能也放在我给它设定的位置,就把交换区里面内容冲掉了,所以我放弃了这个方案。鉴于此,可以把宿主机中一个文件作为交换区,达到“专用”的目的。
另一个问题是,交换区应该放文件的所有内容还是只放物理内存里没有的内容。前者虽然占了更多存储空间,但它的好处是不是每一次换出都需要写回(而是可以选择性地换出非脏页,这样不需要写回),从而省去了写回的时间。对于一个更多是在读内存的文件,后者的优势是更明显的。而且从实现的角度考虑,如果使用了前者策略,就需要再完成一道从虚拟地址到交互区内地址的转换,非常的麻烦。另外,这种策略中我们需要仔细计算交换区的大小,如果恰好让交换区大小+物理内存大小等于虚存大小,这看起来很合理,但实际会陷入“死锁”的(这里只是泛用了死锁的思想),当物理内存和交换区都恰好已满时,如果有个程序想访问不在物理内存中的页,就需要选择一个物理内存页,如果恰好这个页是脏的,就要写出到交换区某一页,但此时交换区是满的,这个页绝不可以覆盖掉即将进入物理内存中的页,因为那样将后者读入物理内存的时候它就不是它了,这就发生了错误。我的第一版lab7用的是这种策略,但考虑并解决了上述问题后,我发现另一个非常棘手的问题就是,加载的时候是code和initData是分开加载的,可能需要把最后那部分code和最开始一部分initData放在同一页中。这个连接工作异常的艰难,还经常出问题。最后,鉴于我做到这里时已经是凌晨3点,我困了,所以我决定把所有内容都读入交换区。
第三个讨论点是选择“不太重要的页”的策略。几种算法都可以,我在“好实现”和“理论性能好”中权衡了一下,于是用的是自己设计的增强型二次机会法,每次调用算法时从上一次结束算法的地方开始(我记得《操作系统》教材中这里没有明确说明,以至于我一直觉得它每次是从头开始的,因此我实现的初期也用的从头开始,但调试的时候发现,这样会严重偏向小号帧,很不合理,所以增加了这样一个连续循环的机制),第一轮逐个检查帧的标志位,如果它的used位为1,就给它一次机会,并把它的used位设为0;如果它used位为0且dirty位为0,那它就是最合适换出的页,因为它换出不需要写回,直接拿新页覆盖掉即可;如果它used位为0且dirty位为1,那也先跳过它。如果进入了第二轮,说明第一轮中要不就是一个不太久远的帧,要不就是一个脏帧,考虑到nachos的物理内存很小,每一帧很小,写出也不是很费时,而且一个脏帧它早晚得写回去;并且从算法复杂度的角度,选择换出不太久远的帧只需要在第二轮中选择第一个帧即可,如果选择脏页换出还需要遍历找到脏帧之前的每个帧,而且如果在这一轮的最后几帧发现它是脏的,就会非常纠结要不要给它换出去,因为它的使用时间距离现在很近,很可能和当前页处于同一个工作集中,不应该被换出去。综上所述,第二轮中我直接选择了第一帧,即最老的一帧换出去。
第四个是参与换页规模,可以在所有物理帧中选择换出者,也可以在本进程自己占用的物理帧中选择换出者。《操作系统》书上说,前者的优势在于会有更好的系统吞吐量,也更常用,但不能控制页错误率;后者分配给每个进程的帧的数量不变,它的优势是每个进程的执行性能只和它自己有关,尤其当出现颠簸的时候,可以把灾难控制在小范围。由于Nachos是非抢占式的,某个进程执行的时候别的进程都在长时间地休息,那些进程占用的帧非常闲,使用了它们的帧也不会对它们(已经很糟糕的)性能带来太多不利影响,所以选择全局置换更合适。但实现时,由于没有系统页表,所以物理帧管理器(我自己写的一个辅助类)即使在加载页的时候记录了占用该帧的进程id,也无法根据id找到页表,再将页表中该物理页对应的虚拟页的表项的valid改为false。所以解决方案是,让系统维护一个各个进程pageTable的指针数组,每个进程在创建pageTable时把它放入pageTable中自己的位置(这个位置的选择也很烦人,我原本想用map实现,但map中一些函数可能是和nachos中函数冲突了,无法使用;vector可以动态伸缩数组大小,但vector.erase(…)
也不能用;如果自己写hash算法又很麻烦,而且复杂度就高了,最后我退而求其末,直接把这个数组开到某个武断地给出的值,每个进程直接把自己的pageTable放到以自己id为索引的位置上),析构AddrSpace时把系统页表中自己的位置设为NULL
。这样物理帧管理器就可以修改id对应进程的pageTable了。如果使用局部置换就直接在自己的页表中试探每个虚拟页对应的物理页是否适合被换出,如果适合就让物理帧管理器帮忙换出,并修改该页表项即可。我采用的方案是全局置换。
还有一个小小技术,就是物理帧管理器要管理的东西,除了bitmap,还要记录每个物理帧是哪个进程占用着,以及这个页是否被使用或被修改过(事实上,页置换算法中判断页面是否为脏或者被使用的时候,并没有使用pageTable中的dirty和use位,而是用的物理帧的dirty和used。在这次课设中,其实完全可以用int id, int dirty, int used来保存这三个变量。但考虑到实际的操作系统中,内核空间非常紧张,必须每一个数据结构都小心地设计,因此我这里采用了一个int解决这三个变量,即将id左移8位,新数值的最后两位分布保存used和dirty位,每个占1位,获取、修改id、used和dirty位时通过移位运算以及与1或0进行“与、或、非”操作实现。这要求id大小不能超过2^24,考虑到一个操作系统中维护进程运行的其它资源更加稀缺,相比之下这24位空间已经很富裕了,也就是制约了可以产生的id号的瓶颈并不在此,所以可以放心移位。
exception.cc中ExceptionHandler中调用DynamicLoad(badVAddr)
函数,这个函数负责所有的判断物理内存剩余空间、选择一个空闲帧分配、选择一个牺牲页、将牺牲页写回、导入新页并修改换出和换入页的页表项的工作。但具体工作也是调用了其它对象的方法完成的,其中,frameManager是系统中的物理帧管理员,提供了找到一个空闲帧、选择一个“最不重要的帧”、通过帧号获取占用该帧的id号方法,AddrSpace提供了根据物理帧号找本进程中正在占用该帧的虚拟页号、让一个虚拟页变得可用(包括交换区管理员swapManager提供的载入动作)、让一个虚拟页变得不可用(包括swapManager提供的写出动作)。原理、策略、思考已经在前文说了很多了,下面的关键代码中将展示所有相关的修改。
addrspace.cc:
int AddrSpace:: virAddr2phyAddr(int virAddr){
int virPage=virAddr/PageSize;
int virOffset=virAddr%PageSize;
int phyPage=pageTable[virPage].physicalPage;
int phyAddr=phyPage*PageSize+virOffset;
return phyAddr;
}
void AddrSpace:: validPage(int vpage,int phypage){
if(phypage<0)printf("wrong phypage!\n");
printf("valid vpage %d by phypage %d\n",vpage,phypage);
// inspect_memory(phypage*PageSize,PageSize,NUM);
swapManager->ReadPage(&(machine->mainMemory[phypage*PageSize]),vpage*PageSize);
// inspect_memory(phypage*PageSize,PageSize,NUM);
//应该传入一个物理地址,然后把物理地址设上,再改valid
pageTable[vpage].physicalPage=phypage;
pageTable[vpage].valid=TRUE;
frameManager->Mark(phypage,id);
frameManager->SetUse(phypage);
frameManager->ClearDirty(phypage);
// Print();
}
int AddrSpace::GetVpageByFrame(int frame){
for(int i=0;i<numPages;i++){
if(pageTable[i].physicalPage==frame&&pageTable[i].valid==TRUE){
return i;
}
}
}
void AddrSpace::InvalidPage(int vpage){
pageTable[vpage].valid=FALSE;
int phyPage=pageTable[vpage].physicalPage;
int isDirty=frameManager->CheckIsDirty(phyPage);
// printf("frame %d isDirty=%d\n",phyPage,isDirty);
if(isDirty==1){
//char* buffer, int dest
swapManager->DumpPage(&(machine->mainMemory[phyPage*PageSize]),vpage*PageSize);
}
//不需要ClearMark
}
AddrSpace::AddrSpace(OpenFile *executable){
……
ASSERT(numPages <= machine->remainVirMemPage); // check we're not trying
// to run anything too big --
// at least until we have
// virtual memory
DEBUG('a', "Initializing address space, num pages %d, size %d\n",
numPages, size);
executable->ReadAt(&(machine->mainMemory[virAddr2phyAddr(next_to_load_virtualAddr)]),
//都写到宿主机文件中
char bridge[2*PageSize];
char temp[size];
int mix_start;
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;
remain_initData_bytes-=first_load_bytes;
next_to_load_virtualAddr+=first_load_bytes;
bzero(bridge,PageSize);
while(remain_initData_bytes>0){
int load_bytes=remain_initData_bytes<PageSize?remain_initData_bytes:PageSize;
printf("initData: %d-%d\n",next_to_load_virtualAddr,next_to_load_virtualAddr+load_bytes);
executable->ReadAt(&bridge[next_to_load_virtualAddr%PageSize],load_bytes, noffH.initData.inFileAddr+next_to_load_virtualAddr-noffH.initData.virtualAddr);
swapManager->DumpPage(bridge,next_to_load_virtualAddr);
remain_initData_bytes-=load_bytes;
next_to_load_virtualAddr+=load_bytes;
}
}
AddrSpace::~AddrSpace()
{
for(int i=0;i<numPages;i++){
frameManager->Clear(pageTable[i].physicalPage);
}
// ProcessList.erase(*this);
// ProcessMap.erase (id);
PageTables[id]=NULL;
delete [] pageTable;
machine->remainVirMemPage+=numPages;
}
exception.cc:
ExceptionHandler(ExceptionType which){
……
else if(which == PageFaultException){
int badVAddr=machine->ReadRegister(BadVAddrReg);//badVAddr是个相对于文件起始位置的逻辑地址
interrupt->DynamicLoad(badVAddr);
}
……
}
FrameManager.cc:
#include "copyright.h"
#include "machine.h"
#include "system.h"
#include "FrameManager.h"
BitMap * phymem_bitmap;
int * frameTable;
FrameManager::FrameManager(){
phymem_bitmap=new BitMap(NumPhysPages);
frameTable=new int[NumPhysPages];
for(int i=0;i<NumPhysPages;i++){
frameTable[i]=-1;
}
clock_last_stop=-1;
}
void FrameManager::Mark(int which,int pid){
//pid格式:... 15..8 7..2 1 0
// pid use dirty
ASSERT(pid<(1<<20));//留出一些空余
frameTable[which]=pid<<8;//新用上的帧,use和dirty都是0
phymem_bitmap->Mark(which);
}
void FrameManager::Clear(int which){
if(which<0||which>=NumPhysPages)return;
frameTable[which]=-1;
phymem_bitmap->Clear(which);
}
bool FrameManager::Test(int which){
return phymem_bitmap->Test(which);
}
int FrameManager::Find(){
return phymem_bitmap->Find();
}
int FrameManager::GetOwner(int which){
if(frameTable[which]==-1){
return -1;
}
return frameTable[which]>>8;
}
int FrameManager::NumClear(){
return phymem_bitmap->NumClear();
}
bool FrameManager::CheckIsMine(int which, int pid){
ASSERT(pid<(1<<20));//留出一些空余
return (frameTable[which]>>8)==pid;
}
bool FrameManager::CheckIsUsed(int which){
ASSERT(frameTable[which]!=-1);
return (frameTable[which]>>1)&1;
}
bool FrameManager::CheckIsDirty(int which){
ASSERT(frameTable[which]!=-1);
return frameTable[which]&1;
}
void FrameManager::SetUse(int which){
ASSERT(frameTable[which]!=-1);
frameTable[which]=frameTable[which]|2;//0x10
}
void FrameManager::ClearUse(int which){
ASSERT(frameTable[which]!=-1);
frameTable[which]=frameTable[which]&0xfffffffd;//0x11...1 1101
}
void FrameManager::ClearDirty(int which){
ASSERT(frameTable[which]!=-1);
frameTable[which]=frameTable[which]&0xfffffffe;//0x11111111110
}
void FrameManager::SetDirty(int which){
ASSERT(frameTable[which]!=-1);
frameTable[which]=frameTable[which]|1;
}
int FrameManager::Select(){
// printf("clock_last_stop=%d\n",clock_last_stop);
int sacrificer=-1;
int counter=clock_last_stop+1;
while(counter<clock_last_stop+NumPhysPages){
int i=counter%(NumPhysPages);
if(CheckIsUsed(i)){
// printf("frame %d is newly used\n",i);
ClearUse(i);
}else{
if(!CheckIsDirty(i)){
// printf("frame %d is old and is not dirty\n",i);
sacrificer=i%NumPhysPages;
clock_last_stop=counter;
break;//找到了counter就不会++了,所以clock_last_stop指向上一次调入的帧
}
}
counter++;
}
// printf("every frame has been modified\n");
sacrificer=counter%NumPhysPages;
clock_last_stop=counter+1;
return sacrificer;
}
void FrameManager::Release(int pid){
for(int i=0;i<NumPhysPages;i++){
if(frameTable[i]!=-1&&(frameTable[i]>>8)==pid){
frameTable[i]=-1;
phymem_bitmap->Clear(i);
}
}
}
FrameManager::~FrameManager() {
delete[] phymem_bitmap;
delete[] frameTable;
}
Interrupt.cc:
bool Interrupt::DynamicLoad(int badVAddr) {
int targetFrame=frameManager->Find();
if(targetFrame<0){
targetFrame=frameManager->Select();//targetFrame是物理帧号
int owner=frameManager->GetOwner(targetFrame);
//在InvalidPage中完成写回
AddrSpace* tar=PageTables[owner];
int vpage=tar->GetVpageByFrame(targetFrame);
tar->InvalidPage(vpage);
}
return true;
}
SwapManager.cc:
#include "copyright.h"
#include "machine.h"
#include "system.h"
#include "SwapManager.h"
SwapManager::SwapManager(){
file = fopen("swap","wb+"); //打开文件
if(file==NULL)printf("open swap fail\n");
}
void SwapManager::DumpPage(char* buffer, int dest){
// printf("dumppage to %d\n",dest);
// printf("buffer=%d %d %d %d ... %d %d %d %d %d\n",buffer[0],buffer[1],buffer[2],buffer[3],
// buffer[PageSize-4],buffer[PageSize-3],buffer[PageSize-2],buffer[PageSize-1],buffer[PageSize-0]);
fseek(file, dest, SEEK_SET ); // 设置回到文件头
fwrite(buffer, sizeof(char),PageSize, file ); // 在头部写入hello world
// fwrite("hello world\n", strlen("hello world\n"),PageSize, file ); // 在头部写入hello world
}
void SwapManager::ReadPage(char* buffer, int ori){
fseek(file, ori, SEEK_SET ); // 设置回到文件头
int res=fread (buffer, sizeof(char), PageSize, file);
}
void SwapManager::ReadAll(int size){
printf("read all:\n");
char* buffer;
fseek(file, 0, SEEK_SET ); // 设置回到文件头
int res=fread (buffer, sizeof(char), size, file);
for(int i=0;i<size;i++){
printf("%d ",buffer[i]);
if((i+1)%PageSize==0)printf("\n\n");
}
}
void SwapManager::DumpMore(int dest, int size){
fseek(file, dest, SEEK_SET ); // 设置回到文件头
for(int i=0;i<size;i++) {
fputc('\0', file);
}
}
SwapManager::~SwapManager(){
fclose(file);
}
translate.cc:
Translate(…){
……
entry->use = TRUE; // set the use, dirty bits
frameManager->SetUse(pageFrame);
if (writing) {
entry->dirty = TRUE;
frameManager->SetDirty(pageFrame);
}
……
}