BUAA_OS lab4实验报告
一、思考题
1. 思考4.1
思考并回答下面的问题:
• 内核在保存现场的时候是如何避免破坏通用寄存器的?
• 系统陷入内核调用后可以直接从当时的$a0-$a3 参数寄存器中得到用户调用msyscall 留下的信息吗?
• 我们是怎么做到让sys 开头的函数“认为”我们提供了和用户调用msyscall 时同样的参数的?
• 内核处理系统调用的过程对Trapframe 做了哪些更改?这种修改对应的用户态的变化是?
-
调用SAVE_ALL宏函数,以sp为基地址将所有寄存器值存入。
-
能,因为调用函数时默认前四个参数传入$a0-$a3寄存器。但在内核态中可能使用这些寄存器进行一些操作计算,此时寄存器原有值被改变,因此再次以这些参数调用其他函数时需要重新以sp为基地址,按相应偏移从用户栈中取用这四个寄存器值。
-
将调用函数时都将前四个参数按顺序放入$a0-$a3寄存器,后两个参数按顺序存入内核栈中的相同位置(相对sp偏移相同)。
-
涉及到Trapframe修改的部分代码:
lw t0, TF_EPC(sp)
addiu t0, t0, 4
sw t0, TF_EPC(sp)将EPC加4,使得系统调用后pc指向调用时的下一条指令。
2. 思考4.2
思考下面的问题,并对这两个问题谈谈你的理解:
• 子进程完全按照fork() 之后父进程的代码执行,说明了什么?
• 但是子进程却没有执行fork() 之前父进程的代码,又说明了什么?
-
子进程的代码段与父进程相同(子进程代码段共享了父进程的物理空间)。
-
子进程恢复的上下文位置为fork函数接下来的位置。
3. 思考4.3
关于fork 函数的两个返回值,下面说法正确的是:
A. fork 在父进程中被调用两次,产生两个返回值
B. fork 在两个进程中分别被调用一次,产生两个不同的返回值
C. fork 只在父进程中被调用了一次,在两个进程中各产生一个返回值
D. fork 只在子进程中被调用了一次,在两个进程中各产生一个返回值
答:C
4. 思考4.4
如果仔细阅读上述这一段话, 你应该可以发现, 我们并不是对所有的用户空间页都使用duppage 进行了保护。那么究竟哪些用户空间页可以保护,哪些不可以呢,请结合include/mmu.h 里的内存布局图谈谈你的看法。
在0 ~ USTACKTOP范围的内存,除只读、共享的页面外都需要设置PTE_COW进行保护。
5. 思考4.5
在遍历地址空间存取页表项时你需要使用到vpd 和vpt 这两个“指针的指针”,请思考并回答这几个问题:
• vpt 和vpd 的作用是什么?怎样使用它们?
• 从实现的角度谈一下为什么能够通过这种方式来存取进程自身页表?
• 它们是如何体现自映射设计的?
• 进程能够通过这种存取的方式来修改自己的页表项吗?
-
vpd是指向用户页目录的指针,以*vpd(指向数组中第一个元素的指针)为基地址,加上页目录项偏移数即可指向va对应页目录项,即
((Pde*)(*vpd)) + (va >> 22)
;vpt是指向用户页表的指针数组的指针,以*vpt(指向数组中第一个元素的指针)为基地址,加上页表项偏移数即可指向va对应的页表项,即((Pte*)(*vpt)) + (va >> 12)
。 -
在user/entry.S中定义了vpt和vpd,它们分别指向UVPT和(UVPT + (UVPT >> 12) * 4),即用户页表和用户页目录的虚拟地址,有了基地址,和从虚拟地址中获得的偏移,即可实现对自身进程页表的操作。
-
注意到,vpd的地址在UVPT和UVPT + PDMAP之间,说明将页目录映射到了某一页表位置,即实现了自映射。
-
不能。因为用户态不能修改页表项。
6. 思考4.6
page_fault_handler 函数中,你可能注意到了一个向异常处理栈复制Trapframe 运行现场的过程,请思考并回答这几个问题:
• 这里实现了一个支持类似于“中断重入”的机制,而在什么时候会出现这种“中断重入”?
• 内核为什么需要将异常的现场Trapframe 复制到用户空间?
-
中断重入可能是因为在处理缺页中断时又发生了中断。
-
在我们的操作系统中,异常是在用户态处理的,需要根据异常现场的Trapframe进行处理;同时,需要保存现场的寄存器值,防止其被破坏。
7. 思考4.7
到这里我们大概知道了这是一个由用户程序处理并由用户程序自身来恢复运行现场的过程,请思考并回答以下几个问题:
• 用户处理相比于在内核处理写时复制的缺页中断有什么优势?
• 从通用寄存器的用途角度讨论用户空间下进行现场的恢复是如何做到不破坏通用寄存器的?
-
尽量减少内核出现错误的可能,即使程序崩溃,也不会影响系统的稳定,同时微内核的模式下,用户态进行新页面的分配映射也更加灵活方便。
-
通用寄存器只是用于CPU计算处理,在宏观来看,得益于模块化的处理结构,且这一步并不是系统突发的异常,而是顺次执行到的,因此进入现场恢复阶段前的计算结果都已完成利用或存入了相应区域,此时通用寄存器中没有需要保护的内容了,因此可以随意覆盖。
8. 思考4.8
请思考并回答以下几个问题:
• 为什么需要将set_pgfault_handler 的调用放置在syscall_env_alloc 之前?
• 如果放置在写时复制保护机制完成之后会有怎样的效果?
• 子进程需不需要对在entry.S 定义的字__pgfault_handler 赋值?
-
用于处理在alloc过程中发生的缺页中断。
-
由于无法处理缺页中断错误,写时复制保护机制不能执行。
-
不需要。该值已在父进程中设置,子进程只需与父进程保持一致。
二、实验难点
1. handle_sys函数
根据我的理解,用户态和内核态的栈空间大致按如图所示的关系操作,在跳转到相应的系统调用函数之前,前四个参数可以由寄存器传递,但后两个参数需要通过开辟新的栈空间传递。
2. 进程通信
-
sys_ipc_recv
-
设置允许接收
-
设置接受信息的地址dstva
-
进程进入阻塞状态ENV_NOT_RUNNABLE
-
-
sys_ipc_can_send
-
通过envid找到接收消息的进程
-
value表示传递的信息
-
设置消息来源(进程id)
-
若涉及传递物理页面,srcva != 0,此时需要向被通信进程的dstva映射来自srcva的页面
-
被通信进程就绪ENV_RUNNABLE
-
-
ipc_send
-
不断向消息接收的进程发送消息,直到该进程成功接收
-
-
ipc_recv
-
设置接收消息进程接受信息的地址dstva
-
通过指针传递消息来源(进程id)
-
3. fork函数
对进程用户空间页设置保护时,需要先检查页目录项是否有效,再检查页表项是否有效,否则若先检查页表项,将出现大量pageout。
4. duppage函数
需要写时复制保护的页面:可写&&非共享&&非写时复制,此时需要对父子分别设置PTE_COW位保护(即分别进行map)。
注意进行空间映射时需要先映射子进程,再映射父进程。原因:整个duppage的时间较长,本地运行大概需要几秒的时间,这期间将进行无数次时钟中断,若恰好在map父进程结束而尚未map子进程时发生中断,而这一页又恰好是duppage函数运行所在页,此时回到duppage时继续循环,会对循环变量i进行写操作,于是触发写时复制,为子进程映射了一个新的页,这样结果将与评测机不符。
三、体会与感想
本次lab用时约30h(好久),期间经历了无数的bug,可以说甚至遍历了几乎所有分数组合,当然还有很多地方没有搞懂,估计也搞不懂了,逻辑上似乎没有什么毛病,但运行结果就是奇奇怪怪,而且过不了评测。试图询问助教,但似乎助教de了一天也不知道为什么会有这样的结果,遂改为了大众写法。lab4-2的Extra更是过得非常魔幻,课上逻辑没有什么问题,但就是只能过1个点,甚至本地写了好几个测试程序都能正确运行,后来破釜沉舟改了一下sys_set_env_status之后居然过了,但我非常肯定地觉得这个改后的逻辑不对,所以我非常疑惑,于是又找助教de了一天,依旧无果,摊手。
总之体验并不是很好,感觉bug像个小恶魔趴在暗处盯着我,但我找不出它来,本不想不了了之,但奈何...一群人都不知道为什么,时间有限,只能匆匆开启下一个lab了。
四、残留难点
1. lab4-1
起初我的逻辑与多数人似乎并不相同,我在sys_ipc_recv函数中进程将自己状态设为ENV_NOT_RUNNABLE时将进程从调度队列中移除了,在sys_ipc_can_send函数中将进程状态设为ENV_RUNNABLE时又插入了调度队列,但这样做评测时一直是4/7,不知道为什么。后来只能改成通信时不操作调度队列,让阻塞的进程留在队列里。疑惑。
2. lab4-2
起初我的逻辑是在sys_env_alloc的时候将进程加入调度队列(此时进程为ENV_NOT_RUNNABLE,理论上不会有副作用),而在sys_set_env_status时由于进程已经处于调度队列中,可直接改变进程状态。上机时因为评测始终无法通过,我将逻辑改为仅在sys_set_env_status时将进程插入或移出调度队列,竟然通过了!(事实上,我认为这种做法是不合理的,因为在lab4-1中进程通信时阻塞的队列没有被移出,此时若仅在阻塞变为就绪时将进程插入队列,可能导致同一进程在队列中出现两次?)疑惑。