lab4——系统调用与fork

思考题

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

  • 内核在保存现场的时候是如何避免破坏通用寄存器的?

通过SAVE_ALL将所有通用寄存器的值存入sp中

  • 系统陷入内核调用后可以直接从当时的$a0-$a3 参数寄存器中得到用户调用msyscall 留下的信息吗?

可以

  • 我们是怎么做到让sys 开头的函数“认为”我们提供了和用户调用msyscall 时同样的参数的?

前四个参数放在对应的寄存器上,后两个参数存在栈上的相同位置

  • 内核处理系统调用的过程对Trapframe 做了哪些更改?这种修改对应的用户态的变化是?

修改了EPC的值,用户态返回的时候可以继续执行下一条指令

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

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

说明子进程的代码段和父进程是一样的

说明子进程恢复到的上下文位置是fork函数

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

A. fork 在父进程中被调用两次,产生两个返回值
B. fork 在两个进程中分别被调用一次,产生两个不同的返回值
C. fork 只在父进程中被调用了一次,在两个进程中各产生一个返回值
D. fork 只在子进程中被调用了一次,在两个进程中各产生一个返回值

当然只完成子进程部分,子进程还不能正常跑起来,父亲在儿子醒来之前则需要做更多的准备,而这些准备中最重要的一步是遍历进程的大部分用户空间页,对于所有可以写入的页面的页表项,在父进程和子进程都加以PTE_COW 标志位保护起来。这里需要实现duppage 函数来完成这个过程。

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

从0到USERSTACKTOP的地址空间里,对于不是共享的和只读的页面可以保护起来。

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

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

vpt和vpd分别指向页表项和页目录所在的虚拟地址,可以通过取他们的内容来获得相应的指针

因为vpt和vpd通过宏定义对应了内存中页表所在的虚拟地址

vpd:(UVPT+(UVPT>>12)*4)

可以

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

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

当有COW的页面被修改的时候

因为需要在用户态处理异常

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

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

体现了微内核的思想,让用户进程实现内核的功能,就算内核出了问题操作系统也能运行

通过把所有通用寄存器压入栈中,在使用时取出

难点

Exercise 4.1 填写user/syscall_wrap.S 中的msyscall 函数,使得用户部分的系统调用机制可以正常工作。

通过特权指令syscall陷入内核态,之后jr返回即可(注意延迟槽)

Exercise 4.2 按照lib/syscall.S 中的提示,完成handle_sys 函数,使得内核部分的系统调用机制可以正常工作。

第一步:从TF中取出EPC的值,加4之后放回TF中,使得异常处理结束后可以继续执行下一条指令

第二步:从TF中读取a0的值(系统调用号)

第三步:在内核栈上分配储存六个参数的空间,并且把六个参数放到正确的位置

注意在mips中按照规定,前四个参数并不放在栈上,而是直接留在寄存器中

第四步:恢复内核栈的位置

Exercise 4.3 实现lib/syscall_all.c 中的int sys_mem_alloc(int sysno,u_int envid,u_int va, u_int perm) 函数

先判断错误情况:va超出了用户空间(涉及了内核空间),试图用COW作为perm,或者perm中没有V,此时返回-E_INVAL

然后通过envid2env得到对应的进程,用page_alloc和page_insert分配和插入页面

注意每一步都需要检测是否出现返回值不正常的情况,如果有要继续传递出去

Exercise 4.4 实现lib/syscall_all.c 中的int sys_mem_map(int sysno,u_int srcid,u_int srcva, u_int dstid, u_dstva, u_int perm) 函数

先判断错误情况:srcva和dstva是否超出了用户空间

通过envid2env得到对应进程,page__lookup查找src中的页面,page_insert插入到dst中

Exercise 4.5 实现lib/syscall_all.c 中的int sys_mem_unmap(int sysno,u_int envid,u_int va) 函数

先判断错误情况:va是否超出了用户空间

通过envid2env得到对应进程,page_remove解除映射关系

Exercise 4.6 实现lib/syscall_all.c 中的void sys_yield(void) 函数

该函数用于当前进程主动放弃cpu,因为lab3中编写的env_run中切换进程是从TIMESTACK保存的通用寄存器等信息,所以在调用调度算法切换进程之前,需要从内核栈KERNEL_SP中把Trapframe拷贝到TIMESTACK的相应位置。

Exercise 4.7 实现lib/syscall_all.c 中的void sys_ipc_recv(int sysno,u_int dstva)函数和int sys_ipc_can_send(int sysno,u_int envid, u_int value, u_int srcva,u_int perm) 函数。

sys_ipc_recv负责将当前进程设置成准备好接受ipc的状态,写入接受的va地址,并且改变env状态使其暂停执行,然后调用sys_yield主动放弃cpu

sys_ipc_can_sen首先检测目标env是否处于准备好接受ipc信息的状态,如果不是返回对应错误

只有srcva不是0的情况下才调用page__lookup和page_insert传递信息

最后把接受信息的进程env信息修改即可 perm要赋值!!!

Exercise 4.8 填写lib/syscall_all.c 中的sys_env_alloc 函数

这个函数用于创建子进程。需要把父进程中的信息(Trapframe pri)传递给子进程。

因为子进程还不能执行,状态要设置为不能运行;同时为了结束异常后能够返回到正确的上下文位置,需要把tf中pc更新为epc的值。

注意tf中reg2(代表返回值的寄存器)要设置为0,用于实现父子进程调用sys_env_alloc返回不同的值,以便fork进行区分。

Exercise 4.9 填写user/fork.c 中的fork 函数中关于sys_env_alloc 的部分和“子进程”执行的部分

通过sys_env_alloc得到两种返回值:父进程返回子进程的id,子进程返回0

当返回值为0时,子进程通过syscall_getenvid()得到自己的id,并且将env设置为对应的进程指针

Exercise 4.10 结合注释,填写user/fork.c 中的duppage 函数

这一函数主要实现了子进程页表的“复制”。说是复制,但是实际上只是让父子进程共享相同的物理页面而已。

这里的难点一个在于怎么获取页表的perm信息,一个在于怎么根据perm信息对子进程的perm进行赋值。

perm = ((Pte*)(*vpt))[pn] &0xfff其实就是取得页表vpt中第pn项,然后看它低位的值

子进程赋值大概分为以下几种情况:

无效——报错

只读——直接给相同perm

共享——给相同的perm

COW——给相同的perm

可写——父子进程都追加COW

修改perm通过syscall_mem_map即可

Exercise 4.11 完成lib/traps.c 中的page_fault_handler 函数,设置好异常处理栈以及epc寄存器的值。

把epc设置为对应进程page_fault_handler 的指针即可,这样从异常返回后就会进入异常处理函数

Exercise 4.12 完成lib/syscall_all.c 中的sys_set_pgfault_handler 函数

把对应env的page_fault_handler 和xstack设置成对应值即可

Exercise 4.13 填写user/fork.c 中的pgfault 函数

这个函数用于处理COW处罚的中断

va对页大小取整,在USTACKTOP上面invalid的区域分配一块临时区域,用于拷贝va处的信息

之后把这段信息插入到子进程的对应va上

最后取消tmp在页表中的映射关系

Exercise 4.14 填写lib/syscall_all.c 中的sys_set_env_status 函数

判断status是否合法,否则报错

设置env为对应的status,根据情况插入或者移出就绪队列

Exercise 4.15 填写user/fork.c 中的fork 函数中关于“父进程”执行的部分

难点中的难点

首先需要通过set_pgfault_handler设置异常处理函数,然后syscall_env_alloc创建子进程

之后根据返回值的不同分别执行父子进程的操作

父进程要在循环中遍历用户空间页表中的每一项,对于页目录和页表都有效的,复制给子进程

然后分配空间给异常处理栈,设置__sam_pgfault_handler,最后改变子进程状态让它运行起来

感悟

这次实验上来就把我难住了。对于在用户态和内核态之间的切换,还有各种寄存器、堆栈、数据的设置都弄得不是很清楚。导致很长一段时间里程序连正常执行都做不到,debug也无从下手。琢磨了很长时间才把前两个练习填对。

系统调用和ipc部分倒是还算比较快的做完了,接下来的fork又是一大难点。

现在回过头再去看填的这些函数,我感觉当时的理解其实也没有错。问题就在于,理解该怎么做,和知道具体怎么实现是两回事。我知道需要获取页表的信息来判断是不是需要添加COW,需不需要通过duppage进行复制,但是我并不太清楚从哪得到页表的信息。

通过grep查看定义知道了vpt和vpd的作用,但是实际使用的过程中怎么做都不对。最后请教同学才发现,取完vpt和vpd之后加上对应指针的强制类型转换才行。但是定义的时候vpt已经是Pte*类型的了,我以为不需要转换也应该是对的,最后也不知道为什么。

这次实验卡在最后的时间才做完实在惊险,六天前就写完了所有内容,没想到debug居然就用了整整5天,几乎都没做其他事情。下一个实验一定要早点开始做,避免重蹈覆辙。

残留难点

((Pte*)(*vpt))[n](*vpt)[n]为什么会结果不同?vpt不是已经定义了是Pte*类型的数组了吗?

posted @ 2020-08-26 09:54  bl水滴  阅读(1254)  评论(0编辑  收藏  举报