OS-lab4实验报告

lab4实验报告

一、实验思考题

Thinking 4.1 思考并回答下面的问题:

  • 内核在保存现场的时候是如何避免破坏通用寄存器的?
  • 系统陷入内核调用后可以直接从当时的$a0-$a3 参数寄存器中得到用户调用msyscall 留下的信息吗?
  • 我们是怎么做到让sys 开头的函数“认为”我们提供了和用户调用msyscall 时同样的参数的?
  • 内核处理系统调用的过程对Trapframe 做了哪些更改?这种修改对应的用户态的变化是?

答:1、内核会通过调用SAVE_ALL把所有通用寄存器的值存入栈中进行维护

2、可以,a0-a3寄存器的值没有被改变

3、从内核态的栈中取出a0-a3寄存器的值,再从用户态的栈中取出其余两个参数,放到内核态的栈中

4、做了两件事:把epc的值加4,把系统调用的返回值储存在v0当中。使得返回用户态后可以继续执行下一条语句,可以取出得到系统调用的返回值

Thinking 4.2 思考下面的问题,并对这两个问题谈谈你的理解:

  • 子进程完全按照fork() 之后父进程的代码执行,说明了什么?
  • 但是子进程却没有执行fork() 之前父进程的代码,又说明了什么?

答:1、说明子进程的代码段和父进程是相同的

2、说明子进程被创建时,PC值等状态与父进程一致

Thinking 4.3 关于fork 函数的两个返回值,下面说法正确的是:

A. fork 在父进程中被调用两次,产生两个返回值

B. fork 在两个进程中分别被调用一次,产生两个不同的返回值

C. fork 只在父进程中被调用了一次,在两个进程中各产生一个返回值

D. fork 只在子进程中被调用了一次,在两个进程中各产生一个返回值

答:C

Thinking 4.4 如果仔细阅读上述这一段话, 你应该可以发现, 我们并不是对所有的用户空间页都使用duppage 进行了保护。那么究竟哪些用户空间页可以保护,哪些不可以呢,请结合include/mmu.h 里的内存布局图谈谈你的看法。

答:从上到下来说明

1、UTOP以上的地址空间对于用户进程是不可修改的,不需要保护

2、UTOP — UTOP - BY2PG的地址空间是user exception stack,即用户进程的异常栈。子进程和父进程的调度情况不同,应当拥有各自的异常栈,所以不应该保护

3、UTOP - BY2PG — USTACKTOP这部分地址空间是invalid memory即无效内存,不需要保护

4、USTACKTOP — UTEXT这部分地址空间中的页面中,对于不是只读的页,可以进行保护

Thinking 4.5 在遍历地址空间存取页表项时你需要使用到vpd 和vpt 这两个“指针的指针”,请思考并回答这几个问题:

  • vpt 和vpd 的作用是什么?怎样使用它们?
  • 从实现的角度谈一下为什么能够通过这种方式来存取进程自身页表?
  • 它们是如何体现自映射设计的?
  • 进程能够通过这种存取的方式来修改自己的页表项吗?

答:1、vpt是指向二级页表的指针,vpd是指向一级页表(页目录)的指针。用法即通过虚拟地址va,得到va对应的页表项(*vpt)[va >> 12]和页目录项(*vpd)[va >> 22]

2、在mmu.h中可以看到下面两行代码:vpt的类型为Pte数组,vpd的类型为Pde数组

extern volatile Pte *vpt[];
extern volatile Pde *vpd[];

entry.S中有下面几行代码:vpt指向UVPT区域,vpd指向(UVPT+(UVPT>>12)*4)区域

	.globl vpt
vpt:
	.word UVPT
	
	.globl vpd
vpd:
	.word (UVPT+(UVPT>>12)*4)

3、vpt指向UVTP区域,而UVPT + UVPT >> 10正是vpd指向的地址,实现了自映射

4、不能

Thinking 4.6 page_fault_handler 函数中,你可能注意到了一个向异常处理栈复制Trapframe 运行现场的过程,请思考并回答这几个问题:

  • 这里实现了一个支持类似于“中断重入”的机制,而在什么时候会出现这种“中断重入”?
  • 内核为什么需要将异常的现场Trapframe 复制到用户空间?

答:1、发生中断处理的过程中,再次发生中断

2、我们是在用户进程中处理此缺页中断的,所以用户进程需要读取Trapeframe的值来处理中断和中断结束后恢复现场,所以需要复制到用户空间

Thinking 4.7 到这里我们大概知道了这是一个由用户程序处理并由用户程序自身来恢复运行现场的过程,请思考并回答以下几个问题:

  • 用户处理相比于在内核处理写时复制的缺页中断有什么优势?
  • 从通用寄存器的用途角度讨论用户空间下进行现场的恢复是如何做到不破坏通用寄存器的?

答:1、体现“微内核”思想,减少内核的工作量,加快内核运行速度

2、将通用寄存器的值压入栈中,使用时再取出:取寄存器值时先取除sp寄存器以外的其他统用寄存器的值,再取sp寄存器的值

Thinking 4.8 请思考并回答以下几个问题:

  • 为什么需要将set_pgfault_handler 的调用放置在syscall_env_alloc 之前?
  • 如果放置在写时复制保护机制完成之后会有怎样的效果?
  • 子进程需不需要对在entry.S 定义的字__pgfault_handler 赋值?

答:1、因为syscall_env_alloc过程中同样可能产生缺页中断

2、导致写时复制保护机制无法正常运行,缺页错误无法处理

3、不需要,子进程与父进程中的__pgfault_handler值相同


二、实验难点图示

1、系统调用

这一部分的难点主要在于理解系统调用的实现过程

系统调用的实现过程如下:

  • 调用一个封装好的用户空间的库函数(如writef
  • 调用用户空间的syscall_*函数
  • 调用msyscall,用于陷入内核态
  • 陷入内核,内核取得信息,执行对应的内核空间的系统调用函数(sys_*
  • 执行系统调用,并返回用户态,同时将返回值“传递”回用户态
  • 从库函数返回,回到用户程序调用处

另外,系统调用的过程中需要使用汇编函数进行跳转,上学期计组的学习过程中,我们所使用的一切寄存器都是自己使用和维护的,对于寄存器规则这方面是比较弱化的,但是操作系统实验中就不一样了,我们需要很清楚寄存器的使用规则,谨慎使用寄存器:

  • V0 - V1会存储函数返回值;
  • A0 - A3会存储函数传入值(剩余传值在栈里)
  • S0 - S7在调用函数前后不变
  • SP存放栈值
  • RA存放返回地址

2、duppage函数的实现

duppage函数的主要问题有两个,一个是如何获取页面perm,另一个是根据页的perm的具体情况可能需要改变perm。第一个问题比较简单perm = (*vpt)[pn] & 0xfff,第二个问题要分情况讨论,主要有以下几种情况:

  • 只读页面——给相同perm
  • 共享页面——给相同的perm
  • 写时复制页面——给相同的perm
  • 可写页面——父子进程的权限都追加PTE_COW

3、fork函数的实现

fork函数应该是本次实验中最难的函数了,一方面是需要我们了解fork函数的执行过程,另一方面是fork函数调用了很多函数,我们需要先保证这些函数的正确性,可以说fork函数把整个Lab4的内容串起来了。

fork函数的执行流程图如下

具体实现如下:

int
fork(void)
{
	// Your code here.
	u_int newenvid;
	extern struct Env *envs;
	extern struct Env *env;
	u_int i;

	//The parent installs pgfault using set_pgfault_handler
	set_pgfault_handler(pgfault);

	//alloc a new alloc
	if ((newenvid = syscall_env_alloc()) == 0) {
		env = &envs[ENVX(syscall_getenvid())];
		return 0;
	}

	for (i = 0; i < USTACKTOP; i += BY2PG) {
		if (((*vpd)[i >> PDSHIFT] & PTE_V) && ((*vpt)[i >> PGSHIFT] & PTE_V)) {
			duppage(newenvid, i >> PGSHIFT);
		}
	}

	if(syscall_mem_alloc(newenvid, UXSTACKTOP - BY2PG, PTE_V | PTE_R) < 0){
		user_panic("failed alloc UXSTACK.\n");
	}

	if(syscall_set_pgfault_handler(newenvid, __asm_pgfault_handler, UXSTACKTOP) < 0){
		user_panic("page fault handler setup failed.\n");
	}

	if(syscall_set_env_status(newenvid, ENV_RUNNABLE) < 0) {
		user_panic("syscall_set_env_status failed~!");
	}

	return newenvid;
}

三、体会与感想

这次实验一开始并没有很担心,因为听说lab3是最难的,再加上系统调用和IPC部分相对来说还是比较简单的,在五一之前就已经写完了,于是就放松了警惕,然后就被fork打了一个措手不及。

系统调用和IPC部分,即便没有整体了解整个lab也可以比较顺利地完成,按着指导书一步一步写就行了。但是到了fork部分,难度直接起飞了Orz,因为这部分是一个整体,一个fork函数就几乎串联起来整个lab4的内容,这要求我们对于实现原理和执行流程非常清楚,并且不能出错,需要填写的东西很多,分布在很多的文件中,写函数时我们需要很清楚函数是用户态还是内核态,是执行流程的哪一部分,先把握好基础的东西,再去写。

还有,到了lab4,祖传bug一个接一个出现在完全想不到的地方,尽管我十分确信在之前的lab中都尽量做到正确,不留bug,但是很遗憾,还是没能拦住bug的出现。这些bug的出现侧面表明评测机不会很完善地检测我们每次编写的所有函数,可能有些函数根本就没有测试点,所以需要我们写的时候小心小心再小心。

还有就是,感觉lab的难度越来越大了,无论是对代码的阅读还是对于整体流程的理解(还有万恶的祖传bug呜呜)。

posted @ 2021-05-20 14:42  神樂坂清清  阅读(212)  评论(0编辑  收藏  举报