BUAA_OS_Lab4_实验报告

思考题

Thinking 4.1

思考并回答下面的问题:

  • 内核在保存现场的时候是如何避免破坏通用寄存器的?
  • 系统陷入内核调用后可以直接从当时的 $a0-$a3 参数寄存器中得到用户调用 msyscall留下的信息吗?
  • 我们是怎么做到让 sys 开头的函数“认为”我们提供了和用户调用 msyscall 时同样的参数的?
  • 内核处理系统调用的过程对 Trapframe 做了哪些更改?这种修改对应的用户态的变化是什么?
  • 内核在保护现场的时候会调用SAVE_ALL宏,把通用寄存器的sp复制到$k0中。保存现场需要使用$v0作为协寄存器到内存的中转寄存器,写到内存时需要sp,所以在正式保存协寄存器和通用寄存器前先保存这两个寄存器。
  • 可以的。因为内核在陷入内核、保存现场的过程中,寄存器 a0-a3 中的值都没有被破坏。用户在调用 msyscall 时,传入的参数会被保存在 a0-a3 寄存器和堆栈中。当陷入内核时,a0-a3 寄存器不会被破坏,并且会将用户栈中的相应参数复制取出到内核栈中。因此,sys_* 函数可以从寄存器和用户栈处获得用户调用 msyscall 时传入的参数值。
  • 更改了 epc 的值(+4),使得返回用户态后能够正常执行下一条指令;返回值会被存储到 v0 寄存器内,使得返回用户态后可以获取调用后的返回值。

Thinking 4.2

思考 envid2env 函数: 为什么 envid2env 中需要判断 e->env_id != envid 的情况?如果没有这步判断会发生什么情况?

我们在试图通过 envid 来获取对应的进程控制块时,只取了 envid 的后 10 位来作为数组下标。但要确保 envid 和取出的进程块完全对应,所以我们仅仅看后 10 位是不够的。用 e->env_id != envid 这一步来确定传入的 envid 确实和取出的进程块是对应的。如果不判断envid是否相同,会取到错误的或者本该被销毁的进程控制块。

Thinking 4.3

思考下面的问题,并对这个问题谈谈你的理解:请回顾 kern/env.c 文件中 mkenvid() 函数的实现,该函数不会返回 0,请结合系统调用和 IPC 部分的实现与envid2env() 函数的行为进行解释。

在系统调用和 ipc 通讯的相关函数里,我们如果传入的 envid 为 0,那么代表相应的进程为 当前进程。因此,需要保证每一个进程块的 envid 不为 0,才能让这个标准正确实现。

Thinking 4.4

关于 fork 函数的两个返回值,下面说法正确的是:
A、 fork 在父进程中被调用两次,产生两个返回值
B、 fork 在两个进程中分别被调用一次,产生两个不同的返回值
C、 fork 只在父进程中被调用了一次,在两个进程中各产生一个返回值
D、 fork 只在子进程中被调用了一次,在两个进程中各产生一个返回值

C

Thinking 4.5

我们并不应该对所有的用户空间页都使用 duppage 进行映射。那么究竟哪些用户空间页应该映射,哪些不应该呢?请结合 kern/env.cenv_init 函数进行的页面映射、 include/mmu.h 里的内存布局图以及本章的后续描述进行思考。

  • 在 0 - USTACKTOP 范围的内存需要使用 duppage 进行映射;
  • USTACKTOPUTOP 之间的 user exception stack 是用来进行页写入异常的,不会在处理COW异常时调用 fork() ,所以 user exception stack 这一页不需要共享;
  • UTOP以上页面的内存与页表是所有进程共享的,且用户进程无权限访问,不需要做父子进程间的duppage
  • 其上范围的内存要么属于内核,要么是所有用户进程共享的空间,用户模式下只可以读取。除只读、共享的页面外都需要设置 PTE_COW 进行保护。

Thinking 4.6

在遍历地址空间存取页表项时你需要使用到 vpdvpt 这两个指针,请参考 user/include/lib.h 中的相关定义,思考并回答这几个问题:

  • vpt 和 vpd 的作用是什么?怎样使用它们?
  • 从实现的角度谈一下为什么进程能够通过这种方式来存取自身的页表?
  • 它们是如何体现自映射设计的?
  • 进程能够通过这种方式来修改自己的页表项吗?
  1. vptvpd 分别是指向用户页表和用户页目录的指针。它们用于访问和遍历进程的地址空间中的页表项。以 vpt 为例,要获取当前虚拟地址 va 所对应的页表项,可以使用 vpt[VPN(va)],使用宏 VPN(va) 获取虚拟地址 va 对应的页面的编号,即相对 (*vpt) 的偏移量,通过这个偏移量就可以得到对应的页表项。
  2. 存储页表的空间属于用户空间,因此用户进程可以通过指针取得页表的地址来进行访问。系统是线性地进行页面的映射,因此可以很方便地实现“虚拟地址->虚拟页号”的转变。虚拟页号即虚拟地址对应的虚拟页面相对页表项的偏移量,获得了页表首地址和偏移量后,就可以获取对应的页表项。
  3. vpd 的值为 (UVPT + (PDX(UVPT) << PGSHIFT)),在UVPTUVPT + PDMAP之间,说明将页目录映射到了某一页表位置;通过 vpt 来找 vpd,这表明页表 vpt 中存在某一页为页目录,由此体现了自映射设计。
  4. 不能,该区域对用户只读不写,用户进程只能读取页表项,不能进行修改。想要修改需要陷入内核

Thinking 4.7

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

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

出现 COW 异常时,也即进程尝试写 COW 页面时,会出现这样的异常重入。

异常处理完毕之后,恢复现场的工作会在用户态进行,因此需要把 tf 保存在用户态的栈空间下。

Thinking 4.8

在用户态处理页写入异常,相比于在内核态处理有什么优势?

  • 解放内核,不用内核执行大量的页面拷贝工作;
  • 可以减少内核态和用户态之间的切换次数,从而提高异常处理的效率;
  • 内核态处理失误产生的影响较大,可能会使得操作系统崩溃;
  • 用户状态下不能得到一些在内核状态才有的权限,避免改变不必要的内存空间。

Thinking 4.9

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

  • 为什么需要将 syscall_set_tlb_mod_entry 的调用放置在 syscall_exofork 之前?
  • 如果放置在写时复制保护机制完成之后会有怎样的效果?

确保在子进程创建过程中,页表项的修改能够被正确地加载到 TLB 中。如果不这样做,可能会导致在子进程创建过程中需要访问的页表项尚未被加载到 TLB 中,从而影响子进程的正常创建和运行。

实验难点分析

lab4 的主要内容主要包括系统调用、进程通信机制(ipc)以及 fork 操作。我觉得实验的难点在于对系统调用流程的理解和实现。在做实验的时候很容易忘记自己是基于内核还是基于用户。

fork操作的难点在于理解子进程如何继承父进程的上下文,即,什么样的页面需要被赋予 COW 属性。

实验体会

lab4的上机还是比较轻松的。exam大概用了20分钟就写完了,主要就是要理解用户态和内核态函数调用的层次逻辑。extra有点难度,最后只拿了50分。整体感觉下来还是对C语言不够熟悉,有很多代码逻辑不是很清晰。