说明:blog 中写到的这几个实验,不全面而且也不是上交实验报告的最终版本(是自己实验过程中用typora简单记录的笔记),完整内容(含代码+实验报告)可以通过(山东大学软件学院操作系统课设)下载,或者微信公众号关注“陌兮blog”免费获取(未设置自动回复,看见会回复)
系统调用是用户程序和操作系统内核的接口。用户程序从系统调用函数取得系统服务。当 CPU 控制从用户程序切换到系统态时,CPU 的工作方式由用户态改变为系统态。而当内核完成系统调用功能时,CPU 工作状态又从系统态改变回用户态并且将控制再次返回给用户程序。两种不同的 CPU 工作状态提供了操作系统基本的保护方式。
通过阅读实验指导书了解到,在nachos系统中,用户编写的 C 语言程序在由 gcc MIPS 交叉编译后都在前面链接上一个由 MIPS 汇编程序 start.s 生成的叫 start.o 的目标模块。实际上 start 是用户程序真正的启动入口,由它来调用 C 程序的 main 函数。所以不要求用户编程时一定要把 main 函数作为第一个函数。这个汇编程序也为 Nachos 系统调用提供了一个汇编语言的存根(stub)。以下是汇编程序 start.s 的汇编清单:
nachos系统中,在 nachos/userprog/syscall.h 文件里面定义了一些系统调用函数
用户程序可以这个文件中定义的一系列系统调用函数,而这些函数的实现是定义在 nachos/test/start.s 中的汇编代码。
以 Exec() 为例:
这里的实现过程是先把 SC_Exec 放入 2 号寄存器,然后调用 syscall。
其实通过观察其他的系统调用的实现,发现所有的系统调用都是先把 SC_*** 放入 2 号寄存器,然后执行的 syscall。实验指导书中也讲解了这部分:
在 start.s 中的这些系统调用的接口程序代码都是一样的。即:
当一个系统调用由一个用户进程发出时,由汇编语言编写的对应于存根的程序就被执行。然后,这个存根程序会由执行一个系统调用指令而引发一个异常或自陷。模拟 MIPS 计算机的异常和自陷管理的是 Machine 类中的函数RaiseException(ExceptionType which, int badVAddr)。其中的第一个参数 which 是一个 ExceptionType 枚举类型的变量。ExceptionType 类型的定义也在 machine/machine.h 文件中。
系统调用是这些异常中的一个。MIPS 计算机的 ”SYSCALL” 指令在 Nachos 中是由 machine/mipssim.cc 中 534-536 行上的通过发系统调用异常模拟的:
注意在系统调用异常处理之后的下一条语句是一条 return 返回语句,而不是 break 语句。这一点很重要,return 语句不会使程序计数器 PC 向前推进,从而在异常处理之后同一条指令将会再次被启动。所以为了避免重复执行,我们需要手动进行 PC 的推进。
函 数 RaiseException() 的 代 码 在 machine/machine.cc 文件中,而 RaiseException 函数实际上最终调用的是nachos/userprog/exception.cc 中的函数 ExceptionHandler(ExceptionType which)。
分析一下这个函数,首先读取 2 号寄存器中的内容,然后有一个 if 判断,如果传入的异常类型是系统调用,并且从寄存器中读取到的内容是 SC_Halt ,那么就执行 Halt(),否则就是正常的异常捕获处理。
可以看出,这里仅仅能处理带有 SC__Halt 代码的系统调用,如果我们想实现 Exec(),就应该在这里加上一个判断分支,使其可以处理 SC_Exec 的系统调用。
关于参数问题,实验指导书和代码中的注释也都提到了
寄存器 4 - 7 包含着当系统调用开始处理时的前 4 个参数。系统调用的返回值,如果有,都将返回 2 号寄存器。
再次查看一下 syscall.h 中,关于 Exec 系统调用的定义
通过注释内容可以了解到,该系统调用的功能是,从可执行文件 name 运行一个新的用户程序,并行执行,并返回新的程序的内存空间标识符 SpaceId,它将作为 Join 系统调用的参数。Join 系统调用可用于返回以 SpaceId 为标识的进程结束时的退出状态。
参照实验指导书,为了能够了解 Nachos 中多用户程序驻留内存的情况,查看内存页表的分配情况以及对应关系,可以在 AssSpace 类中增加以下打印成员函数 Print:
addrspace.h 的 AssSpace 类 public 中声明
void Print();
addrspace.cc 中实现
void
AddrSpace::Print() {
printf("page table dump: %d pages in total\n", numPages);
printf("============================================\n");
printf("\tVirtPage, \tPhysPage\n");
for (int i=0; i < numPages; i++) {
printf("\t%d, \t\t%d\n", pageTable[i].virtualPage, pageTable[i].physicalPage);
}
printf("============================================\n\n");
}
在 AddrSpace 构造方法最末尾添加 Print(),便于调试。
接下来我们就需要进一步完成用户内存空间的扩充以便多用户程序同时驻留内存,进而使多用户进程并发执行。
首先看一下当前的 Nachos 有关用户内存页表的初始化功能,即 AddrSpace.cc 中的有关片段:
注意 89 行物理帧的分配总是循环变量 i 的值。因为当第二个程序进行分配时,也会从 0 开始分配,所以当两个程序同时驻留内存时后一个程序会装入到前一个程序的物理地址中,从而将先前已装入的程序覆盖。可见基本的 Nachos 并不具有多个程序同时驻留内存的功能。
接下来我们需要改进 Nachos 的内存分配算法的设计。
设计思路:利用 Nachos 在…/userprog/bitmap.h 中文件定义的 Bitmap 类。利用 bitmap 记录和申请内存物理帧,使不同的程序装入到不同的物理空间中去。
这就需要 AddrSpace 类维护一个静态的 BitMap 来标注物理内存中的页表分配情况。
首先在 addrspace.h 中,添加 #include “bitmap.h” 来导入 bitmap 类。
然后在 AddrSpace 类的定义中,在 private 块中加入声明
static BitMap *bitmap;
addrspace.cc 中,在构造方法前,创建静态对象 bitmap
BitMap* AddrSpace::bitmap = new BitMap(NumPhysPages);
这样我们就已经完成了在 AddrSpace 类中创建 BitMap 对象,用于管理空闲内存页表。
接下来仅需将原来的物理页表直接分配为 i 改为调用 bitmap 来分配即可
最后记得在析构函数中将这个空间释放,保证在内存空间删除时,能够及时地将空间释放出来。
for (int i = 0; i< numPages; i++){
bitmap->Clear(pageTable[i].physicalPage);
}
下面修改可执行文件读取到内存的逻辑
在 addrspace.cc 中将可执行文件的内容读取到内存中的代码实现如下:
很明显在这里将逻辑地址作为物理地址进行读入,这样逻辑地址和物理地址的对应关系就失去了意义。
我们应该将 code.virtualAddr 和 initData.virtualAddr 转换为与上面物理页表的对应的物理地址,也就是上学期课程所学将逻辑地址转换为物理地址。
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);
}
最后需要对初始化内存进行修改,在构造函数中注释掉 bzero(machine->mainMemory, size);
这是在创建 AddrSpace 对象时,清空整个内存的操作。但是,我们如果创建了新的 AddrSpace,就会把原有的内存全部清空。而实际上,在将新的可执行文件读入内存时,不需要清空内存也可以正常执行,所以我们简单地注释掉这一行。
第一部分的分析中已经提及到了为什么要实现 AdvancePC,因为在文件 …/machine/mipssim.cc 中系统调用模拟指令的操作是以 return 返回的,这意味着在执行完系统调用后是否令程序计数器向前推进的工作交给了对应的系统调用处理函数去决定。所以我们应实现 AdvancePC 以便当系统调用成功后向前推进程序计数器
实验指导书中已经给出了实现的示例,这里我们在 interrupt.cc 中实现 AdvancePC,因为后面会在这里实现 Exec 函数,需要调用 AdvancePC ,而且应该在其他函数前面实现,或者提前声明
void AdvancePC() {
machine->WriteRegister(PrevPCReg,machine->ReadRegister(PCReg));
machine->WriteRegister(PCReg, machine->ReadRegister(PCReg) + 4);
machine->WriteRegister(NextPCReg, machine->ReadRegister(NextPCReg) + 4);
}
有关 SpaceId 的值需要解决两个问题:
实现思路:首先是标识符总数要小于最大线程数(128),且能够全局访问(例如join等操作),并且能够唯一标识。由此,我们需要在 AddrSpace 中添加一个属性来记录这个标识符,在初始化时对其赋值,同时,为了避免重复,我们在 system.h 中声明一个数组来标记当前在使用的标识符,这样全局都可以进行访问。在 AddrSpace 对象销毁时,将对应的标识符置空。
具体的实现为,在 system.h 中,声明全局变量。system.h 文件就是用于声明全局变量的
extern bool ThreadMap[128];
在 system.cc 中将进程标识符数组进行初始化
在 system.cc 的 Initialize 方法中,将其赋值为 0;system.cc 用于 Nachos初始化和常规清理。Initialize 方法用于初始化,Cleanup 方法用于清理。
bzero(ThreadMap,128);
在 addrspace.h 中为 AddrSpace 类添加属性
int spaceID;
添加方法
int getSpaceID(){return spaceID;}
在 addrspace.cc 中,在构造方法前面,通过循环遍历,来获取并标识一个唯一的 spaceID
AddrSpace::AddrSpace(OpenFile *executable)
{
//2021.11.23 add +++++++++++++++++++++++++++++++++++++++++
bool flag = false;
//遍历一下,找到未被使用的spaceID,即ThreadMap中对应值为0
//找到以后,标记为已使用
for(int i = 0; i < 128; i++){
if(!ThreadMap[i]){
ThreadMap[i] = 1;
flag = true;
spaceID = i;
printf("spaceID:%d\n",spaceID);
break;
}
}
ASSERT(flag);
//2021.11.23 add +++++++++++++++++++++++++++++++++++++++++
最后需要在析构方法中释放空间
AddrSpace::~AddrSpace()
{
//2021.11.23 add +++++++++++++++++++++++++++++++++++++++++
ThreadMap[spaceID] = 0;
for (int i = 0; i< numPages; i++){
bitmap->Clear(pageTable[i].physicalPage);
}
//2021.11.23 add +++++++++++++++++++++++++++++++++++++++++
delete [] pageTable;
}
前面的部分已经分析过,现有的 ExceptionHandler 函数仅能判定 SC_Halt ,所以这里需要将 ExceptionHandler 函数中的逻辑判定添加 if SC_Exec 分支的代码以处理 Exec 系统调用,根据前面分析以及实验指导书中的讲解可以知道,执行系统调用时,实际是异常处理函数 Exec 。异常属于一种中断处理,因此通常把这类处理函数都封装到 Interrupt 类中,作为 Interrupt 类的成员函数,所以这里直接调用 Interrupt 类的 Exec 函数
接下来实现 Interrupt 类的 Exec 函数
实验指导书中讲解到,对于异常处理函数 Exec 的编码可以参考 …/userprog/progtest.cc 文件中 StartProcess 函数。该函数代码如下:
主要的不同处在于 StartProcess 中要打开的文件名是从命令行传递过来的,而 Exec 要打开的文件名是由$4 寄存器传递过来的。
首先在 Interrupt 类中声明 Exec 函数
然后在实现 Exec 函数之前,先实现 StartProcess 函数,在 interrupt.cc 文件的前面实现,或者先声明
Thread* thread;
AddrSpace *space;
void
StartProcess(int n){
currentThread->space = space;
currentThread->space->InitRegisters(); // set the initial register values
currentThread->space->RestoreState(); // load page table register
machine->Run(); //jump to the user progap
ASSERT(FALSE); //machine->Run never returns;
//the address space exits
//by doing the syscall "exit"
}
最后实现 Exec 函数
代码:
int
Interrupt::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 0;
}
//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();
}
在 nachos/test 下新建文件 exec.c
#include "syscall.h"
int
main(){
Exec("../test/halt.noff");
Halt();
}
首先在 test/MakeFile 中的 targets 一句末尾加上 exec
然后在 test 目录和 lab6 目录分别 make 编译。
命令行执行:./nachos -x …/test/exec.noff
这里出现了一个小问题,如果出现下面这种情况,显示 “不能打开文件 xxxx” 可能是权限不够的问题,打开 test 文件夹有该文件,但是上面显示一个小锁,所以切换到 root 用户即可
参考实验指导书以及网上相关文章完成
参考链接:https://blog.csdn.net/mottled233/article/details/78633571