Status
Done

实验目的

实验内容:
  1. 扩展现有的class AddrSpace的实现,使得Nachos可以实现多道用户程序。按照实验指导书中的方法,完成class AddrSpace中的Print函数。实现Nachos系统调用:Exec(),一个用户程序启动另一个用户程序。注意本实验要求实现的Exec()系统调用,是在另一个地址空间运行指定的另一个用户程序,新程序并没有覆盖调用者的地址空间。这与Unix/Linux的系统调用exec()不同。
  1. 在Nachos中增加并实现一个新的系统调用:PrintInt(),在用户程序中打印一个整数值。
  1. 在实现了多道用户程序的基础上,若要求在Nachos中实现与Unix/Linux 的fork()/exec()功能类似的Nachos系统调用Fork()/Exec(),及写时复制 (copy-on-write) 机制,请给出在Nachos中实现的具体方法(实现时假定有足够的物理内存,无需页面置换。不要求实现可运行的代码。在实验报告中用文字描述即可,必要时可在文字中结合关键代码片段、数据结构、对象等说明)。
参阅:
操作系统课程设计 指导教程 -张鸿烈 2012.pdf,pp.53-60,pp.64-68
code/lab6/n6
code/lab6/n6readme.txt
code/lab6/n6screen.txt
man fork
man exec
注1:实现PrintInt系统调用需要修改code/userprog/syscall.h及code/test/start.s。在对原文件备份后,这两个文件可原位修改
Lab6涉及2个用户进程,其源码文件分别命名为code/test/exec.c及code/test/halt2.c,其代码分别为:
 

实验步骤与内容

扩展地址空间的实现并实现Exec系统调用

扩展现有的class AddrSpace的实现,使得Nachos可以实现多道用户程序。按照实验指导书中的方法,完成class AddrSpace中的Print函数。实现Nachos系统调用:Exec(),一个用户程序启动另一个用户程序。注意本实验要求实现的Exec()系统调用,是在另一个地址空间运行指定的另一个用户程序,新程序并没有覆盖调用者的地址空间。这与Unix/Linux的系统调用exec()不同。
 
多道程序设计技术,就是指允许多个程序同时进入内存并运行。即同时把多个程序放入内存,并允许它们交替在CPU中运行,它们共享系统中的各种硬、软件资源。当一道程序因I/O请求而暂停运行时,CPU便立即转去运行另一道程序。
 
Nachos的用户进程由两部分组成:核心部分和用户程序部分。核心部分同一般的系统线程没有区别,它共用了Nachos的正文段和数据段,运行在宿主机上;而用户程序部分则有自己的正文段、数据段和栈段,它存储在Nachos的模拟内存中,运行在Nachos的模拟机上。
在控制结构上,Nachos的用户进程比系统线程多了以下内容:
 
首先,修改test/Makefile文件中的targets,使其可以编译exec.c和halt2
 

现有的AddrSpace类

【addrspace.h】用户程序空间有AddrSpace类来描述
 
【addrspace.cc】
AddrSpace (OpenFile *executable):初始化用户程序空间
 
InitRegisters方法:初始化寄存器,让用户程序处于可以运行状态。
 
RestoreState方法:恢复处理机用户程序空间的状态。
 
 

扩展现有的class AddrSpace的实现

为了实现多道用户程序的运行,我们需要正确管理不同线程虚拟地址与物理地址的对应,当前实现的地址空间假设一个简单的 1:1 映射(虚拟页号与物理页号相同),这在多道程序环境下不成立,因为多个用户程序需要共享物理内存。
 
在system.h中添加全局变量ProgMap和freeMM_Map的声明:
ProgMap 是一个布尔数组,长度为 NumPhysPages(通常表示物理页的数量),用于表示每个物理页是否被某个进程占用,管理Nachos系统中物理页的分配状态。
freeMM_Map 是一个指向 BitMap 的指针,用于动态管理内存资源(如物理页或其他可分配资源),提供更灵活的内存管理功能。
 
因此还需再system.cc中添加对freeMM_Map 的初始化和释放
在系统启动时,需要分配这个 BitMap 实例,作为管理内存的核心数据结构。
由于 freeMM_Map 是动态分配的,为了防止内存泄漏,必须显式释放。
扩展addrspace类——构造函数
接下来为用户程序建立一个页表,完成 虚拟页到物理页的映射。初始化页表中的各项标志位,为虚拟内存管理和页面置换算法提供基础。通过这段代码,每个虚拟页都被分配了一个唯一的物理页,使得用户程序可以在虚拟地址空间中正确运行,同时物理内存的使用受到严格管理。
接下来初始化用户程序的代码段和数据段,将可执行文件中的相关内容加载到主存中,为程序的执行做好准备。
 
扩展addrspace类——析构函数
 
 

完成class AddrSpace中的Print函数

为了能够了解Nachos中多用户程序驻留内存的情况,按照实验指导书要求,在【addrspace.cc】AssSpace类中增加以下打印成员函数Print,并在【addrspace.h】种补充函数定义
 

系统调用

系统调用是用户程序和操作系统内核的接口。用户程序从系统调用函数取得系统服务。
当CPU控制从用户程序切换到系统态时,CPU的工作方式由用户态改变为系统态。而当内核完成系统调用功能时,CPU工作状态又从系统态改变回用户态并且将控制再次返回给用户程序。两种不同的CPU工作状态提供了操作系统基本的保护方式。
 
Nachos用户程序的机构
C语言编写的用户程序在由gcc MIPS 交叉编译后都在前面连接上一个由MIPS汇编程序start.s生成的叫start.o的目标模块。实际上start是用户程序真正的启动入口,由它来调用C程序的main函数。所以不要求用户编程时一定要把main函数作为第一个函数。这个汇编程序也为Nachos系统调用提供了一个汇编语言的存根(stub)。
例如C程序halt.c被编译为haltt.o,同时start.s也被汇编为start.o。之后两个目标模块被连接成可执行的Coff格式的可执行文件,最后这个Coff文件又被转换为Noff格式的Nachos可执行文件。
 
系统调用接口
所有Nachos系统调用的接口原型都定义在文件userprog/syscall.h中。当编译用户程序时编译器会括入这个文件并取得这些系统调用接口原型的信息(这就是为什么当编译例如halt.c时Makefile文件中必须加入-I../userprog定义)
对应的系统调用的汇编语言存根在test/start.s文件中。如果要添加自己的系统调用,就应当首先在syscall.h和start.s中声明你的系统调用原型和存根。当一个系统调用由一个用户进程发出时,由汇编语言编写的对应于存根的程序就被执行然后,这个存根程序会由执行一个系统调用指令而引发一个异常或自陷。
在start.s中的这些系统调用的接口程序代码都是一样的。即:
  • 将对应的系统调用的编码送$2寄存器
  • 执行系统调用指令SYSCALL
  • 返回到用户程序
 
模拟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向前推进,从而在异常处理之后同一条指令将会再次被启动。
函数RaiseException(ExceptionType which, int badVAddr)的代码在machine/nachine.cc文件中。
这个函数模拟硬件的动作,切换到系统态并且在异常处理完成后返回到用户态。
ExceptionHandler(which)函数调用模拟硬件的动作发一个异常中断到对应的异常处理程序。
该函数在userprog/exception.cc种实现
 
 
 

实现Nachos系统调用:Exec(),一个用户程序启动另一个用户程序。

注意本实验要求实现的Exec()系统调用,是在另一个地址空间运行指定的另一个用户程序,新程序并没有覆盖调用者的地址空间。这与Unix/Linux的系统调用exec()不同。
 
具体实现思路:
  • Fork创建线程,根据线程需要执行的文件,
  • New AddrSpace创建一块新的进程地址空间,并将新进程地址空间映射到当前核心线程。
  • 初始化用户寄存器,初始化系统页表,进入用户模式开始执行用户程序。
  • 获得已创建好的进程地址空间,并将新进程地址空间映射到当前核心线程。让新线程去使用CPU。
 
观察userprog/syscall.h和test/start.s可知Exec()系统调用已经被声明。
 
接下来需要在exception.cc的ExceptionHandler(which)函数中实现调用模拟硬件的动作发一个异常中断到对应的异常处理程序,给出的代码中已经实现了对SC_Halt停机的系统调用,现在仿照添加处理SC_Exec系统调用,代码如下:
其中Exec()在interrupt.h中定义并在interrupt.cc中实现
 
 

添加PrintInt系统调用

在Nachos中增加并实现一个新的系统调用:PrintInt(),在用户程序中打印一个整数值。
正如前文所言,所有Nachos系统调用的接口原型都定义在文件userprog/syscall.h中,因此在该文件中添加新的系统调用的定义。
在test/start.s种声明系统调用的存根
 
接下来需要在ExceptionHandler(which)函数中实现调用模拟硬件的动作发一个异常中断到对应的异常处理程序
系统调用的参数分别存储在寄存器4、5、6、7中,此处只传入一个参数,被存在寄存器中,因此此处从寄存器中读取了该参数。
在interrupt.h中定义并在interrupt.cc中实现printInt方法
 
 
接下来我们进行一下测试
notion image
测试结果与给出的示例相同。
 

实现与Unix系统fork/exec功能类似的Fork/Exec系统调用

在实现了多道用户程序的基础上,若要求在Nachos中实现与Unix/Linux 的fork()/exec()功能类似的Nachos系统调用Fork()/Exec(),及写时复制 (copy-on-write) 机制,请给出在Nachos中实现的具体方法(实现时假定有足够的物理内存,无需页面置换。不要求实现可运行的代码。在实验报告中用文字描述即可,必要时可在文字中结合关键代码片段、数据结构、对象等说明)。
 

Linux中的fork

fork() 可以创建一个新进程,并复制调用进程的资源(如数据和代码)。父进程和子进程共享初始状态,但其变量和地址空间是独立的。
Fork的返回:
1) 在父进程中,fork返回新创建子进程的进程ID;
2) 在子进程中,fork返回0;
3) 如果出现错误,fork返回一个负值;
 
Nachos中实现fork
1.首先要在userprog/syscall.h中定义系统调用,并在test/start.s种声明系统调用的存根。
2.在ExceptionHandler(which)函数中实现调用模拟硬件的动作发一个异常中断到对应的异常处理程序:创建一个新的Thread作为子线程,为其分配地址空间,将子进程添加到就绪队列中。
3.复制页表、堆栈等地址空间结构,为写时复制机制做准备(例如设置共享页和只读标记)。
 

Linux中的exec

exec() 用指定的可执行文件替换调用进程的内容。调用 exec 后,进程的地址空间、代码段、数据段等都会被新的程序覆盖。
常见的是父进程通过 fork() 创建子进程,子进程调用 exec() 加载新程序,从而实现类似“创建新进程”的效果。
 

写时复制 copy-on-write

我们已经知道,fork会将调用进程的所有内容原封不动的拷贝到新产生的子进程中去,这些拷贝的动作很消耗时间,而如果fork完之后我们马上就调用exec,这些辛辛苦苦拷贝来的东西又会被立刻抹掉,这看起来非常不划算。
写时复制用于优化 Fork() 的地址空间复制,将父子进程的页共享为只读,只有在页被写入时才执行真正的物理页复制。
实现步骤:
1.页表共享与标记,在 Fork() 时,共享父进程的页表,将物理页设置为只读。
2.在 CPU 访问内存时,如果发生写操作且页为只读,需处理写时复制:
  • 捕获异常
  • 分配新的物理页
  • 将旧页内容复制到新页
  • 更新页表,解除只读标记
3.同步父子进程的页表状态

结论分析和体会

本次实验的难度与前面的实验相比,难度很大,断断续续的写了很多天,一点一点的了解学习操作系统中多道程序设计的实现原理,尤其是在Nachos系统中实现多道用户程序运行的关键技术。
这次实验的核心任务是实现Exec系统调用与PrintInt系统调用,同时探索Fork/Exec机制和写时复制策略的设计与优化。在实验过程中,我对地址空间管理的重要性有了更加直观的认识,特别是如何通过页表实现虚拟地址与物理地址的映射,以及如何合理扩展地址空间以支持多道程序的执行。这不仅让我理解了操作系统在内存分配上的策略,也体会到了多进程环境中隔离与资源共享之间的平衡。
系统调用的设计与实现是实验的另一大重点。在实现Exec系统调用时,我深刻体会到其作为操作系统与用户程序之间接口的关键作用,需要综合考虑进程创建、资源分配和执行环境初始化等问题。这部分实验让我对系统调用的本质有了更深入的理解,尤其是操作系统如何通过这些接口完成用户程序的功能请求和资源管理。
实验中涉及的多线程与多进程的协作让我意识到调度机制在多道程序运行中的重要性。通过对线程与进程控制结构的扩展,我逐步理解了操作系统如何实现进程之间的资源共享与隔离,如何通过合理的调度算法提升系统的并发性能。此外,通过分析写时复制机制的设计,我更加明确了内存优化在操作系统性能提升中的作用,也对减少不必要的内存拷贝有了更加深刻的认识。
 
Loading...