BUAA-OS-lab4
lab4 实验报告
思考题
Thinking 4.1
内核在保存现场的时候会调用 SAVE_ALL
函数。这个函数的内容如下:
.macro SAVE_ALL
.set noat
.set noreorder
mfc0 k0, CP0_STATUS
andi k0, STATUS_UM
beqz k0, 1f
move k0, sp
/*
* If STATUS_UM is not set, the exception was triggered in kernel mode.
* $sp is already a kernel stack pointer, we don't need to set it again.
*/
li sp, KSTACKTOP
1:
subu sp, sp, TF_SIZE
sw k0, TF_REG29(sp)
mfc0 k0, CP0_STATUS
sw k0, TF_STATUS(sp)
mfc0 k0, CP0_CAUSE
sw k0, TF_CAUSE(sp)
mfc0 k0, CP0_EPC
sw k0, TF_EPC(sp)
mfc0 k0, CP0_BADVADDR
sw k0, TF_BADVADDR(sp)
mfhi k0
sw k0, TF_HI(sp)
mflo k0
sw k0, TF_LO(sp)
sw $0, TF_REG0(sp)
sw $1, TF_REG1(sp)
...
sw $31, TF_REG31(sp)
.set at
.set reorder
.endm
MIPS架构中,通常 k0 和 k1 寄存器被用作临时寄存器,在内核代码中使用,因为它们在异常处理过程中不会由操作系统保存和恢复。在 SAVE_ALL
函数中,k0 寄存器被用来保存临时数据。代码中通过连续的存储指令 sw 将所有的通用寄存器(0-31)保存在栈上,这样在执行异常处理程序时可以恢复这些寄存器的值,从而不会影响当前的程序状态。
可以。因为内核在陷入内核、保存现场的过程中,寄存器 a0-a3 中的值都没有被破坏。
用户在调用 msyscall 时,传入的参数会被保存在 a0-a3 寄存器和堆栈中。当陷入内核时,a0-a3 寄存器不会被破坏,并且会将用户栈中的相应参数复制取出到内核栈中。因此,sys_*
函数可以从寄存器和用户栈处获得用户调用 msyscall 时传入的参数值。
更改了 epc 的值(+4),使得返回用户态后能够正常执行下一条指令;返回值会被存储到 v0 寄存器内,使得返回用户态后可以获取调用后的返回值。
Thinking 4.2
我们在试图通过 envid
来获取对应的进程控制块时,只取了 envid
的后 10 位来作为数组下标,但要确保 envid
和取出的进程块完全对应,仅仅看后 10 位是不够的,因此用 e->env_id != envid
这一步来进一步确定传入的 envid
确实和取出的进程块是对应的。
Thinking 4.3
在系统调用和 ipc 通讯的相关函数里,我们如果传入的 envid
为 0,那么代表相应的进程为 当前进程。因此,需要保证每一个进程块的 envid
不为 0,才能让这个标准正确实现。
Thinking 4.4
正确答案是 C.
fork()
系统调用用于创建一个新进程,这个新进程是调用进程(父进程)的一个副本。fork()
函数只会在父进程中调用一次,但它会返回两次——一次在父进程中,一次在子进程中。
Thinking 4.5
在 0 - USTACKTOP 范围的内存需要使用 duppage 进行映射;
其它范围的内存空间有一部分属于内核,有一部分是无效的内存空间,这些空间都不是用户态下共享可写的,因此不需要重新设为 COW.
Thinking 4.6
vpt
和vpd
分别是指向用户页表和用户页目录的指针。它们用于访问和遍历进程的地址空间中的页表项。以vpt
为例,要获取当前虚拟地址va
所对应的页表项,可以使用vpt[VPN(va)]
,使用宏VPN(va)
获取虚拟地址va
对应的页面的编号,即相对(*vpt)
的偏移量,通过这个偏移量就可以得到对应的页表项。- 存储页表的空间属于用户空间,因此用户进程可以通过指针取得页表的地址来进行访问。系统是线性地进行页面的映射,因此可以很方便地实现“虚拟地址->虚拟页号”的转变。虚拟页号即虚拟地址对应的虚拟页面相对页表项的偏移量,获得了页表首地址和偏移量后,就可以获取对应的页表项。
vpd
的值为(UVPT + (PDX(UVPT) << PGSHIFT))
,通过vpt
来找vpd
,这表明页表vpt
中存在某一页为页目录,由此体现了自映射设计。- 不可以,进程只能读取页表项,不能进行修改。
Thinking 4.7
出现 COW 异常时,也即进程尝试写 COW 页面时,会出现这样的异常重入。
异常处理完毕之后,恢复现场的工作会在用户态进行,因此需要把 tf 保存在用户态的栈空间下。
Thinking 4.8
可以减少内核态和用户态之间的切换次数,从而提高异常处理的效率,并提供了更大的灵活性来直接操作用户空间的数据。
Thinking 4.9
确保在子进程创建过程中,页表项的修改能够被正确地加载到 TLB 中。如果不这样做,可能会导致在子进程创建过程中需要访问的页表项尚未被加载到 TLB 中,从而影响子进程的正常创建和运行。
实验难点分析
lab4 的主要内容主要包括系统调用、进程通信机制(ipc)以及 fork 操作。实验的难点在于对系统调用流程的理解和实现,特别是如何正确地处理用户态和内核态之间的参数传递;以及 fork 创建新进程后,父子进程间的页面权限管理。
在实现系统调用时,首先需要在用户态建立调用 msyscall
函数,然后用汇编语言调用 syscall
,并跳转到内核态的相应位置,再在内核态里完成相应操作。
fork 操作的难点在于理解子进程如何继承父进程的上下文,即,什么样的页面需要被赋予 COW 属性。这一部分代码里的注释描述似乎和指导书里的要求有些许差异,在完成时遇到了一些 bug。
实验体会
lab4 有两次上机,两次上机的 exam 都很快的完成了,4-1 是一个信号量机制,4-2 是一个和 strace 相关的东西,主要难点是进程间通信,而在用户下进行进程间通信又需要进行系统调用,两次 exam 中都需要自己实现系统调用函数。其实整个实现的流程都在题面里给出了,照着做就好,感觉难度不是特别大。但是 lab4 和此前的 lab 最大的不同在于,lab4 里都是在用户态下进行操作,而不是以前的内核态。因此,我们需要明确在用户态下到底什么是可以直接访问和调用的,什么是一定要通过系统调用来执行的。明确了这一点,剩下的事情就只剩照着题意模拟了。