1. 思考题

  1. 内核在保存现场的时候,通过 SAVE_ALL 宏操作,来将通用寄存器中的值保存在内核栈指针所指向的位置,在异常处理结束时恢复现场,从而防止通用寄存器的值被破坏改变。

    可以从当时的 $a0 - $a3 寄存器中得到用户调用 msyscall 时传入的参数。因为在系统调用的时候,这几个寄存器的值不会被改变,因此可以直接获取。

    方式就是让sys开头的函数通过正常获取参数的方式,能够获取正确的参数。而这样做的保证是,在用户调用syscall相关函数后,会进行传参,此时会在$a0 - $a3寄存器中保存值,并在用户栈中开辟栈帧,存入相关参数。之后调用msyscall函数,这个函数是个页函数,因此没有栈帧,不会改变栈指针的位置,这个函数直接调用了syscall,陷入了内核态,因此此时 $a0 - $a3 中的值和用户栈中的栈帧跟syscall传参后没有区别,可以通过正常获取参数的方式来获取正确的参数。

    将Trapframe中的epc加了4,也就是使其指向了下一条指令。这样做的变化是,当内核态执行完操作后,会调用eret指令,让PC指向epc,此时用户态的函数,也就是msyscall,就会从jr ra 开始运行,从而返回。否则将会再执行syscall而陷入死循环。


  2. 因为需要保证e所指向的env的id就是envid。如果没有这步判断,那么说明 e = &envs[ENVX(envid)] 所找到的env的id不是envid,也就是这一步的实现有错。

  3. 以上是该函数的具体实现,可以看到该函数中有一个静态全局变量 i,该变量初始值是 0,之后每次 make 新 id 的时候,该值就会++,并存储在 id 的高位。由于 i 初始值就是0,因此第一次调用之后一直都是大于 0 的。因此 id 不会是 0,mkenvid 不会返回 0。


  4. 正确答案应该是C


  5. 仅需映射 0 - USTACKTOP 的部分即可。对于 USTACKTOP 以上的部分,分别是异常处理栈、全局 pages 和 envs 、vpt,这些部分不需要映射。


  6. vpd 是页目录的首地址,vpt是所有页表的首地址。使用方式就是通过下表索引机制。比如想要访问页目录中的第一个页目录项,那么这一项的值就是 vpd[1]。

    在 entry.S 中,这两个变量被设置为了 .globl ,也就是说对于用户进程,这两个变量就是全局变量,可以直接访问。

    vpt = UVPT;
    vpd = UVPT | (UVPT >> 10);
    这里体现出了页目录自映射机制。

    不能,因为用户没有权限去修改自己的页目录项或者页表项,其权限位PTE_R是0。


  7. 当用户态的程序尝试对没有写入权限的页进行写入时,将会触发异常,并经过异常分发来到handle_tlb_mod,其调用了do_tlb_mod函数。也就是说在这种情况下会进行异常重入。

    因为我们的MOS操作系统采用微内核设计,将大部分的操作移至用户态进行,因此需要把异常现场复制到用户空间来让用户态的函数去访问。


  8. 如果发生崩溃,整个操作系统不会跟着崩溃,可以防止潜在的崩溃隐患。


  9. 因为在父进程创建子进程的时候,有可能发生缺页中断,此时也需要进行异常处理。因此父进程需要知道异常处理的函数的入口地址,这样才能在发生异常的时候跳转到cow_entry函数去处理。

    如果防止在其之后,那么fork出来的子进程读取到的就是修改后的页面,而不是原来的页面。

2. 难点分析

  1. 本次作业中先让我们填写了很多内核态的系统调用函数,并理解了系统调用的过程,之后让我们了解了IPC机制,进程间是如何互相通信的。最后让我们实现了fork函数。整个过程内容较多,需要我们好好理解其中的细节。

  2. 首先,如果系统触发了异常,那么CPU会自动跳转到异常处理入口地址。之后会先调用SAVE_ALL宏操作来保存当前上下文,之后经过exception_handler异常分发到对应的异常处理函数。如果是系统调用异常,那么会跳转到handle_sys,这里会继续调用do_syscall,在这里会先进行epc+4,让返回的PC指向下一条指令,防止重复syscall,之后通过异常调用码来找到对应的异常调用函数,并传入相关参数。最后在执行完内核操作后,调用ret_from_exception,返回用户态。

  3. 进程间通信机制,即ipc,依赖于进程控制块内的信息存储和共享页面机制。接收进程可以调用recv函数,设置自己状态并让出CPU。发送进程可以调用sending去给接收进程发送信息。有两种信息传输途径,发送进程可以把信息保存到接收进程的env中,同时可以通过共享页面操作让两个进程访问相同的内存。

  4. fork机制可以让一个进程创造出一个子进程。二者共享代码段、数据段,同时还保留了解释复制机制。

3. 实验体会

  1. 本次实验需要认真体会,好好复习。首先要理解异常处理流程,明确系统调用和陷入机制。我们在实验中填写了众多系统调用函数,从而让用户态的进程可以调用这些系统调用函数,完成相关的操作。

  2. fork操作需要先设置父进程的写时复制处理函数入口地址,这样父进程在接下来的fork操作时如果发生了写入异常,那么可以跳转到cow_entry函数完成写时复制操作。之后调用syscall_exoenv来创建子进程的进程控制块。之后需要将父进程的页面映射到子进程中,并把PTE_D有效同时PTE_COW有效的页面设置成PTE_D为0,这样进程在写这样的页面时会触发异常,从而分配出新的一页,不妨碍另外一个进程读页面。最后再设置状态并加入调度队列即可。

4. 原创声明

本实验报告均为原创。