BUAA OS Lab4

Lab4实验报告

20375061 鹿煜恒

思考题

thinking4.1

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

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

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

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

在进入异常处理时会调用SAVA_ALL宏,如下:

 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

 6 .macro SAVE_ALL
  7 .set noreorder
  8 .set noat
  9         move    k0, sp
 10 .set reorder
 11         bltz    sp, 1f
 12         li      sp, KSTACKTOP
 13 .set noreorder
 14 1:
 15         subu    sp, sp, TF_SIZE
 16         sw      k0, TF_REG29(sp)
 17         mfc0    k0, CP0_STATUS
 18         sw      k0, TF_STATUS(sp)
 19         mfc0    k0, CP0_CAUSE
 20         sw      k0, TF_CAUSE(sp)
 21         mfc0    k0, CP0_EPC
 22         sw      k0, TF_EPC(sp)
 23         mfc0    k0, CP0_BADVADDR
 24         sw      k0, TF_BADVADDR(sp)
 25         mfhi    k0
 26         sw      k0, TF_HI(sp)
 27         mflo    k0
 28         sw      k0, TF_LO(sp)
 29         sw      $0, TF_REG0(sp)
……
 59         sw      $31, TF_REG31(sp)
  1. 其中首先将用户空间的sp地址放到k0处,如果不处于内核态,则设置sp为内核栈,随后向sp中压入原sp和所有相关寄存器数据。此时,借用k0寄存器,将所有数据都存储在TrapFrame中,保存了现场。在Restore宏中只需要将所有内容恢复,即避免了破坏通用寄存器。

  2. 可以访问到a1-a3,因为其在进入系统调用过程中没有被写入,而a0写入了保存的TrapFrame地址,丢失了用户调用时留下的信息。

  3. 在SAVEALL中保存了前四个参数和sp位置。在do_syscall中,通过读取tf中的reg5-reg7及当前用户空间对应的sp中第5、6号参数,并将其传给func。虽然实际上func对应的函数只接受少于等于5个参数,但是在汇编层面调用时即使在对应位置多传入了参数也不影响对应函数正常执行,对于内核中的syscall处理函数而言,就好像直接调用了一样。

  4. 做了以下改动:

    1. 如果sysno超限,则设置tf的2号寄存器,将返回值设为-E_NO_SYS,不进行其他改动。
    2. 将cp0EPC+4,从而执行syscall的下一条语句。
    3. 调用系统调用处理函数,并将tf的2号寄存器设为返回值。

thinking4.2

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

因为获取e时使用envs+ENVX(envid),当envid[31:10]不匹配env时时依然可以获得e如env被free后重新分配,再由老envid调用envid2env,但此时envid!=e->env_id,说明envid对应的env不存在于当前的envs中,应当属于非法envid并返回-E_BAD_ENV。

thinking4.3

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

不会返回0,10位以上从1开始保持递增从而避免冲突,一方面使0可以表示当前env,另一方面可以让不同进程先后共用一个env块。调用envid时需要同时匹配envid和id,在ipc相关操作的时候可以通过envid2env返回错误值确保接受方是发送方想要的env。

thinking4.4

关于 fork 函数的两个返回值,下面说法正确的是:

A、fork 在父进程中被调用两次,产生两个返回值

B、fork 在两个进程中分别被调用一次,产生两个不同的返回值

C、fork 只在父进程中被调用了一次,在两个进程中各产生一个返回值

D、fork 只在子进程中被调用了一次,在两个进程中各产生一个返回值

C

thinking4.5

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

需要映射0到ustacktop之间的内存空间。其上的用户空间保存了所有用户空间共享的envs、pages和异常处理栈,

thinking4.6

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

  • vpt 和 vpd 的作用是什么?怎样使用它们?
  • 从实现的角度谈一下为什么进程能够通过这种方式来存取自身的页表?
  • 它们是如何体现自映射设计的?
  • 进程能够通过这种方式来修改自己的页表项吗?
  1. 定义为:

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

    作用是为页表和页目录提供指针,直接读取即可,但是不能写入。

  2. 在env_setup_vm中首先为进程页目录分配了一个内存块,随后在env的虚拟地址空间中设置UVPT位置为页目录所在物理地址位置。这样当用户程序运行时,访问UVPT对应的位置就通过tlb页表映射到自身的分配的env_pgdir中,以此来实现忽略pgidr的具体物理位置。

  3. 自映射即页目录也是自身的一项对应的页表。体现在页目录pgdir也是自身UVPT位置对应的二级页表。

  4. 不能,没有PTE_D权限位,则无法修改。又因为没有该权限为位,进程无法修改自身的该权限位,保证了安全。

thinking4.7

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

  • 这里实现了一个支持类似于“异常重入”的机制,而在什么时候会出现这种“异常重 入”?
  • 内核为什么需要将异常的现场 Trapframe 复制到用户空间?
  • 此时实现的异常重入是为了防止触发tlbmod后的异常处理函数中再次触发tlbmod。然而我个人理解时几乎不太可能存在这种情况,因为cowentry时开了一个新的栈,此时无法再次触发tlbmod异常。综合考虑,应当是为多个用户态异常处理函数的交叉触发提供tf的保存,避免在某个异常处理函数中触发另一个用户态异常时覆盖异常栈。
  • 因为需要在用户空间利用syscall_set_trapframe恢复触发异常前的执行状态。

thinking4.8

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

可以让用户态程序自己处理异常,与内核解耦,精简内核并为用户进程提供更大的自由度。

thinking4.9

请思考并回答以下几个问题: • 为什么需要将 syscall_set_tlb_mod_entry 的调用放置在 syscall_exofork 之前? • 如果放置在写时复制保护机制完成之后会有怎样的效果?

  • 因为父进程在duppage的过程中可能会涉及到本次调用栈所在位置,导致下一次调用时触发tlbmod异常需要进入cow_entry函数。因此需要在duppage前,实际上不需要再syscall_exofork前。
  • 如上所述,会导致内核态中do_tlb_mod函数中寻找用户态处理函数的时候找不到函数,进而导致panic。

实验难点

  • 在对内存操作时为什么要传入asid?

    • 因为内存操作后需要invalidate对应tlb,这时需要asid
  • 回顾系统调用实现方式:

    1. 在entry.S中定义异常入口exc_gen_entry,系统调用、时钟中断等异常均会在此处进入。除tlbmiss异常外均会执行SAVA_ALL宏。
    2. 在trap.c中定义了异常处理句柄exception_handlers,其中定义了处理syscall等异常的入口。
    3. 上文提到的处理入口定义在genex.S,其中使用宏定义构造了处理入口,具体而言,会将TF位置既当前内核栈位置作为第一个参数,调用定义的handler,定义的位置处于各个.c文件中。
  • do_tlb_mod函数

    • 第一次进入tlbmod异常时,sp寄存器位于内核栈,栈顶是进入异常处理时SAVEALL保存的tf,下方是函数调用栈,函数调用参数指向内核栈中的tf。
    • 在函数中,首先保存了内核栈中的tf内容到局部变量tmp_tf,随后修改了内核栈中的tf的29sp寄存器为用户空间异常栈UXSTACKTOP,并将之前保存的tmp_tf压入栈顶。
    • 随后,如果当前env有处理函数,则将用户异常栈中保存的原始tf作为参数,利用异常处理完成的恢复操作调用该处理函数。此时,tf被修改为异常处理栈中的地址,epc为用户提供的tlb_mod处理函数。

实验感想

本次实验来回切换于用户态和内核态,需要对两个态的sp指针切换原理和环境保存、恢复原理有较为深入的理解才能够顺利完成实验。

其中对于异常的用户态处理的用户空间异常栈十分有趣,通过越过fork的duppage来处理duppage产出的cow错误,值得注意的是cow_entry中的局部变量保存在异常栈中,执行过程中不会循环出发cow错误。因此不能在其中调用fork之类的函数。

总体而言,难度有所提升,理解难度维持较高水平。

Licensed under CC BY-NC-SA 4.0
京ICP备2021032224号-1
Built with Hugo
主题 StackJimmy 设计
vi ./themes/hugo-theme-learn/layouts/partials/footer.html