本次实验主要实现了MOS操作系统的系统调用
Thinking 4.1
思考并回答下面的问题:
- 内核在保存现场的时候是如何避免破坏通用寄存器的?
- 系统陷入内核调用后可以直接从当时的 $a0-$a3 参数寄存器中得到用户调用 msyscall 留下的信息吗?
- 我们是怎么做到让 sys 开头的函数”认为”我们提供了和用户调用 msyscall 时同样的参数的?
- 内核处理系统调用的过程对 Trapframe 做了哪些更改?这种修改对应的用户态的变化是什么?
- 调用SAVE_ALL宏函数,以sp为基地址将所有寄存器值存入
- 不能得到,虽然调用函数时默认前四个参数传入$a0-$a3 寄存器,但实际上,在内核态中会使用这些寄存器进行一些操作计算,此时寄存器原有值被改变,因此再次以这些参数调用其他函数时需要重新从栈中取用这四个寄存器值
- 将调用函数时都将前四个参数按顺序放入$a0-$a3 寄存器,后面的参数按顺序存入内核栈中的相同位置,通过保存现场并使用现场数据中保存的参数,让 sys 开头的函数”认为”我们提供了和用户调用 msyscall 时同样的参数
- 将epc加4,使得系统调用后pc指向调用时的下一条指令;同时修改 tf 中的 v0 寄存器,向用户态传递返回值
Thinking 4.2
思考 envid2env 函数: 为什么 envid2env 中需要判断 e->env_id != envid 的情况?如果没有这步判断会发生什么情况?
32位进程编号结构是:低 10 位索引该编号对应进程控制块在 envs 中的偏移量,而高 22 位则表示该进程被分配的序号。在生成 envid 时,可能会出现使用一个 envid 在 envs中索引到的进程控制块中的 envid 和我们预期的不符的情况,如果不判断 envid 是否相同,可能会取到错误的进程控制块
Thinking 4.3
思考下面的问题,并对这个问题谈谈你的理解:请回顾 kern/env.c 文件中 mkenvid() 函数的实现,该函数不会返回 0,请结合系统调用和 IPC 部分的实现与envid2env() 函数的行为进行解释。
u_int mkenvid(struct Env *e) {
static u_int i = 0;
return ((++i) << (1 + LOG2NENV)) | (e - envs);
}
- 在 mkenvid() 函数中,++i 保证分配的 envid 一定不会为 0,而 envid2env() 中,如果 envid 为 0,函数返回 curenv
- 这样做的目的是:在用户使用系统调用时,会通过传入 0 这个 id 来对当前进程进行操作,而若 mkenvid 在生成进程 id 时生成了 0,这样的调用行为存在二义性,无法明确用户指定的进程是当前进程还是 id 为 0 的进程,因此,该函数不应该返回 0
Thinking 4.4
关于 fork 函数的两个返回值,下面说法正确的是: A、 fork 在父进程中被调用两次,产生两个返回值 B、 fork 在两个进程中分别被调用一次,产生两个不同的返回值 C、 fork 只在父进程中被调用了一次,在两个进程中各产生一个返回值 D、 fork 只在子进程中被调用了一次,在两个进程中各产生一个返回值
正确答案是 C
Thinking 4.5
我们并不应该对所有的用户空间页都使用 duppage 进行映射。那么究竟哪些用户空间页应该映射,哪些不应该呢?请结合 kern/env.c 中 env_init 函数进行的页面映射、include/mmu.h 里的内存布局图以及本章的后续描述进行思考。
- 0~USTACKTOP 范围的内存需要使用 duppage 进行映射
- USTACKTOP~UTOP 范围的内存是用来进行页写入异常的,不会在处理 COW 异常时调用 fork(),因此不需要映射
- UTOP以上页面的内存和页面是所有进程共享的,且用户进程无权访问,不需要映射
Thinking 4.6
在遍历地址空间存取页表项时你需要使用到 vpd 和 vpt 这两个指针,请参考 user/include/lib.h 中的相关定义,思考并回答这几个问题:
- vpt 和 vpd 的作用是什么?怎样使用它们?
- 从实现的角度谈一下为什么进程能够通过这种方式来存取自身的页表?
- 它们是如何体现自映射设计的?
- 进程能够通过这种方式来修改自己的页表项吗?
#define vpt ((const volatile Pte *)UVPT)
#define vpd ((const volatile Pde *)(UVPT + (PDX(UVPT) << PGSHIFT)))
- vpd 和 vpt 将用户进程地址空间中的页表基址和页目录基址以指针的形式封装起来,方便编程时使用以访问页表和页目录,使用上可以当作一般的 Pte* 指针和 Pde* 指针使用
-
根据 mmu.h 中的地址空间布局图,所有的进程的页表基址均为 UVPT,根据这种统一性,可以通过这样一个宏定义得到的指针来对进程自身的页表进行读取
-
vpd 的地址在以 UVPT 为基址的第 PDX(UVPT) 个页面,说明将页目录映射到了页表相应的位置,体现了页目录自映射机制
- 不能,用户态下无权修改页表项,该区域对用户只读不写,若想要增添页表项,需陷入内核进行操作
Thinking 4.7
在 do_tlb_mod 函数中,你可能注意到了一个向异常处理栈复制 Trapframe运行现场的过程,请思考并回答这几个问题:
- 这里实现了一个支持类似于“异常重入”的机制,而在什么时候会出现这种“异常重入”?
- 内核为什么需要将异常的现场 Trapframe 复制到用户空间?
- 当出现COW异常时,需要使用用户态的系统调用发生中断,这时会出现中断重入
- 处理COW异常时调用 handle_mod() 函数把 epc 改为用户态的异常处理函数 env_user_tlb_mod_entry ,因此退出内核中断后,会跳转到 epc 所在的用户态的异常处理函数,由于用户态把异常处理完毕后仍然在用户态恢复现场,所以需要要把内核保存的现场保存在用户空间的用户异常栈
Thinking 4.8
在用户态处理页写入异常,相比于在内核态处理有什么优势?
- 遵循了微内核设计原则,将异常处理的操作交给用户进程自己来完成,精简了内核的功能范围
- 将处理的核心逻辑从内核下放到用户态,降低了处理失败对整个操作系统造成的危害与损失
Thinking 4.9
请思考并回答以下几个问题:
- 为什么需要将 syscall_set_tlb_mod_entry 的调用放置在 syscall_exofork 之前?
- 如果放置在写时复制保护机制完成之后会有怎样的效果?
- 因为COW中断的处理机制依赖于 syscall_set_tlb_mod_entry,父进程调用 syscall_exofork 的过程中也可能会触发缺页异常,所以将 syscall_set_tlb_mod_entry 的调用放置在 syscall_exofork 之前
- 父进程在调用写时复制保护机制可能会引发缺页异常,而如果 syscall_set_tlb_mod_entry 未设置好,操作系统会无法处理这个异常并报错
实验难点
-
权限位处理:本次实验由于引入用户态和内核态的转换,以及写时复制机制的引入,使得权限位的设置和检查变得复杂,尤其在 duppage 中,需要清楚哪些部分地址的页面是需要被映射的,哪些是不需要的
- 不同栈的区分:明确在MIPS微内核的设计下,内核栈、用户栈和用户异常栈的不同作用,用户栈保存系统调用参数,用户异常栈保存中断现场,在处理异常和系统调用时,需要正确使用对应的栈空间
- 内核与用户空间:在实验中,时刻清楚当前是在修改内核还是用户空间代码至关重要,内核函数不能在用户空间调用,需明确两者的界限和不同要求,用户态申请内核态的系统调用需要通过中断机制
实验感想
- 本次实验涉及系统调用、IPC 通信机制、fork 进程创建、页面写入异常处理等多方面知识,且与前序 lab 紧密联系,如内存管理与 lab2 相关,进程部分与 lab3 相关,需综合运用多方面知识,对计组的中断机制等知识也有较高要求,需深入理解底层原理才能顺利完成实验
- 通过本次实验,不仅加深了我对各部分知识的理解,更提升了综合运用知识解决复杂问题的能力。同时,指导书中的流程图对理解实验流程和逻辑有很大帮助,让我对操作系统的底层原理理解更进一步