OSClassProjectWithNachos
山东大学 OS课设 nachos
Install / Use
/learn @ZWN2001/OSClassProjectWithNachosREADME
title: OS课设 date: 2022-10-09 16:33:36 tags: 操作系统 category: 学习
山东大学2022操作系统课程设计
你可以直接看这份文档 https://www.zwn2001.space/posts/Undergraduate-Course/OS%E8%AF%BE%E8%AE%BE/
OS课设
环境配置
老师给了安装教程PPT,并不麻烦。
在apt install前,确认你的虚拟机网络可用。
关于虚拟机的网络配置,我参考了https://zhuanlan.zhihu.com/p/130984945 ,整个教程我也复制整理了一遍放在文末。
Linux下使用CLion进行Debug
首先打开项目:将以下两个Makefile进行Associte With File Type,定义为GNU MakeFile文件,记得定义第一行匹配模式。完成后:
MakeFile.common中会报错,将如下冒号替换成空格:
<img src="OS课设\18.png" style="zoom:60%;" />然后打开具体的lab文件夹,会发现有报错,在Seetings中找到MakeFile,删掉Build target的all,选OK即可。
<img src="OS课设\19.png" style="zoom:60%;" />对于Configurations:
<img src="OS课设\20.png" style="zoom:60%;" />然后就可以打断点用debug模式运行了。
Lab1:Road Map Through Nachos
.
├── COPYRIGHT
├── gnu-decstation-ultrix // 交叉编译工具链
├── nachos-3.4.zip // 未经任何修改的源码和交叉编译工具,实验就是修改源码完善各个模块的功能
├── README
└── nachos-3.4 // 实验过程中完善的代码
├── test // 该目录下编写用户自己的程序,需要修改Makfile添加自己的文件
├── bin // 用户自己的程序需要利用coff2noff转换,才能在nachos下跑起来
├── filesys // 文件系统管理
│ ├── directory.cc // 目录文件,由目录项组成,目录项里记录了文件头所在扇区号
│ ├── directory.h
│ ├── filehdr.cc
│ ├── filehdr.h // 文件头数据结构,通过索引记录了文件内容实际存储的所有扇区号
│ ├── filesys.cc // 文件系统数据结构,创建/删除/读/写/修改/重命名/打开/关别等接口
│ ├── filesys.h
│ ├── fstest.cc
│ ├── Makefile
│ ├── openfile.cc // 管理所有打开的文件句柄
│ ├── openfile.h
│ ├── synchdisk.cc // 同步磁盘类,加锁保证互斥和文件系统的一致性
│ ├── synchdisk.h
│ └── test
├── machine // 机器硬件模拟
│ ├── console.cc // 终端
│ ├── console.h
│ ├── disk.cc // 磁盘
│ ├── disk.h
│ ├── interrupt.cc // 中断处理器,利用FIFO维护一个中断队列
│ ├── interrupt.h
│ ├── timer.cc // 模拟硬件时钟,用于时钟中断
│ ├── timer.h
│ ├── translate.cc // 用户程序空间虚拟地址和物理之间的转换类
│ └── translate.h
├── network // 网络系统管理
│ ├── Makefile
│ ├── nettest.cc
│ ├── post.cc
│ ├── post.h
│ └── README
├── threads // 内核线程管理
│ ├── list.cc // 工具模块 定义了链表结构及其操作
│ ├── list.h
│ ├── main.cc // main入口,可以传入argv参数
│ ├── Makefile
│ ├── scheduler.cc // 调度器,维护一个就绪的线程队列,时间片轮转/FIFO/优先级抢占
│ ├── scheduler.h
│ ├── stdarg.h
│ ├── switch.c // 线程启动和调度模块
│ ├── switch.h
│ ├── switch-old.s
│ ├── switch.s // 线程切换
│ ├── synch.cc // 同步与互斥,锁/信号量/条件变量
│ ├── synch.dis
│ ├── synch.h
│ ├── synchlist.cc // 类似于一个消息队列
│ ├── synchlist.h
│ ├── system.cc // 主控模块
│ ├── system.h
│ ├── thread.cc // 线程数据结构
│ ├── thread.h
│ ├── threadtest.cc
│ ├── utility.cc
│ └── utility.h
├── userprog // 用户进程管理
│ ├── addrspace.cc // 为noff文件的代码段/数据段分配空间,虚拟地址空间
│ ├── addrspace.h
│ ├── bitmap.cc // 位图,用于管理扇区的分配和物理地址的分配
│ ├── bitmap.h
│ ├── exception.cc // 异常处理
│ ├── Makefile
│ ├── progtest.cc // 测试nachos是否可执行用户程序
│ └── syscall.h // 系统调用
└── vm // 虚拟内存管理
└── Makefile // 多线程编译: make -j4
└── Makefile.common // 各个模块公共的Makefile内容存放到这里面
└── Makefile.dep // 依赖
<img src="OS课设\16.png" style="zoom:70%;" />
编译过程是先通过 .c 文件编译出 .o 文件,再通过 .o 文件编译出 .coff 文件,再通过 .coff 文件编译出 .noff 文件与 .flat 文件。
Nachos Machine
PS. 频繁出现的一个术语:例程,英文原文为routine,部分地方实际上指的是方法/函数,还有一些地方不好翻译,特此说明。
Nachos模拟了一台大致接近MIPS架构的机器。这台机器有寄存器、内存和cpu。此外,事件驱动的模拟时钟提供了一种机制来调度中断并在稍后的时间执行。模拟的MIPS机器可以执行任意程序。只需将指令加载到机器的内存中,初始化寄存器(包括程序计数器PCReg),然后告诉machine开始执行指令。然后机器获取PCReg指向的指令,对其进行解码并执行。该过程无限期地重复,直到执行非法操作或产生硬件中断。当陷阱或中断发生时,MIPS指令的执行被暂停,并调用Nachos中断服务例程来处理。
从概念上讲,Nachos有两种执行模式,其中一种是MIPS模拟器,类似于操作系统的用户态。Nachos通过将用户级进程加载到模拟器的内存中,初始化模拟器的寄存器,然后运行模拟器来执行用户级进程。用户程序只能访问与模拟机器相关联的内存。第二模式对应于Nachos内核。内核在Nachos首次启动时执行,或者当用户程序执行导致硬件陷阱(例如非法指令,页面故障,系统调用等)的指令时执行。在内核模式中,Nachos跟Unix进程一样执行。即执行Nachos源代码对应的语句,访问分配给Nachos变量的内存。
Machine Components
Nachos/MIPS机器由Machine对象实现,其中的一个实例是在Nachos首次启动时创建的。Machine对象对外提供Nachos内核直接访问的许多操作和公共变量。在下面,我们描述了Machine对象的一些重要变量。描述它们的作用有助于解释模拟硬件都做了什么。
Nachos机器对象提供寄存器、物理内存、虚拟内存支持以及运行机器或检查其当前状态的操作。当Nachos首次启动时,它会创建Machine对象的实例,并通过全局变量machine使其可用。Nachos内核可以访问以下公共变量:
-
registers/寄存器:一共40个,具有特殊含义的寄存器在
machine.h中有说明,如下#define StackReg 29 // User's stack pointer #define RetAddrReg 31 // Holds return address for procedure calls #define NumGPRegs 32 // 32 general purpose registers on MIPS #define HiReg 32 // Double register to hold multiply result #define LoReg 33 #define PCReg 34 // Current program counter #define NextPCReg 35 // Next program counter (for branch delay) #define PrevPCReg 36 // Previous program counter (for debugging) #define LoadReg 37 // The register target of a delayed load. #define LoadValueReg 38 // The value to be loaded by a delayed load. #define BadVAddrReg 39 // The failing virtual address on an exception #define NumTotalRegs 40 //注意这里写的是寄存器下标,比如PC寄存器是第34号寄存器。虽然寄存器可以通过
machine->registers[x]直接访问,但是Machine对象为此提供了ReadRegister()和WriteRegister()方法(下面将更详细地描述)。 -
mainMemory/主存:内存是字节可寻址的,并组织成128字节的页,与磁盘扇区大小相同。对应于物理地址x的内存可以通过
machine->mainMemory[x]访问。默认情况下,Nachos/MIPS机器有31页物理内存。实际使用的页数由machine.h中的NumPhysPages变量控制。 -
Virtual Memory/虚拟内存:Nachos通过单个线性页表或软件管理的TLB(虽然不是同时)来提供对虚拟内存的支持。通过初始化
Machine类的tlb或变量pageTable来进行高效控制。当执行指令时,在验证它们不是同时设置的前提下,machine对象使用定义的那个方案。
在此,我们了解了machine对象,以此来解释它如何执行任意的用户程序:首先,我们将程序的指令加载到机器的物理内存中(例如,machine->mainMemory变量)。接下来,我们初始化machine的页表和寄存器。最后我们调用machine->Run(),启动fetch-execute循环。
Machine对象提供以下操作/函数:
-
Machine(bool debug):构造函数Machine()接受单个参数debug。如果debug为true,MIPS模拟器会在单步模式下执行指令,在每个指令执行后调用调试器。调试器允许人们以交互方式检查机器状态,以验证(例如)寄存器或内存是否包含预期值。默认情况下,单步执行处于禁用状态。它是通过在启动Nachos时指定"-s"命令行选项来启用。- 对应代码
machine/machine.cc,Line55-76。 - 59-63初始化主存与寄存器。如果使用了TLB,65-68对TLB和页表进行初始化。(关于TLB和分页)
- 最后调用
CheckEndian()检查字节顺序,即内存多于一个字节类型的数据在内存中的存放顺序,通常有小端、大端两种字节顺序。
- 对应代码
-
RaiseException(ExceptionType which, int badVAddr):打印错误信息并将错误地址保存在寄存器中,结束进程中所有任务并通过中断进入内核态处理异常(或者系统调用),完成后返回用户态。 -
Debugger():调试器,输出调试信息,其中调用中断的DumpState()打印完整的中断状态,包括当前状态以及计划在未来发生的所有中断,Machine的DumpState()方法则打印了寄存器状态。 -
OneInstruction():执行指令的实际工作。它从PC寄存器获取当前指令地址,将其从内存中取出,解码,最后执行。执行方式就是switch-case。不过注意,在实际访问物理内存之前,作为fetch-execute周期一部分引用的任何地址(包括PCReg给出的指令地址)都要通过Translate()方法转换为物理地址。machine.h中给出了定义但在cpp文件中并没有给出实现。- 具体实现在
mipssim.cc中给出
-
Run():启动MIPS machine,启动fetch-execute循环。这个方法应该只有在机器寄存器和内存正确初始化后才被调用。它只是进入一个无限的fetch-execute循环。 运行中的主循环做了三件事:1)它调用Onelnstruction()来实际执行一条指令,2)如果用户在命令 行上请求单步模式,它调用调试器,3)它在每条指令后增加一个模拟时钟。-
machine.h中给出了定义但在cpp文件中并没有给出实现。 -
具体实现在
mipssim.cc中给出:void Machine::Run(){ Instruction *instr = new Instruction; // storage for decoded instruction if(DebugIsEnabled('m')) printf("Starting thread \"%s\" at time %d\n", currentThread->getName(), stats->totalTicks); interrupt->setStatus(UserMode); for (;;) { OneInstruction(instr); interrupt->OneTick(); if (singleStep && (runUntilTime <= stats->totalTicks)) Debugger(); } }
-
-
int ReadRegister(int num)/void WriteRegister(int num, int value):不多赘述。 -
bool ReadMem(int addr, int size, int* value):在虚拟地址addr中读1、2、4字节的内存。请注意,addr是当前执行的用户级程序的虚拟地址;在访问物理内存之前应当调用Translate()。-
需要注意的一点是,如果地址转换失败(无论出于何种原因),ReadMem失败(返回FALSE)。因此,如果物理内存中不存在该页,则ReadMem将失败。ReadMem不会将临时故障(例如,页面不在内存中)与硬错误(例如,无效的虚拟地址)区分开来。例如,在取消引用系统调用的参数时使用ReadMem。
-
machine.h中给出了定义但在cpp文件中并没有给出实现。 -
translate.cc给出了实现 -
bool Machine::ReadMem(int addr, int size, int *value){ int data; ExceptionType exception; int physicalAddress; DEBUG('a', "Reading VA 0x%x, size %d\n", addr, size); exception = Translate(addr, &physicalAddress, size, FALSE); if (exception != NoException) { machine->RaiseException(exception, addr); return FALSE; } switch (size) { case 1: data = machine->mainMemory[physicalAddress]; *value = data; break; case 2: data = *(unsigned short *) &machine->mainMemory[physicalAddress]; *value = ShortToHost(data); break; case 4: data = *(unsigned int *) &machine->mainMemory[physicalAddress]; *value = WordToHost(data); break; default: ASSERT(FALSE); } DEBUG('a', "\tvalue read = %8.8x\n", *value); return (TRUE); }
-
-
bool WriteMem(int addr,int size,int* value):在虚拟地址addr中写1、2、4字节的内存machine.h中给出了定义但在cpp文件中并没有给出实现。translate.cc给出了实现
Interrupt Management
machine/interrupt.cc & machine/interrupt.h
Nachos通过维护事件队列和模拟时钟来模拟中断。随着时钟滴答,检查事件队列以查找中计划现在发生的事件。时钟完全由软件维护,并在下列条件下滴答:
-
每次恢复中断(并且恢复的中断掩码已启用中断)时,时钟前进一个刻度。Nachos代码经常通过显式调用
interrupt::SetLevel()来禁用和恢复中断互斥锁。不过只有两种
Level,开与关 -
每当MIPS模拟器执行一条指令时,时钟前进一个刻度。
-
每当准备列表为空时,时钟就会前进所需要的数量的节拍,来快进当前时间到下一个预定事件的时间。
每当时钟前进时,都会检查事件队列,并通过调用与计时器事件相关联的过程(例如中断服务例程)来服务任何挂起的中断事件。所有中断服务routine都在禁用中断的情况下运行,中断服务routine可能不会重新启用它们。
Warning:中断处理器可能不能调用任何导致当前线程上下文切换的例程(例如,
scheduler::Run()或SWITCH())。这样做可能会导致死锁。这种限制是在 Nachos 下模拟中断方式的产物,不应该被视为在真实机器上完成事情的方式的指示。具体来说,考虑多个事件恰好在完全相同的时间被调度的情况。如果第一个事件的处理器(handler)调用sleep(调用SWITCH),则其他事件将无法在正确的时间得到服务。事实上,调用sleep的线程实际上可能正在等待现在应该发生的一个其他事件,但由于切换而延迟。因此发生死锁。为了正确地实现抢占,当一个正在运行的线程过期时,需要切换到另一个线程,就需要调用的中断处理程序。这是通过让中断服务例程调用
Thread::YieldOnReturn()来处理的,它会延迟实际的抢占,直到这样做是安全的。实际上interrupt.h中也给出了这个方法,但是方法的实现有点草率
To correctly implement preemption, the interrupt handler invoked when a running threads quantum expires needs to switch to another thread, This is handled by having the interrupt service routine invoke
Thread::YieldOnReturn(), which delays the actual preemption until it is safe to do so.
所有与中断管理相关的方法都由中断对象提供。主要包括:
-
void Schedule(VoidFunctionPtr handler, int arg, int fromnow, IntType type):调度未来的一个事件在时间fromnow发生/执行。当事件应该发生时Nachos会调用handler并置入参数arg。machine/interrupt.cc,生成一个PendingInterrupt并有序地插入事件队列。
