Post

BUAA-OS-lab4

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

  1. vptvpd 分别是指向用户页表和用户页目录的指针。它们用于访问和遍历进程的地址空间中的页表项。以 vpt 为例,要获取当前虚拟地址 va 所对应的页表项,可以使用 vpt[VPN(va)],使用宏 VPN(va) 获取虚拟地址 va 对应的页面的编号,即相对 (*vpt) 的偏移量,通过这个偏移量就可以得到对应的页表项。
  2. 存储页表的空间属于用户空间,因此用户进程可以通过指针取得页表的地址来进行访问。系统是线性地进行页面的映射,因此可以很方便地实现“虚拟地址->虚拟页号”的转变。虚拟页号即虚拟地址对应的虚拟页面相对页表项的偏移量,获得了页表首地址和偏移量后,就可以获取对应的页表项。
  3. vpd 的值为 (UVPT + (PDX(UVPT) << PGSHIFT)),通过 vpt 来找 vpd,这表明页表 vpt 中存在某一页为页目录,由此体现了自映射设计。
  4. 不可以,进程只能读取页表项,不能进行修改。

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 里都是在用户态下进行操作,而不是以前的内核态。因此,我们需要明确在用户态下到底什么是可以直接访问和调用的,什么是一定要通过系统调用来执行的。明确了这一点,剩下的事情就只剩照着题意模拟了。

This post is licensed under CC BY 4.0 by the author.