概述
前言
如果你对这篇文章可感兴趣,可以点击「【访客必读 - 指引页】一文囊括主页内所有高质量博客」,查看完整博客分类与对应链接。
文章目录
- 前言
- 一、实验基础信息
- 1.1 实验信息
- 1.2 实验目的
- 1.2.1 实验六
- 1.2.2 实验七
- 1.2.3 实验八
- 1.3 实验任务
- 1.3.1 实验六
- 1.3.2 实验七
- 1.3.3 实验八
- 二、实验基本方法
- 2.1 运行 Nachos 应用程序的方法
- 2.2 Nachos 应用程序
- 2.3 页表
- 2.4 用户进程的创建与启动
- 2.4.1 StartProcess 函数
- 2.4.2 Instruction 类
- 2.4.3 Machine::ReadMem() 函数
- 2.4.4 Machine()::Translate() 函数
- 2.4.5 Machine::OneInstruction() 函数
- 2.4.6 Machine::Run() 函数
- 2.4.7 AddrSpace::RestoreState() 函数
- 2.4.8 Machine 类
- 2.4.9 Thread 类
- 2.4.10 Scheduler::Run() 函数
- 2.4.11 应用程序创建与启动过程
- 2.5 PCB
- 2.6 用户进程映射到核心线程
- 2.7 线程调度算法
- 2.8 Nachos 调试命令
- 三、源代码及注释
- 3.1 修改内容概述
- 3.2 AddrSpace
- 3.2.1 AddrSpace 类
- 3.2.2 AddrSpace::AddrSpace()
- 3.2.3 AddrSpace::~AddrSpace()
- 3.2.4 AddrSpace::Print()
- 3.2.5 AddrSpace 中寄存器保存与恢复
- 3.2.6 AddrSpace 中基于 FILESYS 实现的函数
- 3.3 Thread
- 3.3.1 Thread 类
- 3.3.2 Thread::Thread()
- 3.3.3 Thread::Finish()
- 3.3.4 Thread::Join()
- 3.3.5 Thread::Terminated()
- 3.4 Scheduler
- 3.4.1 Scheduler 类
- 3.4.2 Scheduler::Scheduler()
- 3.4.3 Scheduler::~Scheduler()
- 3.4.4 Scheduler::deleteTerminatedThread()
- 3.5 List
- 3.5.1 List 类
- 3.5.2 List::RemoveItem()
- 3.6 OpenFile
- 3.6.1 OpenFile 类
- 3.6.2 OpenFile::WriteStdout()
- 3.6.3 OpenFile::ReadStdin()
- 3.7 exception.cc
- 3.7.1 Exec()
- 3.7.2 Exit()
- 3.7.3 Join()
- 3.7.4 Yield()
- 3.7.5 FILESYS_STUB - Create()
- 3.7.6 FILESYS_STUB - Open()
- 3.7.7 FILESYS_STUB - Write()
- 3.7.8 FILESYS_STUB - Read()
- 3.7.9 FILESYS_STUB - Close()
- 3.7.10 FILESYS - Create()
- 3.7.11 FILESYS - Open()
- 3.7.12 FILESYS - Write()
- 3.7.13 FILESYS - Read()
- 3.7.14 FILESYS - Close()
- 四、实验测试方法及结果
- 4.1 Nachos 应用程序与可执行程序
- 4.1.1 ../test/Makefile
- 4.1.2 修改 ../test/halt.c 并重新编译
- 4.1.3 生成 halt.s 文件
- 4.1.4 进入目录 ../userprog
- 4.2 Nachos 可执行程序格式
- 4.3 页表的系统转储
- 4.4 应用程序进程的创建与启动
- 4.4.1 Nachos 为应用程序创建进程的过程
- 4.4.2 理解系统为用户进程分配内存空间、建立页表的过程,分析目前的处理方法存在的问题?
- 4.4.3 理解应用进程如何映射到一个核心线程
- 4.4.4 如何启动主进程(父进程)
- 4.4.5 理解当前进程的页表是如何与CPU使用的页表进行关联的
- 4.4.6 思考如何在父进程中创建子进程,实现多进程机制
- 4.4.7 思考进程退出要完成的工作有哪些?
- 4.5 分配更大的地址空间
- 4.6 创建 exec 与 bar 程序
- 4.7 修改 AddrSpace 类
- 4.8 寻找编写系统调用代码的地方
- 4.9 系统调用机制
- 4.9.1 Nachos 系统调用执行过程
- 4.9.2 Nachos 系统调用参数传递
- 4.9.3 Openfile for the User program
- 4.9.4 Advance PC
- 4.9.5 SpaceId
- 4.10 系统调用 Join() 的实现
- 4.10.1 Join() 实现思路
- 4.10.2 Join() 具体代码
- 4.10.3 Join() 实验验证
- 4.11 系统调用 Exec() 的实现
- 4.11.1 修改 exception.cc 思路
- 4.11.2 修改 progtest.cc 思路
- 4.11.3 修改 AddrSpace 思路
- 4.11.4 Exec() 系统调用小结及代码
- 4.11.5 Exec() 系统调用验证
- 4.12 系统调用 Exit() 的实现
- 4.12.1 Exit Status
- 4.12.2 Exit() 实现思路及代码
- 4.12.3 Exit() 系统调用验证
- 4.13 系统调用 Yield() 的实现
- 4.13.1 Yield() 实现思路及代码
- 4.13.2 Yield() 实现验证
- 4.14 综合测试
- 4.15 基于 FILESYS_STUB 实现文件的有关系统调用
- 4.15.1 Create()
- 4.15.2 Open()
- 4.15.3 Write()
- 4.15.4 Read()
- 4.15.5 Close()
- 4.15.6 综合测试
- 4.16 基于 FILESYS 实现文件的有关系统调用
- 4.16.1 Makefile
- 4.16.2 实现文件 Id 的分配与释放
- 4.16.3 Create()
- 4.16.4 Open()
- 4.16.5 Write()
- 4.16.6 Read()
- 4.16.7 Close()
- 4.16.8 综合测试
- 五、实验体会
- 5.1 Nachos 用户程序与系统调用
- 5.2 地址空间的扩展
- 5.3 系统调用的实现
- 5.3.1 Exec()
- 5.3.2 Join()
- 5.3.3 Exit()
- 5.3.4 Yield()
- 5.3.5 FILESYS_STUB & FILESYS
- 5.4 总结
一、实验基础信息
1.1 实验信息
日期:2019.12.4
题目:Nachos 用户程序与系统调用、地址空间的扩展、系统调用 Exec() 与 Exit()
1.2 实验目的
1.2.1 实验六
- 为后续实验中实现系统调用 Exec() 和 Exit() 奠定基础
- 理解 Nachos 可执行文件的格式与结构
- 掌握 Nachos 应用程序的编程语法,了解用户进程是如何通过系统调用与操作系统内核进行交互的
- 掌握如何利用交叉编译生成 Nachos 的可执行程序
- 理解系统如何为应用程序创建进程,并启动进程
- 理解如何将用户进程映射到核心线程,核心线程执行用户程序的原理与方法
- 理解当前进程的页表是如何与CPU使用的页表进行关联的
1.2.2 实验七
- 通过考察系统加载应用程序过程,如何为其分配内存空间、创建页表并建立虚页与实页帧的映射关系,理解 Nachos 的内存管理方法
- 理解系统如何对空闲帧进行管理
- 理解如何加载另一个应用程序并为其分配地址空间,以支持多进程机制
- 理解进程的 pid
- 理解进程退出所要完成的工作
1.2.3 实验八
- 理解用户进程是如何通过系统调用与操作系统内核进行交互的
- 理解系统调用是如何实现的
- 理解系统调用参数传递与返回数据的回传机制
- 理解核心进程如何调度执行应用程序进程
- 理解进程退出后如何释放内存等为其分配的资源
- 理解进程号 pid 的含义与使用
1.3 实验任务
1.3.1 实验六
- 阅读 …/bin/noff.h,分析 Nachos 可执行程序 .noff 文件的格式组成
- 阅读 …/test 目录下的几个 Nachos 应用程序,理解 Nachos 应用程序的编程语法,了解用户进程是如何通过系统调用与操作系统内核进行交互的
- 阅读 …/test/Makefile,掌握如何利用交叉编译生成 Nachos 的可执行程序
- 阅读 …/threads/main.cc、…/userprog/progtest.cc,根据对命令行参数 -x 的处理过程,理解系统如何为应用程序创建进程,并启动进程的
- 阅读 …/userprog/protest.cc、…/threads/scheduler.cc(Run()),理解如何将用户进程映射到核心线程,以及核心线程执行用户程序的原理与方法
- 阅读 …/userprog/progtest.cc、…/machine/translate.cc,理解当前进程的页表是如何与 CPU 使用的页表进行关联的
1.3.2 实验七
- 阅读 …/prog/protest.cc,深入理解 Nachos 创建应用程序进程的详细过程
- 阅读理解类 AddrSpace,然后对其进行修改,使 Nachos 能够支持多进程机制,允许 Nachos 同时运行多个用户进程
- 在类 AddrSpace 中添加完善的 Print() 函数(在实验六中已给出)
- 在类 AddrSpace 中实例化类 BitMap 的一个全局对象,用于管理空闲帧
- 如果将 SpaceId 直接作为进程号 Pid 是否合适?如果不合适,应该如何为进程分配相应的 Pid?
- 为了实现 Join(pid),考虑如何在该进程相关联的核心线程中保存进程号
- 根据进程创建时系统为其所做的工作,考虑进程退出时应该做哪些工作
- 考虑系统调用 Exec() 与 Exit() 的设计实现方案
1.3.3 实验八
- 阅读 …/userprog/exception.cc,理解系统调用 Halt() 的实现原理
- 基于实验 6、7 中所完成的工作,利用 Nachos 提供的文件管理、内存管理及线程管理等功能,编程实现系统调用 Exec() 与 Exit()(至少实现这两个)。
Nachos 目前仅实现了系统调用 Halt(),其实现代码参见 …/userprog/exception.cc 中的函数 void ExceptionHandler(ExceptionType which),其余的几个系统调用都没有实现。Nachos 系统调用对应的宏在 …/userprog/syscall.h 中声明如下。
二、实验基本方法
2.1 运行 Nachos 应用程序的方法
Nachos 是一个操作系统,可以运行 Nachos 应用程序。Nachos 应用程序采用类 C 语言语法,并通过 Nachos 提供的系统调用进行编写。
由于 Nachos 模拟了一个执行 MIPS 指令的 CPU,因此需要利用 Nachos 提供的交叉编译程序 gcc-2.8.1-mips.tar.gz 将用户编写的 Nachos 应用程序编译成 MIPS 框架的可执行程序。
gcc MIPS 交叉编译器将 Nachos 的应用程序编译成 COFF 格式的可执行文件,然后利用 …/test/coff2noff 将 COFF 格式的可执行程序转换成 Nachos CPU 可识别的 NOFF 可执行程序。
运行 Nachos 应用程序的流程:
- 在 …/test 目录下运行 make,将该目录下的几个现有的 Nachos 应用程序交叉编译,并转换成 Nachos 可执行的 .noff 格式文件。
- 在 …/userprog 目录下运行 make 编译生成的 Nachos 系统,键入命令 ./nachos -x …/test/halt.noff 令 Nachos 运行应用程序 halt.noff,参数 -x 的作用是使 Nachos 运行相应应用程序。
2.2 Nachos 应用程序
观察 …/test/Makefile 文件可知,Nachos 利用了交叉编译器提供的 gcc、as、ld 工具用于编译。
编译的目标文件是 .noff 文件和 .flat 文件,编译过程是先通过 .c 文件编译出 .o 文件,再通过 .o 文件编译出 .coff 文件,再通过 .coff 文件编译出 .noff 文件与 .flat 文件。
其中 .noff 文件头结构如下所示。
// 下述内容为 Nachos 文件的结构。在 Nachos 中,只有三种类型的段文件,只读代码、已初始化数据、未初始化数据
#define NOFFMAGIC 0xbadfad // Nachos 文件的魔数
typedef struct segment {
int virtualAddr; // 段在虚拟地址空间中的位置
int inFileAddr; // 段在文件中的位置
int size; // 段大小
} Segment;
typedef struct noffHeader {
int noffMagic; // noff 文件的魔数
Segment code; // 可执行代码段
Segment initData; // 已初始化数据段
Segment uninitData; // 未初始化数据段, 文件使用之前此数据段应为空
} NoffHeader;
我们可以继续观察 Addrspace 类的构造函数,观察 Nachos 是如何创建一个地址空间用于运行用户程序,以及如何打开 noff 文件进行文件空间计算。
class AddrSpace {
private:
TranslationEntry *pageTable; // 线性页表(虚拟页-物理页)
unsigned int numPages; // 应用程序页数
};
AddrSpace::AddrSpace(OpenFile *executable) {
// 可执行文件中包含了目标代码文件
NoffHeader noffH; // noff文件头
unsigned int i, size;
executable->ReadAt((char *)&noffH, sizeof(noffH), 0); // 读出noff文件
if ((noffH.noffMagic != NOFFMAGIC) && (WordToHost(noffH.noffMagic) == NOFFMAGIC))
SwapHeader(&noffH); // 检查noff文件是否正确
ASSERT(noffH.noffMagic == NOFFMAGIC);
// 确定地址空间大小,其中还包括了用户栈大小
size = noffH.code.size + noffH.initData.size + noffH.uninitData.size + UserStackSize;
numPages = divRoundUp(size, PageSize); // 确定页数
size = numPages * PageSize; // 计算真实占用大小
ASSERT(numPages <= NumPhysPages); // 确认运行文件大小可以运行
// 第一步,创建页表,并对每一页赋初值
pageTable = new TranslationEntry[numPages];
for (i = 0; i < numPages; i++) {
pageTable[i].virtualPage = i; // 虚拟页
pageTable[i].physicalPage = i; // 虚拟页与物理页一一对应
pageTable[i].valid = TRUE;
pageTable[i].use = FALSE;
pageTable[i].dirty = FALSE;
pageTable[i].readOnly = FALSE; // 只读选项
}
// 第二步,清空文件数据
bzero(machine->mainMemory, size);
// 第三步,将noff文件数据拷贝到物理内存中
if (noffH.code.size > 0) {
executable->ReadAt(&(machine->mainMemory[noffH.code.virtualAddr]),
noffH.code.size, noffH.code.inFileAddr); // ReadAt调用了bcopy函数
}
if (noffH.initData.size > 0) {
executable->ReadAt(&(machine->mainMemory[noffH.initData.virtualAddr]),
noffH.initData.size, noffH.initData.inFileAddr);
}
}
2.3 页表
在 Nachos 中,页表实现了虚页与实页的对应关系,系统根据页表实现存储保护,页面置换算法根据页表信息进行页面置换。
我们查看 …/machine/translate.h 文件得到如下页表项结构。
class TranslationEntry {
public:
int virtualPage; // 虚拟内存中的页编号
int physicalPage; // 物理内存中的页编号(相对于主存位置)
bool valid; // valid = 1, 该转换有效(已经被初始化)
bool readOnly; // readOnly = 1, 该页内容不允许修改
bool use; // use = 1, 该页被引用或修改(变量由硬件修改)
bool dirty; // dirty = 1, 该页被修改(变量由硬件修改)
};
上述的这个数据结构用于管理 “虚页->实页” 的转换,用于管理存储用户程序的物理内存。而且该数据结构在 Nachos 有两个用途,一个是作为页表项使用,另一个是用作 TLB 项。
2.4 用户进程的创建与启动
2.4.1 StartProcess 函数
查看 …/threads/main.cc 文件可以发现 Nachos 的参数 -x 调用了 …/userprog/progtest.cc 中的 StartProcess(char *filename); 函数。
具体函数内容如下。由于下述文件中出现了打开文件的操作,因此我们查看 …/userprog/Makefile.local 文件可以发现在用户程序中的宏定义为 FILESYS_STUB,即并非使用实验四、五中的文件系统对 DISK 上的文件进行操作,而是直接对 UNIX 文件进行操作。
void StartProcess(char *filename) { // 传入文件名
OpenFile *executable = fileSystem->Open(filename); // 打开文件
AddrSpace *space; // 定义地址空间
if (executable == NULL) {
printf("Unable to open file %sn", filename); // 无法打开文件
return;
}
space = new AddrSpace(executable); // 初始化地址空间
currentThread->space = space; // 将用户进程映射到一个核心线程
delete executable; // 关闭文件
space->InitRegisters(); // 设置Machine的寄存器初值
space->RestoreState(); // 将应用程序页表加载到了Machine中
machine->Run(); // machine->Run()代码中有死循环,不会返回
ASSERT(FALSE);
}
#define StackReg 29 // 用户栈指针
#define PCReg 34 // 当前存储PC值的寄存器
#define NextPCReg 35 // 存储下一个PC值的寄存器
#define PrevPCReg 36 // 存储上一次PC值的寄存器
void AddrSpace::InitRegisters() {
for (int i = 0; i < NumTotalRegs; i++) // 每个寄存器初值置0
machine->WriteRegister(i, 0);
machine->WriteRegister(PCReg, 0); // PC寄存器初值为0
machine->WriteRegister(NextPCReg, 4); // 下一个PC值为4
// 栈指针赋初值, 减去一个数值避免越界
machine->WriteRegister(StackReg, numPages * PageSize - 16);
}
void AddrSpace::RestoreState() {
machine->pageTable = pageTable; // 将应用程序页表赋给Machine
machine->pageTableSize = numPages;
}
通过上述代码,我们可以发现系统要运行一个应用程序,需要为该程序创建一个用户进程,为程序分配内存空间,将用户程序数据装入所分配的内存空间,并创建相应的页表,建立虚页与实页的映射关系。然后将用户进程映射到一个核心线程。
为了使核心线程能够执行用户进程指令,Nachos 根据用户进程的页表读取用户进程指令,并将用户页表传递给了核心线程的地址变换机构。
2.4.2 Instruction 类
Instruction 类封装了一条 Nachos 机器指令,具体信息如下。
class Instruction {
public:
void Decode(); // 解码二进制表示的指令
unsigned int value; // 指令的二进制表达形式
char opCode; // 指令类型
char rs, rt, rd; // 指令的 3 个寄存器
int extra; // 立即数(带符号) 或 目标 或 偏移量
};
2.4.3 Machine::ReadMem() 函数
继续查看 Machine 类,可以看到将虚拟地址数据读取到实际地址的函数,Machine::ReadMem();,如下所示。
// 此函数将虚拟内存addr处的size字节的数据读取到value所指的物理内存中,读取错误则返回false。
bool Machine::ReadMem(int addr, int size, int *value) { // 虚拟地址、读取字节数、物理地址
int data, physicalAddress;
ExceptionType exception; // 异常类型
// 进行虚实地址转换
exception = Translate(addr, &physicalAddress, size, FALSE);
if (exception != NoException) {
machine->RaiseException(exception, addr); // 抛出异常, 返回false
return FALSE;
}
switch (size) { // 对字节大小进行分类处理
case 1: // 读取一个字节, 放入value所指地址
data = machine->mainMemory[physicalAddress];
*value = data;
break;
case 2: // 读取两个字节,即一个short类型
data = *(unsigned short *) &machine->mainMemory[physicalAddress];
*value = ShortToHost(data); // 短字转为主机格式
break;
case 4: // 读取四个字节,即一个int类型
data = *(unsigned int *) &machine->mainMemory[physicalAddress];
*value = WordToHost(data); // 字转为主机格式
break;
default: ASSERT(FALSE);
}
return (TRUE); // 读取正确
}
2.4.4 Machine()::Translate() 函数
可以看到在 ReadMem() 函数中调用了 Translate() 函数进行了虚实地址转换,因此我们继续查看 Machine::Translate() 函数,如下所示。
/* 该函数主要功能是使用页表或TLB将虚拟地址转换为物理地址,并检查地址是否对齐以及其它错误。
如果没有错误,则在页表项中设置use、dirty位初值,并将转换后的物理地址保存在physAddr变量中。
virAddr - 虚拟地址,physAddr - 存储转换结果、size - 写或读的字节数
writing - 可写标记,需要检查TLB中的"read-only"变量。 */
ExceptionType Machine::Translate(int virtAddr, int* physAddr, int size, bool writing) {
int i;
unsigned int vpn, offset, pageFrame;
TranslationEntry *entry; // 页表项
// 检查对齐错误,即如果size = 4,则地址为4的倍数,即低两位为0;
// 若size = 2,则地址为2的倍数,即最低位为0
if (((size == 4) && (virtAddr & 0x3)) || ((size == 2) && (virtAddr & 0x1)))
return AddressErrorException;
// TLB与页表必须有一个为空,有一个不为空
ASSERT(tlb == NULL || pageTable == NULL); // tlb、pageTable均定义在Machine类中
ASSERT(tlb != NULL || pageTable != NULL) // 通过虚拟地址计算虚拟页编号以及页内偏移量
vpn = (unsigned) virtAddr / PageSize; // 虚拟页编号
offset = (unsigned) virtAddr % PageSize; // 页内偏移量
if (tlb == NULL) { // TLB为空,则使用页表
if (vpn >= pageTableSize) // vpn大于页表大小, 即返回地址错误
return AddressErrorException;
else if (!pageTable[vpn].valid) // vpn所在页不可用,即返回页错误
return PageFaultException;
entry = &pageTable[vpn]; // 获得页表中该虚拟地址对应页表项
} else { // TLB不为空,则使用TLB
for (entry = NULL, i = 0; i < TLBSize; i++) // 遍历TLB搜索
if (tlb[i].valid && ((unsigned int)tlb[i].virtualPage == vpn)) {
entry = &tlb[i]; // 找到虚拟地址所在页表项!
break;
}
// 在TLB中没有找到,返回页错误
if (entry == NULL) return PageFaultException;
}
// 想要向只读页写数据,返回只读错误
if (entry->readOnly && writing) return ReadOnlyException;
// 由页表项可得到物理页框号
pageFrame = entry->physicalPage;
// 物理页框号过大,返回越界错误
if (pageFrame >= NumPhysPages) return BusErrorException;
// 设置该页表项正在使用
entry->use = TRUE;
// 设置该页表项被修改了,即dirty位为true
if (writing) entry->dirty = TRUE;
// 得到物理地址
*physAddr = pageFrame * PageSize + offset;
// 物理地址不可越界
ASSERT((*physAddr >= 0) && ((*physAddr + size) <= MemorySize));
// 返回没有错误
return NoException;
}
2.4.5 Machine::OneInstruction() 函数
Nachos 将虚拟地址转化为物理地址后,从物理地址取出指令放入 Machine::OneInstruction(Instruction *instr) 函数进行执行,该函数具体代码如下所示。
#define PCReg 34 // 当前存储PC值的寄存器
#define NextPCReg 35 // 存储下一个PC值的寄存器
#define PrevPCReg 36 // 存储上一次PC值的寄存器
// 执行一条用户态的指令。如果执行指令过程中有异常或中断发生,则调出异常处理装置,待其处理完成后再继续运行。
void Machine::OneInstruction(Instruction *instr) {
int raw, nextLoadReg = 0, nextLoadValue = 0; // nextLoadValue记录延迟的载入操作,用于之后执行
// 读取指令数据到raw中
if (!machine->ReadMem(registers[PCReg], 4, &raw)) return; // 发生异常
instr->value = raw; // 指令数据赋值
instr->Decode(); // 指令解码
int pcAfter = registers[NextPCReg] + 4; // 计算下下个PC指令地址
int sum, diff, tmp, value;
unsigned int rs, rt, imm;
// 59条指令分类执行
switch (instr->opCode) {
case: // 59个case
...
default:
ASSERT(FALSE);
}
// 执行被延迟的载入操作
DelayedLoad(nextLoadReg, nextLoadValue);
// 增加程序计数器(PC)
registers[PrevPCReg] = registers[PCReg]; // 记录上一个PC值,用于之后调试
registers[PCReg] = registers[NextPCReg]; // 将下一个PC值赋给NOW_PC寄存器
registers[NextPCReg] = pcAfter; // 将下下个PC值赋给NEXT_PC寄存器
}
2.4.6 Machine::Run() 函数
Nachos 中调用了 Machine::Run() 函数循环调用上述 Machine::OneInstruction(Instruction *instr) 函数执行程序指令,具体函数代码如下所示。
// 模拟用户程序的执行,该函数不会返回
void Machine::Run() {
Instruction *instr = new Instruction; // 用于存储解码后的指令
interrupt->setStatus(UserMode); // 将中断状态设为用户模式
for (;;) {
OneInstruction(instr); // 执行指令
interrupt->OneTick(); // 用户模式下执行一条指令,时钟数为1
// 单步调试
if (singleStep && (runUntilTime <= stats->totalTicks)) Debugger();
}
}
2.4.7 AddrSpace::RestoreState() 函数
由上述执行过程中调用的函数代码可知,我们需要将用户进程的页表传递给 Machine 类维护的页表,才能执行用户程序指令。该过程由函数 AddrSpace::RestoreState() 实现,将用户进程的页表传递给 Machine 类,而用户进程的页表再为用户进程分配地址空间时就创建了。AddrSpace::RestoreState() 函数如下所示。
// 通过一次上下文切换,保存machine的状态使得地址空间得以运行
void AddrSpace::RestoreState() {
machine->pageTable = pageTable; // 页表项
machine->pageTableSize = numPages; // 页表大小
}
为了便于上下文切换时保存与恢复寄存器状态,Nachos 设置了两组寄存器,一组是 CPU 使用的寄存器 int registers[NumTotalRegs]
,用于保存执行完一条机器指令时该指令的执行状态;另一组是运行用户程序时使用的用户寄存器 int userRegisters[NumTotalRegs]
,用户保存执行完一条用户程序指令后的寄存器状态。
2.4.8 Machine 类
接下来我们通过查看 Machine 类来了解 CPU 使用的寄存器的定义,具体定义代码如下。
/*
模拟主机工作硬件,包括CPU寄存器、主存等。
用户程序无法分辨他们运行在模拟器上还是真实的硬件上,除非他们发现了该模拟器不支持浮点运算。
模拟器有10条系统调用,但UNIX有200条系统调用。
*/
class Machine {
public:
Machine(bool debug); // 模拟硬件的构造函数,用于运行用户程序
~Machine(); // 析构函数
// 运行用户程序的函数
void Run(); // 运行用户程序
int ReadRegister(int num); // 读取CPU寄存器中的内容
void WriteRegister(int num, int value); // 保存value到num编号的CPU寄存器中
// 模拟硬件的实现过程
void OneInstruction(Instruction *instr); // 执行一条用户程序指令
void DelayedLoad(int nextReg, int nextVal); // 延迟加载
bool ReadMem(int addr, int size, int* value); // 读取虚拟地址处的size个字节到value所指物理地址处
bool WriteMem(int addr, int size, int value); // 将size个字节的value数据写入addr的虚拟地址处
// 将虚拟地址转换为物理地址,并检查是否有异常
ExceptionType Translate(int virtAddr, int* physAddr, int size,bool writing);
// 抛出异常,陷入系统态
void RaiseException(ExceptionType which, int badVAddr);
void Debugger(); // 调出用户程序调试器
void DumpState(); // 打印出用户CPU和主存状态
// 模拟硬件的数据结构
char *mainMemory; // 物理内存,用于存储用户程序、代码与数据
int registers[NumTotalRegs]; // CPU寄存器,用于保存执行完机器指令时该指令执行状态
// 虚、实地址转换(mainMemory首地址为0号地址)
TranslationEntry *tlb; // 快表,存在唯一,因此指针不可修改,类似于只读指针
TranslationEntry *pageTable; // 传统线性页表,可存在多个。
unsigned int pageTableSize; // 页表大小
private:
bool singleStep; // 单步调试开关,即每次用户指令执行结束是否进入调试器
int runUntilTime; // 当运行时间到达该值时,进入调试器
};
2.4.9 Thread 类
我们继续查看 Thread.h,来查看运行用户程序时使用的用户寄存器。
/*
该类定义了线程控制块。
每个线程都拥有(1)线程执行栈(2)数组存储CPU寄存器状态(3)线程状态(运行、可运行、阻塞)
用户进程拥有用户地址空间,仅运行在内核态的线程没有地址空间
*/
class Thread {
private:
// 下述两个变量用于上下文切换,位置不可更改
int* stackTop; // 当前栈指针
_int machineState[MachineStateSize]; // 所有CPU寄存器状态
public:
Thread(char* debugName); // 构造函数
~Thread(); // 析构函数,运行态线程不可析构
// 基础线程操作
void Fork(VoidFunctionPtr func, _int arg); // 使线程运行在 (*func)(arg) 函数位置
void Yield(); // 当前线程,运行态 => 可运行态
void Sleep(); // 当前线程,运行态 => 阻塞态
void Finish(); // 线程运行结束
void CheckOverflow(); // 检查线程栈是否溢出
void setStatus(ThreadStatus st) { status = st; } // 设置线程状态
char* getName() { return (name); } // 获取线程名
void Print() { printf("%s, ", name); } // 输出线程名
private:
int* stack; // 栈底指针,主线程栈底指针为NULL
ThreadStatus status; // 线程状态(运行、可运行、阻塞)
char* name; // 线程名
void StackAllocate(VoidFunctionPtr func, _int arg); // 为线程分配栈空间,用于Fork函数内部实现
#ifdef USER_PROGRAM // 如果为用户程序
// 运行用户程序的线程有两组CPU寄存器,一个存储运行用户代码的线程状态,一个存储运行内核代码的线程状态
int userRegisters[NumTotalRegs]; // 用户态下CPU寄存器状态
public:
void SaveUserState(); // 保存用户态下寄存器状态
void RestoreUserState(); // 恢复用户态下寄存器状态
AddrSpace *space; // 运行用户程序时的地址空间
#endif
};
由于 CPU 只有一个,因此 CPU 寄存器也只有一组。但每个用户程序至少需要映射到一个核心线程,因此每个核心线程都可能执行用户程序,所以每个核心线程都需要维护一组用户寄存器 userRegisters[],用于保存与恢复相应的用户程序指令的执行状态。
2.4.10 Scheduler::Run() 函数
当用户进程进行上下文切换时,即执行用户进程的核心线程发生了上下文切换时,Nachos 就会将老进程的 CPU 寄存器状态保存到用户寄存器 userRegisters[] 中,并将新用户进程的寄存器状态恢复到 CPU 寄存器中,使得 CPU 能够继续执行上次被中断的用户程序。
在 Scheduler::Run() 中,我们可以看到核心进程切换时对 CPU 寄存器与用户寄存器的保存与恢复,具体代码如下所示。
// 给CPU分配下一个线程,即进行上下文切换,需要保存旧线程状态并加载新线程状态。
void Scheduler::Run (Thread *nextThread) {
Thread *oldThread = currentThread; // 旧线程
#ifdef USER_PROGRAM // 运行用户程序
if (currentThread->space != NULL) {
currentThread->SaveUserState(); // 保存用户态下寄存器状态
currentThread->space->SaveState(); // 保存地址空间状态
}
#endif
oldThread->CheckOverflow(); // 检查旧线程是否有栈溢出
currentThread = nextThread; // 当前线程切换到下一个线程
currentThread->setStatus(RUNNING); // 设置当前线程为运行态
SWITCH(oldThread, nextThread); // 新旧线程上下文切换
// 一个线程运行结束时不可以直接删除,因为当前仍然运行在其栈空间中
if (threadToBeDestroyed != NULL) { // 如果之前设置了需要被删除的线程
delete threadToBeDestroyed; // 该变量在 Finish() 函数中设置
threadToBeDestroyed = NULL;
}
#ifdef USER_PROGRAM
if (currentThread->space != NULL) { // 如果新线程运行用户程序
currentThread->RestoreUserState(); // 恢复运行用户程序时CPU寄存器状态
currentThread->space->RestoreState();// 恢复运行用户程序时地址空间
}
#endif
}
2.4.11 应用程序创建与启动过程
回顾一下系统为应用程序创建进程与启动进程的过程。
整个过程一共分为四步,具体过程如下所示。
- 第一步
第一步从文件系统中获得OpenFile。
- 第二步
第二步是通过OpenFile分配地址空间。在这一步中,Nachos 为应用程序分配内存空间并将其装入所分配的内存空间中,然后建立页表,并建立虚页与实页的映射关系。
在此时,space就是该用户进程的标识,在 Nachos 中并没有像 UNIX 一样为每个进程分配一个进程号(pid)。因此此处可以进行扩展,为每个进程分配一个 pid,并建立 pid 与 space 的映射关系,且之后通过 pid 来标识该进程。
在该步之后,代码 currentThread->space = space
,即将该进程映射到一个核心进程上。因此第一个用户进程即映射到核心的主线程 “main” 上。
每个线程维护一个私有变量 AddrSpace *space
,当该线程与一个应用进程捆绑后,space 就会指向系统为该进程所分配的内存空间,以便被调度时执行该进程所对应的应用程序。
AddrSpace 的初始化过程也分为下述三步。
其中有几个注意点,用户程序页表初始化时, i i i 号虚拟页直接对应于 Machine 的 i i i 号物理页,也因为这个地方导致 Nachos 无法执行多个用户程序,此处可以进行修改。
其次在之后 Machine 执行用户程序指令时,都是通过将 PC 寄存器中的虚拟地址转换成物理地址之后,然后再从其物理内存的对应位置中取出指令执行,而程序指令加载到物理内存的过程就发生在 AddrSpace 初始化的时候。
- 第三步
第三步首先执行 space->InitRegisters()
来初始化 CPU 的寄存器,包括数据寄存器、PC以及栈指针等。其中由于 Nachos 将应用程序的可执行文件转变为 .noff 格式时,将程序的入口地址设置为 0,因此应用进程从虚地址 0 开始执行,因此 PC = 0。
系统为应用程序栈分配了 1KB 的空间,将栈顶指针初始化为应用程序空间的尾部(栈向上生长),为防止发生偶然访问到程序地址空间外部,将栈指针从底部向上偏移了 16 字节。
然后执行 space->RestoreState()
来将用户进程的页表传递给系统核心(Machine 类),以便 CPU 能从用户进程的地址空间中读取应用程序指令。
- 第四步
CPU的寄存器状态被设置后,就开始用户进程的执行。Machine::Run() 函数从程序入口开始,完成取指令、译码、执行的过程,直到进程遇到 Exit() 语句或异常才退出。
与常见OS不同的时,在Nachos中,一旦创建了一个用户进程,该用户进程就会立刻执行。而常见OS中,创建进程后应当将进程放入就绪队列,等待进程的调度。且当前 Nachos 不支持多线程运行。
在用户程序的执行过程中,如果与用户进程所关联的核心线程发生了上下文切换,则用户进程也会发生上下文切换。
2.5 PCB
核心线程不需要单独分配内存,利用 Thread::Fork() 创建的线程,只需调用 Thread::StackAllocate() 为其分配栈空间,在几个 CPU 寄存器中设置线程入口 Thread::Root(),线程的执行体,以及线程知悉结束时需要做的工作(线程结束时调用 Thread::Finish())。
Nachos 中没有显式定义 PCB,而是将进程信息分散到相应的类对象中;例如利用所分配内存空间的对象指针表示一个进程,该对象中含有进程的页表、栈指针以及与核心线程的映射等信息。进程的上下文保存在核心线程中,当一个线程被调度执行后,依据线程所保存的进程上下文中执行所对应的用户进程,仍然有较大的改进空间。
2.6 用户进程映射到核心线程
在实现系统调用 Fork() 之前,Nachos 不支持用户多线程机制,因此目前的 Nachos 系统就只是简单的一对一映射关系,仍然具有很大的提升空间。
2.7 线程调度算法
目前 Nachos 默认的线程调度算法是 FCFS,如果在运行加上参数 -rs seed,也可以实现时间片调度算法。因此在当前的 Nachos 中,并不支持优先级调度算法,这也是之后可以进行优化和完善的一个方向。
2.8 Nachos 调试命令
./nachos -d m -x ../test/halt.noff // 输出每一步执行的机器指令
./nachos -d m -s -x ../test/halt.noff // 单步调试,每执行一条机器指令即暂停并输出所有寄存器状态
// 生成汇编代码
/usr/local/mips/bin/decstation-ultrix-gcc -I ../userprog -I ../threads -S halt.c
make exec.s
三、源代码及注释
3.1 修改内容概述
实验六是读懂代码,实验七是扩展 AddrSpace 类,实验八是实现各系统调用并且继续修改了 AddrSpace、Thread、Scheduler、List、OpenFile、BitMap、FileHeader、FileSystem 类以及 exception.cc 文件,接下来依次列出各文件的修改内容。
此处需要注意 BitMap、FileHeader、FileSystem 的修改均为实验五中修改的内容,因此下面代码中不再重复列出。
3.2 AddrSpace
3.2.1 AddrSpace 类
在 AddrSpace 类中添加 spaceId,用于标识一个地址空间;userMap 用于分配物理页表;pidMap 用于分配 spaceId;Print() 用于输出该地址空间的页表。
FILESYS 部分的内容用于实现基于 FILESYS 的文件系统调用,主要是分配和释放文件 Id。
class AddrSpace {
public:
AddrSpace(OpenFile *executable); // 创建地址空间
~AddrSpace(); // 析构函数
void InitRegisters(); // 初始化CPU寄存器
void SaveState(); // 保存、储存地址空间
void RestoreState(); // 恢复地址空间
void Print(); // 打印页表
unsigned int getSpaceId() { return spaceId; }
#ifdef FILESYS
OpenFile *fileDescriptor[UserProgramNum]; // 文件描述符,0、1、2分别为stdin、stdout、stderr
int getFileDescriptor(OpenFile *openfile);
OpenFile *getFileId(int fd);
void releaseFileDescriptor(int fd);
#endif
private:
static BitMap *userMap, *pidMap;// 全局位图
TranslationEntry *pageTable; // 线性页表
unsigned int numPages, spaceId; // 页表中的页表项以及地址编号
};
3.2.2 AddrSpace::AddrSpace()
对于新添加的两个位图以及 FILESYS 部分内容在构造函数中进行初值赋予。
#define MAX_USERPROCESSES 256
BitMap *AddrSpace::userMap = new BitMap(NumPhysPages);
BitMap *AddrSpace::pidMap = new BitMap(MAX_USERPROCESSES);
AddrSpace::AddrSpace(OpenFile *executable) {
ASSERT(pidMap->NumClear() >= 1); // 保证还有线程号可以分配
spaceId = pidMap->Find()+100; // 0-99留给内核线程
// 可执行文件中包含了目标代码文件
NoffHeader noffH; // noff文件头
unsigned int i, size;
executable->ReadAt((char *)&noffH, sizeof(noffH), 0); // 读出noff文件
if ((noffH.noffMagic != NOFFMAGIC) && (WordToHost(noffH.noffMagic) == NOFFMAGIC))
SwapHeader(&noffH); // 检查noff文件是否正确
ASSERT(noffH.noffMagic == NOFFMAGIC);
// 确定地址空间大小,其中还包括了用户栈大小
size = noffH.code.size + noffH.initData.size + noffH.uninitData.size + UserStackSize;
numPages = divRoundUp(size, PageSize); // 确定页数
size = numPages * PageSize; // 计算真实占用大小
ASSERT(numPages <= NumPhysPages); // 确认运行文件大小可以运行
DEBUG('a', "Initializing address space, num pages %d, size %dn", numPages, size);
// 第一步,创建页表,并对每一页赋初值
pageTable = new TranslationEntry[numPages];
ASSERT(userMap->NumClear() >= numPages); // 确认页面足够分配
for (i = 0; i < numPages; i++) {
pageTable[i].virtualPage = i; // 虚拟页
pageTable[i].physicalPage = userMap->Find(); // 在位图找空闲页进行分配
pageTable[i].valid = TRUE;
pageTable[i].use = FALSE;
pageTable[i].dirty = FALSE;
pageTable[i].readOnly = FALSE; // 只读选项
}
// 第二步,将noff文件数据拷贝到物理内存中
if (noffH.code.size > 0) {
int pagePos = pageTable[noffH.code.virtualAddr/PageSize].physicalPage * PageSize;
int offSet = noffH.code.virtualAddr % PageSize;
executable->ReadAt(&(machine->mainMemory[pagePos+offSet]),
noffH.code.size, noffH.code.inFileAddr); // ReadAt调用了bcopy函数
}
if (noffH.initData.size > 0) {
int pagePos = pageTable[noffH.initData.virtualAddr/PageSize].physicalPage * PageSize;
int offSet = noffH.initData.virtualAddr % PageSize;
executable->ReadAt(&(machine->mainMemory[pagePos+offSet]),
noffH.initData.size, noffH.initData.inFileAddr);
}
#ifdef FILESYS
for(int i = 3; i < 10; i++) fileDescriptor[i] = NULL;
OpenFile *StdinFile = new OpenFile("stdin");
OpenFile *StdoutFile = new OpenFile("stdout");
OpenFile *StderrFile = new OpenFile("stderr");
/* 输出、输入、错误 */
fileDescriptor[0] = StdinFile;
fileDescriptor[1] = StdoutFile;
fileDescriptor[2] = StderrFile;
#endif
}
3.2.3 AddrSpace::~AddrSpace()
该函数需要将地址空间所分配到的物理空间、spaceId 等释放。
AddrSpace::~AddrSpace() {
pidMap->Clear(spaceId-100);
for(int i = 0; i < numPages; i++)
userMap->Clear(pageTable[i].physicalPage);
delete [] pageTable;
}
3.2.4 AddrSpace::Print()
该函数用于打印地址空间分配的页表。
void AddrSpace::Print() {
printf("page table dump: %d pages in totaln",numPages);
printf("============================================n");
printf("tVirtPage, tPhysPagen");
for(int i = 0; i < numPages; i++)
printf("t%d,tt%dn",pageTable[i].virtualPage,pageTable[i].physicalPage);
printf("============================================nn");
}
3.2.5 AddrSpace 中寄存器保存与恢复
下述两部分代码用于 CPU 寄存器的保存与恢复。
void AddrSpace::SaveState() {
pageTable = machine->pageTable;
numPages = machine->pageTableSize;
}
void AddrSpace::RestoreState() {
machine->pageTable = pageTable;
machine->pageTableSize = numPages;
}
3.2.6 AddrSpace 中基于 FILESYS 实现的函数
#ifdef FILESYS
int AddrSpace::getFileDescriptor(OpenFile *openfile) {
for(int i = 3; i < 10; i++)
if(fileDescriptor[i] == NULL){
fileDescriptor[i] = openfile;
return i;
}
return -1;
}
OpenFile* AddrSpace::getFileId(int fd) {
ASSERT((fd >= 0) && (fd < UserProgramNum));
return fileDescriptor[fd];
}
void AddrSpace::releaseFileDescriptor(int fd) {
ASSERT((fd >= 0) && (fd < UserProgramNum));
fileDescriptor[fd] = NULL;
}
#endif
3.3 Thread
3.3.1 Thread 类
在 Thread 类中,首先增加了 TERMINATED 进程状态,并定义了 waitProcessSpaceId、 waitProcessExitCode、exitCode 等变量用于 Join() 系统调用的实现,以及一系列函数关于这三个变量的访问与设定。
// 线程状态
enum ThreadStatus { JUST_CREATED, RUNNING, READY, BLOCKED, TERMINATED };
class Thread {
private:
// 下述两个变量用于上下文切换,位置不可更改
int* stackTop; // 当前栈指针
_int machineState[MachineStateSize]; // 所有CPU寄存器状态
int* stack; // 栈底指针,主线程栈底指针为NULL
char *name;
ThreadStatus status; // 线程状态(运行、可运行、阻塞)
// 为线程分配栈空间,用于Fork函数内部实现
void StackAllocate(VoidFunctionPtr func, _int arg);
#ifdef USER_PROGRAM
// 运行用户程序的线程有两组CPU寄存器,一个存储运行用户代码的线程状态,一个存储运行内核代码的线程状态
int userRegisters[NumTotalRegs]; // 用户态下CPU寄存器状态
int waitProcessSpaceId, waitProcessExitCode, exitCode;
#endif
public:
AddrSpace *space; // 运行用户程序时的地址空间
Thread(char* debugName); // 构造函数
~Thread(); // 析构函数,运行态线程不可析构
// 下述为基础线程操作
void Fork(VoidFunctionPtr func, _int arg); // 使线程运行在 (*func)(arg) 函数位置
void Yield(); // 当前线程,运行态 => 可运行态,调度其它线程
void Sleep(); // 当前线程,运行态 => 阻塞态,调度其它线程
void Finish(); // 线程运行结束
void CheckOverflow(); // 检查线程栈是否溢出
void setStatus(ThreadStatus st) { status = st; } // 设置线程状态
char* getName() { return (name); } // 获取线程名
void Print() { printf("%sn", name); } // 输出线程名
#ifdef USER_PROGRAM
void SaveUserState(); // 保存用户态下寄存器状态
void RestoreUserState(); // 恢复用户态下寄存器状态
void Join(int SpaceId);
void Terminated();
int userProgramId() { return space->getSpaceId(); }
int ExitCode() { return exitCode; }
int waitExitCode() { return waitProcessExitCode; }
int setWaitExitCode(int tmpCode) { waitProcessExitCode = tmpCode; }
int setExitCode(int tmpCode) { exitCode = tmpCode; }
#endif
};
3.3.2 Thread::Thread()
针对 Thread 类成员的增加,构造函数也需要作出一定的修改。
Thread::Thread(char* threadName) {
name = new char[50];
strcpy(name,threadName);
stackTop = NULL;
stack = NULL;
status = JUST_CREATED;
#ifdef USER_PROGRAM
space = NULL;
#endif
}
3.3.3 Thread::Finish()
该函数用于结束一个进程,并将其对应的 Joiner 从等待队列中唤醒。
void Thread::Finish () {
(void) interrupt->SetLevel(IntOff);
ASSERT(this == currentThread);
#ifdef USER_PROGRAM
// 运行结束, 执行Exit()命令时已获取退出码
// Joinee 运行结束, 唤醒 Joiner
List *waitingList = scheduler->getWaitingList();
// 检查 Joiner 是否在等待队列中
ListElement *first = waitingList->listFirst(); // 队列首
while(first != NULL){
Thread *thread = (Thread *)first->item; // 强转成Thread指针
if(thread->waitProcessSpaceId == userProgramId()){ // 在队列中
// printf("yesn");
// 将子线程退出码赋给父进程的等待退出码
thread->setWaitExitCode(exitCode);
scheduler->ReadyToRun((Thread *)thread);
waitingList->RemoveItem(first);
break;
}
first = first->next;
}
Terminated();
#else
DEBUG('t', "Finishing thread "%s"n", getName());
threadToBeDestroyed = currentThread;
Sleep();
#endif
}
3.3.4 Thread::Join()
该函数也属于 Join() 系统调用实现的一部分。
void Thread::Join(int SpaceId) {
IntStatus oldLevel = interrupt->SetLevel(IntOff); // 关中断
waitProcessSpaceId = SpaceId; // 设置当前线程所等待进程的spaceId
List *terminatedList = scheduler->getTerminatedList(); // 终止队列
List *waitingList = scheduler->getWaitingList(); // 等待队列
// 确定Joinee在不在终止队列中
bool interminatedList = FALSE;
ListElement *first = terminatedList->listFirst(); // 队列首
while(first != NULL){
Thread *thread = (Thread *)first->item; // 强转成Thread指针
if(thread->userProgramId() == SpaceId){ // 在队列中
interminatedList = TRUE;
waitProcessExitCode = thread->ExitCode(); // 设置父线程等待子线程退出码
break;
}
first = first->next;
}
// Joinee不在终止队列中, 可运行态或阻塞态
if(!interminatedList){
waitingList->Append((void *)this); // 阻塞Joiner
currentThread->Sleep(); // Joiner阻塞
}
// 被唤醒且Joinee在终止队列中,在终止队列中删除Joinee
scheduler->deleteTerminatedThread(SpaceId);
(void) interrupt->SetLevel(oldLevel); // 开中断
}
3.3.5 Thread::Terminated()
该函数为将一个进程终止并加入终止队列的具体代码。
void Thread::Terminated() {
List *terminatedList = scheduler->getTerminatedList();
ASSERT(this == currentThread);
ASSERT(interrupt->getLevel() == IntOff);
status = TERMINATED;
terminatedList->Append((void *)this);
Thread *nextThread = scheduler->FindNextToRun();
while(nextThread == NULL){
// printf("yesn");
interrupt->Idle();
nextThread = scheduler->FindNextToRun();
}
scheduler->Run(nextThread);
}
3.4 Scheduler
3.4.1 Scheduler 类
在 Scheduler 类中,增加了进程等待、终止队列并添加了这两个队列所对应的函数,具体代码如下所示。
class Scheduler {
public:
Scheduler(); // 初始化调度队列
~Scheduler(); // 析构函数
void ReadyToRun(Thread* thread); // 将线程放入可运行队列
Thread* FindNextToRun(); // 找到第一个可运行态线程
void Run(Thread* nextThread); // 运行线程
void Print(); // 打印可运行线程队列
private:
List *readyList; // 可运行态线程的队列
#ifdef USER_PROGRAM
public:
List *getReadyList() { return readyList; }
List *getWaitingList() { return waitingList; }
List *getTerminatedList() { return terminatedList; }
void deleteTerminatedThread(int deleteSpaceId);
void emptyList(List *tmpList) { delete tmpList; }
private:
List *waitingList; // 等待运行线程的队列
List *terminatedList; // 终止运行但未释放线程的队列
#endif
};
3.4.2 Scheduler::Scheduler()
由于添加了新的类成员,因此需要在构造函数对类成员进行初始化。
Scheduler::Scheduler() {
readyList = new List;
#ifdef USER_PROGRAM
// 如果 Joinee 没有退出,Joiner 进入等待
waitingList = new List;
// 线程调用 Finish() 进入该队列,Joiner 通过检查该队列确定 Joinee 是否已经退出
terminatedList = new List;
#endif
}
3.4.3 Scheduler::~Scheduler()
对于新定义的类成员,需要在类的析构函数中将这些变量清除。
Scheduler::~Scheduler() {
delete readyList;
delete waitingList;
delete terminatedList;
}
3.4.4 Scheduler::deleteTerminatedThread()
该函数用于将一个线程从终止队列中移除,依然用于 Join() 系统调用的实现。
#ifdef USER_PROGRAM
void Scheduler::deleteTerminatedThread(int deleteSpaceId) {
ListElement *first = terminatedList->listFirst();
while(first != NULL){
Thread *thread = (Thread *)first->item;
if(thread->userProgramId() == deleteSpaceId){
terminatedList->RemoveItem(first);
break;
}
first = first->next;
}
}
#endif
3.5 List
该部分内容的修改主要是辅助 Scheduler 中针对队列操作,具体修改部分如下所示。
3.5.1 List 类
在该类中主要添加了两个函数,分别是 void RemoveItem(ListElement *tmp)
与 ListElement *listFirst()
,均用于 Scheduler 中的队列操作。
class List {
public:
List(); // 初始化 List
~List(); // 析构函数
void Prepend(void *item); // 将 item 放到 List 首
void Append(void *item); // 将 item 放到 List 尾
void *Remove(); // 将 item 从 List 首移除
void Mapcar(VoidFunctionPtr func); // 对 List 中每个元素应用 "func"
bool IsEmpty(); // List 是否为空
void RemoveItem(ListElement *tmp); // 移除 List 中一个元素
// Routines to put/get items on/off list in order (sorted by key)
void SortedInsert(void *item, int sortKey); // Put item into list
void *SortedRemove(int *keyPtr); // Remove first item from list
ListElement *listFirst() { return first; }
private:
ListElement *first; // List 首,NULL 则为空
ListElement *last; // List 尾
};
3.5.2 List::RemoveItem()
该函数用于从 List 中删除一个 ListElement,用于实现 Scheduler 类中从终止队列中移除一个元素的功能。
void List::RemoveItem(ListElement *tmp) {
bool isFind = FALSE;
ListElement *now = first, *pre = NULL;
while(now != NULL){
if(now->item == tmp->item) { isFind = TRUE; break;}
pre = now;
now = now->next;
}
if(isFind){
if(first == last) first = last = NULL; // 队里只有一个元素
else{
if(pre == NULL) first = first->next; // 删队首
else if(now == last) {last = pre; last->next = NULL;} // 删队尾
else{ // 删中间
pre->next = now->next;
}
}
}
}
3.6 OpenFile
该部分内容的修改主要是用于基于 FILESYS 的文件系统调用的实现,具体修改部分如下所示。
3.6.1 OpenFile 类
该类的修改主要针对于后续基于 FILESYS 实现的文件系统调用的实现。
#ifdef FILESYS
class FileHeader;
class OpenFile {
public:
OpenFile(int sector); // 打开一个文件头在 sector 扇区的文件(DISK)
~OpenFile(); // 关闭文件
void Seek(int position); // 定位文件读写位置
// 读取 numBytes 字节数据到 into 中,返回实际读取字节
int Read(char *into, int numBytes);
// 将 from 中 numByters 数据写入 OpenFile 中
int Write(char *from, int numBytes);
// 从 OpenFile 的 pos 位置读取字节到 into 中
int ReadAt(char *into, int numBytes, int position);
// 从 from 中的 pos 位置读取字节到 OpenFile 中
int WriteAt(char *from, int numBytes, int position);
int Length(); // 返回文件字节数
void WriteBack(); // 将文件头写回 DISK 中
#ifdef FILESYS
OpenFile(char *type) {}
int WriteStdout(char *from, int numBytes);
int ReadStdin(char *into, int numBytes);
#endif
private:
FileHeader *hdr; // 文件头
int seekPosition, hdrSector; // 文件当前读取位置,文件头所在扇区号
};
#endif
3.6.2 OpenFile::WriteStdout()
将数据写入输出对应的 OpenFile 中。
int OpenFile::WriteStdout(char *from, int numBytes) {
int file = 1;
WriteFile(file,from,numBytes); // 将from文件数据写入file中
return numBytes;
}
3.6.3 OpenFile::ReadStdin()
将 OpenFile 中的数据写入对应的 into 文件中。
int OpenFile::ReadStdin(char *into, int numBytes) {
int file = 0;
return ReadPartial(file,into,numBytes); // 将file文件数据写入into中
}
3.7 exception.cc
该代码主要实现了实验中要求实现的各个系统调用,包括 Exec()、Exit()、Join()、Yield() 基础系统调用以及基于文件系统的 Create()、Open()、Write()、Read()、Close() 系统调用,此处分别实现了基于 FILESYS_STUB 与 FILESYS 两套文件系统的文件系统调用。
3.7.1 Exec()
该系统调用主要用于执行一个新的 Nachos 文件,在 FILESYS 中从 DISK 中寻找该文件,在 FILESYS_STUB 中则在 UNIX 系统中寻找文件。
void AdvancePC(){
machine->WriteRegister(PrevPCReg,machine->ReadRegister(PCReg)); // 前一个PC
machine->WriteRegister(PCReg,machine->ReadRegister(NextPCReg)); // 当前PC
machine->WriteRegister(NextPCReg,machine->ReadRegister(NextPCReg)+4); // 下一个PC
}
void StartProcess(int spaceId) {
// printf("spaceId:%dn",spaceId);
ASSERT(currentThread->userProgramId() == spaceId);
currentThread->space->InitRegisters(); // 设置寄存器初值
currentThread->space->RestoreState(); // 加载页表寄存器
machine->Run(); // 运行
ASSERT(FALSE);
}
case SC_Exec:{
printf("This is SC_Exec, CurrentThreadId: %dn",(currentThread->space)->getSpaceId());
int addr = machine->ReadRegister(4);
char filename[50];
for(int i = 0; ; i++){
machine->ReadMem(addr+i,1,(int *)&filename[i]);
if(filename[i] == '