Lab4实验报告
一、思考题
Thinking 4.1
内核在保存现场的时候是如何避免破坏通用寄存器的?
系统陷入内核调用后可以直接从当时的 a 0 − a0- a0−a3参数寄存器中得到用户调用msyscall留下的信息吗?
我们是怎么做到让sys开头的函数“认为”我们提供了和用户调用msyscall时同样的参数的?
内核处理系统调用的过程对Trapframe做了哪些更改?这种修改对应的用户态的变化是?
-
保存现场时,
k0
寄存器暂存了sp
栈指针的值,k1
寄存器更新sp
栈指针的值,除k0
、k1
之外所有的通用寄存器都在修改之前被保存了,k0
和k1
是可以暂时被随意改变的寄存器,因此修改也没关系。 -
可以,
a0-a3
寄存器没有被修改过。 -
人工将参数加载到了sys开头函数认为的位置。
-
对
Trapframe
结构体中的cp0_epc
的值增加了4,将sys开头函数的返回值存入v0
寄存器。系统调用结束后,从syscall
的下一条开始执行。
Thinking 4.2
思考下面的问题,并对这个问题谈谈你的理解: 请回顾 lib/env.c 文件中
mkenvid()
函数的实现,该函数不会返回0,请结合系统调用和IPC部分的实现与envid2env()函数的行为进行解释。
由以上mkenvid()
函数可知,最终返回值的第11位始终为一,所以该函数不会返回零。
Thinking 4.3
思考下面的问题,并对这两个问题谈谈你的理解:
- 子进程完全按照 fork() 之后父进程的代码执行,说明了什么?
- 但是子进程却没有执行 fork() 之前父进程的代码,又说明了什么?
- 说明了子进程和父进程具有相同的代码段。
- 创建子进程时,PC值设置为了
fork()
的后一个指令,所以子进程没有执行fork()
之前父进程的代码。
Thinking 4.4
关于 fork 函数的两个返回值,下面说法正确的是:
A、fork 在父进程中被调用两次,产生两个返回值
B、fork 在两个进程中分别被调用一次,产生两个不同的返回值
C、fork 只在父进程中被调用了一次,在两个进程中各产生一个返回值
D、fork 只在子进程中被调用了一次,在两个进程中各产生一个返回值
C
Thinking 4.5
我们并不应该对所有的用户空间页都使用
duppage
进行映射。那么究竟哪些用户空间页应该映射,哪些不应该呢?请结合本章的后续描述mm/pmap.c 中mips_vm_init
函数进行的页面映射以及 include/mmu.h 里的内存布局图进行思考。
需要映射的是0—USTACKTOP
范围内的空间。
因为其上的范围,USTACKTOP到UXSTACKTOP之间为用户进程的异常栈,而异常栈是进行异常处理的地方,不应映射;UTOP以上为内核相关页表,无权更改,也不需映射。
Thinking 4.6
在遍历地址空间存取页表项时你需要使用到vpd和vpt这两个“指针的指针”,请参考 user/entry.S 和 include/mmu.h 中的相关实现,思考并回答这几个问题:
- vpt和vpd的作用是什么?怎样使用它们?
- 从实现的角度谈一下为什么进程能够通过这种方式来存取自身的页表?
- 它们是如何体现自映射设计的?
- 进程能够通过这种方式来修改自己的页表项吗?
-
vpd
存放页目录基地址,基地址加页目录项偏移数即为va
对应的页目录项;vpt
为页表基地址,基地址加页表项偏移数即为va
对应的页表项。 -
在
entry.S
中,定义了页表和页目录的虚拟地址,使每个进程的页表都能在UVPT中保存。 -
vpd指向
(UVPT+(UVPT>>12)*4)
,这是自映射机制。 -
不能,用户进程无权修改自己和内核的页表项,必须要陷入内核才能进行操作。
Thinking 4.7
page_fault_handler
函数中,你可能注意到了一个向异常处理栈复制Trapframe运行现场的过程,请思考并回答这几个问题:
- 这里实现了一个支持类似于“中断重入”的机制,而在什么时候会出现这种“中断重入”?
- 内核为什么需要将异常的现场Trapframe复制到用户空间?
- 如果在缺页中断时再次响应了外部中断,便会“中断重入”。
- 用户进程在中断结束恢复现场需要Trapframe,写入用户空间,则需要将异常的Trapframe复制到用户空间。
Thinking 4.8
到这里我们大概知道了这是一个由用户程序处理并由用户程序自身来恢复运行现场的过程,请思考并回答以下几个问题:
- 在用户态处理页写入异常,相比于在内核态处理有什么优势?
- 从通用寄存器的用途角度讨论,在可能被中断的用户态下进行现场的恢复,要如何做到不破坏现场中的通用寄存器?
- 在用户态处理可以将操作交由用户自己完成,简化操作系统的复杂度,并且减小处理失误时对操作系统造成的后果。
- 将通用寄存器入栈,然后通过sp寄存器再恢复通用寄存器。
Thinking 4.9
请思考并回答以下几个问题:
- 为什么需要将
set_pgfault_handler
的调用放置在syscall_env_alloc
之前?- 如果放置在写时复制保护机制完成之后会有怎样的效果?
- 子进程是否需要对在entry.S定义的字__pgfault_handler赋值?
- 父子进程共享空间,在父进程调用
env_alloc
的过程中可能也需要进行缺页处理。 - 这样的话发生缺页中断不能够被捕捉到,无法进入缺页中断异常
- 不需要,父子进程可以共享。
二、实验难点图示
1、系统调用
syscall流程图,以writef()函数为例:
2、进程通信机制
进程间通信机制需要通过系统调用实现进程之间的数据交流。
但是由于进程的地址空间都是独立的,要想把数据从一个地址空间转移到另一个空间,需要利用各个进程都共享的内核的2G空间,所以要使用内核中的进程控制块来实现进程通信,即修改PCB的某些属性。
3、fork()区分进程
一个进程调用fork,在两个进程中得到两个返回值,与sys_env_alloc函数密切相关。这里的难点有两个:产生两个返回值的机理,fork的流程。父进程fork中执行了系统调用syscall_env_alloc,需要从系统调用中恢复现场;子进程被创建,但是没有被调度,需要在调度的时候恢复现场。
fork()函数的大致流程如下:
4、缺页中断
5、duppage
duppage中需要分别给父进程与子进程相关的页,设置PTE_COW位,要先给子进程设置,再给父进程设置。
子进程现在是不可运行的,而父进程是可运行的。如果先给父进程某页设置了PTE_COW,父进程可能修改这一页,触发写时复制,父进程重新分配一页,但是这一页不再存在共享的冲突,没有PTE_COW,这个时候map子进程,子进程相应地址也指向这一页,但是有PTE_COW。之后如果父进程再修改这一页,由于没有PTE_COW,不会触发写时复制,这一共享页被修改了,但是对于子进程来说,不应该被修改,于是执行出现错误。
三、体会与感想
本次实验课下花了三四天的时间理解,确实明显感觉到难度上来了。这部分重在理解,第一遍写的时候还不是很懂,后来看了网上一些往届学长的难点梳理,自己也从头到尾重新梳理了一遍,理解才更加清楚了。
lab4难点有几个,系统调用、进程通信、fork等,这些也都总结在难点图示里了,这一单元有很多功能不同的函数,并且要格外注意区分用户态与核心态的执行函数,而且这一部分有些地方需要填写汇编代码,有些需要跳转,有些需要人为操纵寄存器,理解起来比较有难度。总体来说,lab4过的还是比较煎熬的,lab4需要大量之前写过的函数,所以在做lab4的时候找出来很多lab2、lab3的bug,找bug的过程真的很痛苦Orz。
关于lab4的两次上机,lab4-1属于课上样例跑对了但是交上去0分的情况,至今仍不知道问题出在哪,但是由于考lab4-1的时候我lab4的第二部分还没有做完,所以有点怀疑是课下bug没找出来导致的。考lab4-2之前其实感觉自己心里也挺没底,上机时看到题目也觉得有点悬,代码量不大,但是各种权限位属实搞得有点头大,最后竟然过了,也是挺惊喜的。
lab4没有lab1、2、3的分值大,虽然难度上去了,但是反而没有考前面的lab时的那种焦虑了,考试时心态更轻松了,答题状态貌似也好了很多,总之还是收获很大。
四、残留难点
syscall.S中handle_sys中提到了内核栈指针与用户栈指针,对于两个指针到底指向哪里,不是很清楚。