Skip to main content

OS Lab4 实验报告

一、实验思考题

Thinking 4.1

思考并回答下面的问题:

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

答:

1)内核会通过宏SAVE_ALL以与struct Trapframe等同的结构将运行现场保存到内核空间中,Trapframe包括32个通用寄存器、部分CP0寄存器、HI/LO乘除法寄存器和PC指令计数器的值。

2)不可以,系统陷入内核调用后可能会破坏a0a0-a3 参数寄存器的值(虽然本次实验好像没有破坏)

3)在MIPS 的调用规范中,如果函数参数个数≤4,则将参数依次存入a0-a3寄存器中,并在栈帧底部保留16字节的空间(即sp的值减去16),但并不一定使用这些空间。如果函数参数个数>4,则前4个参数依次存入a0-a3寄存器中,从第5个参数开始,依次在前4个参数预留空间之外的空间内存储,即没有寄存器去保存这些值。在handle_sys中,我们从sp中取出a0-a3的值,从用户态栈指针取出其他参数,让sys 开头的函数“认为”我们提供了和用户调用msyscall 时同样的参数。

4)将Trapframe中EPC的值+4后存回,当程序返回用户态时从导致陷入内核态的下一条指令开始执行;将系统调用的返回值存入v0寄存器中,由用户态函数调用。

Thinking 4.2

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

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

答:

1)说明子进程和父进程有相同的代码段。

2)说明fork()后子进程的PC值与父进程执行fork()后的PC值相同。

Thinking 4.3

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

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

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

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

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

Thinking 4.4

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

答:

 /*
o UTOP,UENVS -----> +----------------------------+------------0x7f40 0000 |
o UXSTACKTOP -/ | user exception stack | BY2PG |
o +----------------------------+------------0x7f3f f000 |
o | Invalid memory | BY2PG |
o USTACKTOP ----> +----------------------------+------------0x7f3f e000 |
o | normal user stack | BY2PG |
o +----------------------------+------------0x7f3f d000 |
*/

在[0,UTOP)的用户空间中,[USTACKTOP,USTACKTOP+BY2PG)是无效内存,子进程缺页中断时作为临时空间,不需要保护;[USTACKTOP+BY2PG,UXSTACKTOP)是错误栈,不需要保护,否则父子进程错误栈相同,会陷入死循环中。

另外在duppage函数内部,对不同权限的页有着不同的处理方式。对于只读、共享、写时复制保持其原有的权限即可,对于可写页面则需要对父子进程的页表项加上PTE_COW 标记。

Thinking 4.5

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

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

答:

1)在user/entry.S中可以看到定义:

    .globl vpt
vpt:
.word UVPT

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

因此*vpt是指向用户页表的指针,而根据页表的自映射机制,*vpd是指向用户页目录的指针。

使用方式:可以将其作为数组使用,获得虚拟地址va对应的页表项和页目录项。

(*vpt)[va >> PGSHIFT]       // 获得对应的页表项
(*vpd)[va >> PDSHIFT] // 获得对应的页目录项

2)在fork函数中,我们需要检查页表项是否有效,进而进行duppage的操作。通过vpt 和vpd访问的方式,为用户态提供了访问页表的途径。

3)vpd的地址为(UVPT+(UVPT>>12)*4),是页表项,也是页目录项,体现了自映射设计。

4)不能,这里只能通过这种方式读取页表信息,进而判断是否有效,不可以修改页表。

Thinking 4.6

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

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

答:

1)在处理缺页中断时接受了时钟中断,就会出现这种“中断重入”。

2)将异常的现场Trapframe 复制到用户空间,之后可以在用户态的pgfault函数进行缺页处理,恢复事先保存好的现场与sp寄存器的值,使得子进程恢复执行。

Thinking 4.7

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

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

答:

1)减少了陷入内核态后的工作量,体现了微内核的思想,将通常与内核集成在一起的系统服务层被分离出来,变成可以根据需求加入的选件,提供更好的可扩展性和更加有效的应用环境。

2)恢复现场时,栈指针是由内核设置的在异常处理栈的栈指针,而且指向一个由内核复制好的Trapframe 结构体的底部,从栈中取出除了sp以外的通用寄存器的值,最后利用了MIPS 的延时槽特性跳转的同时恢复了栈指针。

Thinking 4.8

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

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

答:

1)在syscall_env_alloc时会产生缺页中断,需要先设置好处理函数。

2)写时复制保护设置中syscall_mem_map也要用到缺页中断,会导致duppage无法正常执行。

3)不需要,父进程设置的set_pgfault_handler改变的__pgfault_handler与子进程共享。

二、实验难点图示

在这次实验,需要实现系统调用机制,并在此基础上实现进程间通信(IPC)机制和一个重要的系统调用fork。

首先的难点在于MIPS传参机制(在Extra,我由于没有深入理解这一点而吃了很大的苦头),而fork函数同样是一大难点,在什么时候要为父子进程设置什么、返回什么,课程组很贴心的给了一张流程图:

而我自己的难点树如下:

![](G:\OneDrive\OneDrive - buaa.edu.cn\MWD\学习\操作系统\作业\Refer\Lab4 个人难点.png)

三、体会与感想

Lab4难度评价:★★★★☆

Lab4-extra难度评价:★★★★★

花费时间:Lab4 10h,Lab4-extra 10h

体会和感想:

Lab4-extra基础也太难了……好久没有写汇编,我de了五个小时的Bug,听同学说这是去年的Exam-1,我感觉是不是应该庆幸今年是网课……

在这过程中,我发现一些之前实现过的函数还存在着一些Bug,之后可以的话希望重新过一遍之前的Lab,顺便打上中文注释……

最后感谢好多学长的博客(是的我就是灌水群里分享了一堆博客的那位),在一步步理解的时候,指导书和注释还是比较抽象的,感谢学长们的博客(不过大家想的都不一样hhh)

note

这是一篇从Hexo迁移的文章,创建于2020-05-11 00:43:36