尝试实现系统调用Exec() 和 Exit()
需要注意,在前三个步骤不需要修改代码。
一、nachos中系统调用的实现机制
观察nachos/machine/machine,mipssim中的实现可以看出,每一条用户程序中的指令在虚拟机中被读取后,包装成一个OneInstruction对象,经过解码,在mipssim中的一串长SWITCH语句中分别执行不同的操作。
而对于系统调用,在用户程序中的表现是调用nachos/userprog/syscall.h中定义的一系列系统调用函数,而这些函数的实现是定义在nachos/test/start.s中的汇编代码。
可以看出,实际上start.s中,不同的系统调用实际上是把系统调用的类型(SC_***)放入二号寄存器,然后执行指令syscall。
在mipssim中的switch语句中,对syscall的处理如下:
可以看出,这里抛出了一个异常,交给异常处理函数去处理。要注意的是,这里不同于其他指令的处理方式,没有使用break而是使用了return,而一般指令在break之后会进行一些其他处理:
可以看出其中包括了对pc的推进操作。
所以,直接return造成的后果是没有对pc进行推进,那么程序就会再次读入这条系统调用操作,无限循环。所以我们要手动添加这部分对pc推进的代码,稍后会讲到。
而RaiseException函数实际上最终调用的是nachos/userprog/exception.cc中的函数ExceptionHandler(ExceptionType which)。在这里ExceptionType是int的别名,可以当做int处理。
在这个函数中,我们可以看到,它利用了条件判断语句来进行判断,当前错误是系统调用还是用户程序出错,并进一步判断是什么错误。
也不难看出,程序当前只处理了Halt这个系统调用。
所以要完成Exec和Exit,就要在这里添加实现。
二、对Exec和Exit系统调用的分析
对于Exec系统调用来说,在syscall.h中的定义如下:
该调用的功能是,从可执行文件name运行一个新的用户程序,并行执行,并返回新的程序的内存空间标识符SpaceId。
具体的实现思路:根据name指向的地址打开可执行文件,创建AddrSpace,再fork出一个新的线程,将其内存空间设为刚创建的AddrSpace对象,目标函数中,对AddrSpace对象调用InitRegister和RestoreState方法,设置好虚拟机寄存器、内存空间的状态,调用machine->Run()执行。而执行系统调用的线程在fork出新线程后,将SpaceID返回,然后调用AdvancePC使pc寄存器的值前进,避免循环。
对于Exit系统调用来说,在syscall.h中的定义如下:
该调用的功能是,结束用户程序的运行。传入0时正常退出。由于我们没有其他退出情况要处理,所以这个状态码暂时不利用。
具体实现思路:简单地调用currentThread->Finsh()终止当前线程即可。
三、系统调用中对参数、返回值的读取和传递
系统调用中的返回值不同于普通函数的参数传递,参照exception.cc中的注释可以知道,它们是通过寄存器来储存的。
可以看出,参数分别储存在r4到r7内。
而返回值需要放在r2寄存器中。
而我们的参数不能以直接转换成指针的形式从内存中读取,原因是这里的内存地址实际上是虚拟机的内存,而转换成指针形式的内存地址是真实机器中的地址。所以读取要用machine->ReadMem(int addr,int size,int *value)方法。而读取字符串,则需要循环至读取到\0为止。
还需要注意的是,c++中char是1个字节,int是四个字节,这里读取char要将size设置为1.
四、AdvancePC的实现
参照mipssim中的实现,我们在exception.cc中定义如下函数:
五、SpaceID的实现
现有的AddrSpace中并没有实现唯一标识。
考虑以下以下因素:标识符总数要小于最大线程数(128),且能够全局访问(例如join等操作),并且能够唯一标识。由此,我们需要在AddrSpace中添加一个属性来记录这个标识符,在初始化时对其赋值,同时,为了避免重复,我们在system.h中声明一个数组来标记当前在使用的标识符,这样全局都可以进行访问。在AddrSpace对象销毁时,将对应的标识符置空。
具体的实现为,在nachos/threads/system.h中,添加声明
extern bool ThreadMap[128];
在nachos/threads/system.cc中,将其全部置为0
bzero(ThreadMap,128);
在nachos/userprog/AddrSpace.h中
为AddrSpace类添加属性
int spaceID;
添加方法
int getSpaceID(){return spaceID;}
在 nachos/userprog/AddrSpace.cc中,在构造方法开头添加如下语句来获取并标识spaceID:
在析构方法中添加如下语句来释放空间
六、实现Exec调用
将ExceptionHandler函数中的逻辑判定改为如下形式(当然,这样只是看起来舒服一些,如果喜欢也可以使用原版else if的形式)
添加case SC_Exec的代码以处理Exec系统调用,
而StartProcess函数、space,thread定义如下(实际上,必须定义到ExceptionHandler之前的位置)
可以看出大体流程与上面的分析一致。实际上,如果读过nachos/userprog/progtest.cc的代码,会发现大体流程与progtest文件中的StartProcess函数基本一致,除了新建线程和读取参数,返回spaceID的部分。
而且,由于需要返回内存空间AddrSpace的唯一标志符,这里需要在StartProcess之外初始化AddrSpace对象,所以把StartProcess函数拆分了。
七、实现Exit系统调用
添加case SC_Exit的处理如下
简单地终结了当前线程,其中的资源会自动释放掉。
八、编写测试文件
在nachos/test下新建文件exec.c,
实现非常简单,只是调用了exit调用来退出。
修改同目录下halt.c文件
这里halt.c在执行时会调用Exec系统调用启动exec.noff
在test/MakeFile中的targets一句末尾加上exec
在nachos/test目录和nachos/userprog目录分别make编译。
九、测试
在nachos/userprog输入命令
./nachos -x ../test/halt.noff
可以看出实现了多程序并行
为了验证程序正常执行,可以使用以下命令
./nachos -d m -x ../test/halt.noff
其中-d m是为了打印汇编的调试输出。
可以看出,程序正常执行。
附代码:
void
ExceptionHandler(ExceptionType which)
{
int type = machine->ReadRegister(2);
if ((which == SyscallException)) {
switch(type){
case SC_Halt:{
DEBUG('a', "Shutdown, initiated by user program.\n");
interrupt->Halt();
break;
}
case SC_Exec:{
printf("Execute system call of Exec()\n");
//read argument
char filename[50];
int addr=machine->ReadRegister(4);
int i=0;
do{
machine->ReadMem(addr+i,1,(int *)&filename[i]);//read filename from mainMemory
}while(filename[i++]!='\0');
printf("Exec(%s):\n",filename);
//open file
OpenFile *executable = fileSystem->Open(filename);
if (executable == NULL) {
printf("Unable to open file %s\n", filename);
return;
}
//new address space
space = new AddrSpace(executable);
delete executable; // close file
//new and fork thread
thread = new Thread("forked thread");
thread->Fork(StartProcess, 1);
//run the new thread
currentThread->Yield();
//return spaceID
machine->WriteRegister(2,space->getSpaceID());
//advance PC
AdvancePC();
break;
}
case SC_Exit:{
printf("Execute system call of Exit()\n");
//machine->clear();
AdvancePC();
currentThread->Finish();
break;
}
default:{
printf("Unexpected syscall %d %d\n", which, type);
ASSERT(FALSE);
}
}
} else {
printf("Unexpected user mode exception %d %d\n", which, type);
ASSERT(FALSE);
}
}
AddrSpace::AddrSpace(OpenFile *executable)
{
bool flag = false;
for(int i = 0; i < 128; i++)
{
if(!ThreadMap[i]){
ThreadMap[i] = 1;
flag = true;
spaceID=i;
break;
}
}
ASSERT(flag);
if(bitmap == 0)
bitmap = new BitMap(NumPhysPages);
NoffHeader noffH;
unsigned int i, size;
executable->ReadAt((char *)&noffH, sizeof(noffH), 0);
if ((noffH.noffMagic != NOFFMAGIC) &&
(WordToHost(noffH.noffMagic) == NOFFMAGIC))
SwapHeader(&noffH);
ASSERT(noffH.noffMagic == NOFFMAGIC);
// how big is address space?
size = noffH.code.size + noffH.initData.size + noffH.uninitData.size
+ UserStackSize; // we need to increase the size
// to leave room for the stack
numPages = divRoundUp(size, PageSize);
size = numPages * PageSize;
ASSERT(numPages <= NumPhysPages); // 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);
// first, set up the translation
pageTable = new TranslationEntry[numPages];
for (i = 0; i < numPages; i++) {
pageTable[i].virtualPage = i; // for now, virtual page # = phys page #
pageTable[i].physicalPage = bitmap->Find();
ASSERT(pageTable[i].physicalPage!=-1);
pageTable[i].valid = TRUE;
pageTable[i].use = FALSE;
pageTable[i].dirty = FALSE;
pageTable[i].readOnly = FALSE; // if the code segment was entirely on
// a separate page, we could set its
// pages to be read-only
}
// zero out the entire address space, to zero the unitialized data segment
// and the stack segment
//bzero(machine->mainMemory, size);
// then, copy in the code and data segments into memory
if (noffH.code.size > 0) {
DEBUG('a', "Initializing code segment, at 0x%x, size %d\n",
noffH.code.virtualAddr, noffH.code.size);
//code start from this page
int code_page = noffH.code.virtualAddr/PageSize;
//calculate physical address using page and offset (0);
int code_phy_addr = pageTable[code_page].physicalPage *PageSize;
//read memory
executable->ReadAt(&(machine->mainMemory[code_phy_addr]),
noffH.code.size, noffH.code.inFileAddr);
}
if (noffH.initData.size > 0) {
DEBUG('a', "Initializing data segment, at 0x%x, size %d\n",
noffH.initData.virtualAddr, noffH.initData.size);
//data start from this page
int data_page = noffH.initData.virtualAddr/PageSize;
//first data's offset of this page
int data_offset = noffH.initData.virtualAddr%PageSize;
//calculate physical address using page and offset ;
int data_phy_addr = pageTable[data_page].physicalPage *PageSize + data_offset;
//read memory
executable->ReadAt(&(machine->mainMemory[data_phy_addr]),
noffH.initData.size, noffH.initData.inFileAddr);
}
Print();
}
//----------------------------------------------------------------------
// AddrSpace::~AddrSpace
// Dealloate an address space. Nothing for now!
//----------------------------------------------------------------------
AddrSpace::~AddrSpace()
{
ThreadMap[spaceID] = 0;
for (int i = 0; i < numPages; i++) {
bitmap->Clear(pageTable[i].physicalPage);
}
delete [] pageTable;
}