OS

OS lab4

Posted by Steel Shadow on May 12, 2023

思考题

  1. 思考并回答下面的问题:

    • 内核在保存现场的时候是如何避免破坏通用寄存器的?

      进入内核态后,使用 SAVE_ALL (include/stackframe.h) 将原 sp 保存在 k0 中,再为 sp 赋为 KSTACKTOP。
      将 k0(保存着原 sp)保存至 KSTACKTOP正确位置,将其余通用寄存器保存至栈帧地址。k0 无需保护,为操作系统寄存器。

    • 系统陷入内核调用后可以直接从当时的 $a0-$a3 参数寄存器中得到用户调用 msyscall 留下的信息吗?

      可以,msyscall 调用 syscall 的过程中,$a0-$a3 的值没有发生变化。

    • 我们是怎么做到让 sys 开头的函数“认为”我们提供了和用户调用 msyscall 时同样的参数的?

      syscall_* 有最多5个参数,其内部调用 msyscall。

      msyscall 直接调用 syscall 指令并返回。msyscall 的第1个参数(a0)为系统调用号(与特定的 syscall_* 对应),还有其余最多 5 个参数。

      异常处理代码段 exc_gen_entry 中, SAVE_ALL 保存用户态上下文至 KSTACKTOP 以下,随后跳转到 exception_handler[8] 对应的 handle_sys。

      handle_sys 内设置参数 a0 为 sp,再调用 do_syscall。

      do_syscall 的唯一参数 a0 为栈帧 struct Trapframe *tf (SAVE_ALL后,sp = KSTACKTOP - TF_SIZE)。为了执行内核态函数 sys_,读取用户态栈帧,取出系统调用号和其余5个参数,执行 sys_(arg1-5)。

      sys_* 结束 -> do_syscall 结束 -> handle_sys 继续。handle_sys 继续调用 ret_from_exception,使用栈帧恢复用户态上下文,系统调用完全结束。

    • 内核处理系统调用的过程对 Trapframe 做了哪些更改?这种修改对应的用户态的变 化是什么?

      在 do_syscall 中,

      • 若系统调用号无效则修改 Trapfram reg[2] v0,用户态得到返回值为 -E_NO_SYS = -5。

      • tf->cp0_epc += 4; 从系统调用处的下一条指令开始执行。

      • 正常情况下,tf->regs[2] 为 sys_* 返回值,用户态得到该返回值。

      1
      2
      3
      4
      5
      6
      
        if (sysno < 0 || sysno >= MAX_SYSNO) {
            tf->regs[2] = -E_NO_SYS;
            return;
        }
        ... ...
        tf->regs[2] = func(arg1, arg2, arg3, arg4, arg5);  //$v0
      
  2. 思考 envid2env 函数: 为什么 envid2env 中需要判断 e->env_id != envid 的情况?如果没有这步判断会发生什么情况?

    env_id 低 10 位是进程在envs中的偏移量,高位是总进程调用次数。

    如果不进行这一步判断,通过 &envs[ENVX(envid)] 得到的进程可能不是预期的(总调用次数可能不同)。

    1
    2
    3
    4
    
     u_int mkenvid(struct Env* e) {
         static u_int i = 0;
         return ((++i) << (1 + LOG2NENV)) | (e - envs);
     }
    
  3. 思考下面的问题,并对这个问题谈谈你的理解:请回顾 kern/env.c 文件中 mkenvid() 函数的实现,该函数不会返回 0,请结合系统调用和 IPC 部分的实现与 envid2env() 函数的行为进行解释。

    mkenvid() 中, env_id 低 10 位是进程在envs中的偏移量,高位是总进程调用次数, env_id 不可能为0。

    envid2env() 中,envid == 0 则找到当前进程 curenv。

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

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    
     /*
     o      ULIM     -----> +----------------------------+------------0x8000 0000-------
     o                      |         User VPT           |     PDMAP                /|\
     o      UVPT     -----> +----------------------------+------------0x7fc0 0000    |
     o                      |           pages            |     PDMAP                 |
     o      UPAGES   -----> +----------------------------+------------0x7f80 0000    |
     o                      |           envs             |     PDMAP                 |
     o  UTOP,UENVS   -----> +----------------------------+------------0x7f40 0000    |
     o  UXSTACKTOP -/       |     user exception stack   |     BY2PG                 |
     o                      +----------------------------+------------0x7f3f f000    |
     o                      |                            |     BY2PG                 |
     o      USTACKTOP ----> +----------------------------+------------0x7f3f e000    |
     o                      |     normal user stack      |     BY2PG                 |
     o                      +----------------------------+------------0x7f3f d000    |
     a                      |                            |                           |
     a                      ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~                           |
     a                      .                            .                           |
     a                      .                            .                         kuseg
     a                      .                            .                           |
     a                      |~~~~~~~~~~~~~~~~~~~~~~~~~~~~|                           |
     a                      |                            |                           |
     o       UTEXT   -----> +----------------------------+------------0x0040 0000    |
     o                      |      reserved for COW      |     BY2PG                 |
     o       UCOW    -----> +----------------------------+------------0x003f f000    |
     o                      |   reversed for temporary   |     BY2PG                 |
     o       UTEMP   -----> +----------------------------+------------0x003f e000    |
     o                      |       invalid memory       |                          \|/
     a     0 ------------>  +----------------------------+ ----------------------------
     */
    

    ULIM 以下为 kuseg 用户进程地址空间部分。 UTOP ~ ULIM 以上为用户进程 env_alloc 时已经映射的内存,不用 fork 映射。

    UXSTACKTOP-BY2PG ~ UXSTACKTOP 为用户异常处理栈,用于 写时复制 do_tlb_mod。不可映射。

    USTACKTOP 到 USTACKTOP + BY2PG 在 mmu.h 中未映射。

    其余的 0 ~ USTACKTOP 都需要映射(如果valid)。

  6. 在遍历地址空间存取页表项时你需要使用到 vpd 和 vpt 这两个指针,请参 考 user/include/lib.h 中的相关定义,思考并回答这几个问题:
    • vpt 和 vpd 的作用是什么?怎样使用它们?

      1
      2
      
        #define vpt ((volatile Pte *)UVPT)
        #define vpd ((volatile Pde *)(UVPT + (PDX(UVPT) << PGSHIFT)))
      

      vpt 是用户地址空间中的页表起始地址。
      vpt[index] 为页号 index 对应的表项。
      vpd 是用户地址空间的一级自映射页表的第一项地址。
      vpd[index] 为一级页表号对应的二级页表。

    • 从实现的角度谈一下为什么进程能够通过这种方式来存取自身的页表?

      页表自映射,不再赘述。

    • 它们是如何体现自映射设计的?

      vpd 是自映射页目录的基地址。

    • 进程能够通过这种方式来修改自己的页表项吗?

      不可以,用户态无法修改页表项。需要使用系统调用,内核态内部可修改。

  7. 在 do_tlb_mod 函数中,你可能注意到了一个向异常处理栈复制 Trapframe 运行现场的过程,请思考并回答这几个问题:
    • 这里实现了一个支持类似于“异常重入”的机制,而在什么时候会出现这种“异常重入”?

      在处理缺页异常的时候,又出现了缺页异常,则会产生“异常重入”。

    • 内核为什么需要将异常的现场 Trapframe 复制到用户空间?

      用户空间需要根据 Trapframe 恢复现场。

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

    微内核设计思想。

  9. 请思考并回答以下几个问题:
    • 为什么需要将 syscall_set_tlb_mod_entry 的调用放置在 syscall_exofork 之前?

      保证子进程在创建时,就必定和父进程一样拥有缺页处理函数。
      防止子进程创建后,进程立即 时钟中断 切换至子进程(系统调用会被时钟中断打断吗?),子进程还未设置缺页处理函数出错。 此外,syscall_env_alloc 过程中亦可能需要进⾏异常处理。
      我觉得,可以在 env_alloc 进程创建的时候就设置 tlb_mod_entry。

    • 如果放置在写时复制保护机制完成之后会有怎样的效果?

      如上所述

难点分析

课下实验

在之前的基础上,lab4 完成了 系统调用 ipc fork 的设计。

其中,我认为系统调用部分最为复杂,需要从用户态调用 syscall_* 开始,一步步剖析系统内核代码(分析部分在思考题,不赘述),直到最底层的内核函数 sys_* ,其中一系列的调用需要花大量时间才能理解。

我踩了一个坑点,sys_mem_map 中的 envid2env 不需要设置 checkperm,无需检查被映射的进程与操作进程的关系(ipc 通信需要用到这)

在理解了上述的系统调用全流程后,后续的 ipc 和 fork 设计就很通畅了(前提是系统调用写对了 o(╥﹏╥)o)。

在 MOS 中,ipc 和 fork 都是用户态函数,使用内核态系统调用完成对应功能即可。

课上实验

exam 中,barrier 的实现较为简单(题面中,整个系统至多存在1个barrier),我直接在 Env 进程控制块中添加了属性 flag 用以判断进程是否在 barrier 处等待。我的程序出现了一个诡异的bug,我怀疑测试程序有问题,部分进程 env_alloc 初始化 flag,没有设置为期望值。此部分记录在我的 git history中。

extra 需要实现信号量 semaphore,由于时间不足,我没有完成。

实验体会

本单元收获颇丰,学习了系统调用的实现,ipc进程通信,fork 创建子进程。美中不足的是,没有完成 extra 的信号量设计。

如果能完成信号量的设计,是完全掌握了本单元的内容(理论课的实践!)。然而上机结束后丧失了继续写 extra 的动力 XD。