OS lab4 思考题

OS lab4 思考题

rainbowYao Lv3

Thinking 4.1

思考并回答下面的问题:

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

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

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

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

函数调用顺序:

用户态:msyscall(user/lib/syscall_lib.c)——syscall(user/lib/syscall_wrap.S)——

内核态:exc_gen_entry(kern/entry.S)——exception_handlers(kern/genex.S)——do_syscall(kern/syscall_all.c

  1. 保存现场是在exc_gen_entry中的SAVE_ALL宏方法,将通用寄存器保存。

    至于SAVE_ALL宏方法:

    • 先用move k0, sp将通用寄存器的$sp复制到$k0(因为后面$sp就指向KSTACKTOP内核栈了)

    • 然后处理$sp使其处于内核栈的正确位置subu sp, sp, TF_SIZE

    • 再之后调用sw k0, TF_REG29(sp)将现场的$sp(现在储存在$k0中)存储到TF_REG29(sp)

    • 后续是储存一些CP0协处理器,如:mfc0 k0, CP0_STATUS sw k0, TF_STATUS(sp)

    • 最后是对通用寄存器(除去$sp)的储存如:sw $1, TF_REG1(sp)

    • 至此通用寄存器以及CP0协处理器都被储存在了内核态的内核栈KSTACKTOP中了,在env_run等函数中调用的*((struct Trapframe *)KSTACKTOP - 1的由来正是上面的操作,值得一提的是其中的“-1”是因为栈帧的增长方向是从上到下(高地址到低地址),故为了得到struct Trapframe的起始地址(低地址),需要从高地址减掉一个结构体的内存大小(相当于SAVE_ALL中的subu sp, sp, TF_SIZE

  2. 除去$a0,其他理论上可以。按照上面的函数调用顺序检查,只有$a0在从handle_sysdo_syscall 的过程中被设置成了$sp(也就是传递给do_syscall的参数*tf),而原始的$a0保存的信息是msyscall传递进去的系统调用号,这一值(即sysno)只在do_syscall索引sys_*()函数的时候使用,并未继续传入sys_*(),故在sys_*()中直接使用$a1~$a3没有问题。

    但最好的获取用户调用时留下的$a0~$a3的方法是通过tf->regs

  3. 根据上面对SAVE_ALL的解释,异常刚发生时进行了“保存现场”的操作,msyscall传递的参数中 $a0~$a3被保存在了内核栈中,可以通过tf->regs[4|5|6|7]来获得,而另外两个参数被压入了msyscall栈帧,可以通过tf->regs[29] + 16|20来获得

  4. 第一,将EPC寄存器加四,因为保证了syscall不在延迟槽内,结束系统调用要回到他的下一条指令

    第二,将系统调用处理函数sys_*()的返回值写到$v0寄存器中

Thinking 4.2

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

先看mkenvid这个函数:

1
2
3
4
5
6
7
8
9
10
//env.c
u_int mkenvid(struct Env *e) {
static u_int i = 0;
return ((++i) << (1 + LOG2NENV)) | (e - envs);
}

//env.h
#define LOG2NENV 10
#define NENV (1 << LOG2NENV)
#define ENVX(envid) ((envid) & (NENV - 1))

我们可以得出一个envid由两部分组成:高位置表示该进程是第几个被申请的进程(不断自增),低位置表示该进程相对于数组envs的偏移。ENVX函数就是通过低位置索引到的进程控制块,但是一个进程控制块(地址意义上的)可能被申请、释放、再申请,所以两个env_id的低位置相等不代表是一个进程。

如果不做这一步判断,可能索引到的是错误的或者本该被销毁的进程控制块

Thinking 4.3

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

上面给出过mkenvid()函数的实现方法,由于++i保证了envid不会是0,而内核方法envid2env中如果传递的envid == 0的话,返回的是curenv

由于curenv是内核态的变量,用户态无法感知curenv,但为了方便用户进程调用 syscall_*() 时把当前进程的envid作为参数传给内核函数,可以使用envid == 0的操作,比如fork函数调用sys_set_tlb_mod_entry 时就出现了envid == 0时直接返回当前进程的实现

Thinking 4.4

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

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

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

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

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

选:C

fork函数的调用只发生在父进程中,并通过调用syscall_exofork函数创建了进程。此时,内核给两个进程设置了不同的返回值,父进程中返回子进程的id,子进程返回0。

Thinking 4.5

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

  • 在 0 ~ USTACKTOP范围的内存需要使用 duppage 进行映射;
  • USTACKTOP ~ UTOP中user exception stack 进行页写入异常时所用
  • UTOP以上则要么属于内核,要么是所有进程共享,用户模式下只读。

Thinking 4.6

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

  • vpt 和 vpd 的作用是什么?怎样使用它们?
  • 从实现的角度谈一下为什么进程能够通过这种方式来存取自身的页表?
  • 它们是如何体现自映射设计的?
  • 进程能够通过这种方式来修改自己的页表项吗?
  • vpt和vpd分别是页表首地址页目录首地址,通过虚拟地址偏移即可得到相应的页表项,他们可以直接当作数组来使用
  • 在user/include/lib.h的实现中:
1
2
#define vpt ((const volatile Pte *)UVPT)
#define vpd ((const volatile Pde *)(UVPT + (PDX(UVPT) << PGSHIFT)))
  • 上面代码正是体现了自映射,vpd是vpt中的一页
  • 不能,这段区域位于UVPT以上,对于用户程序来说是只读的。若想修改,需要陷入内核进行操作

Thinking 4.7

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

这里实现了一个支持类似于“异常重入”的机制,而在什么时候会出现这种“异常重入”?

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

  • 首先我对于“异常重入”这个词的理解是不在内核态处理异常,而是下方到用户态,但是“用户态——异常——内核态——下方——用户态”这样子一个过程就需要保存两次Trapframe
    因为do_tlb_mod函数并没有实际的处理页写入异常的函数段,而是通过do_tlb_mod函数下放回用户态来书写处理异常的函数段,所以处理TLB Mod异常的时候就需要异常重入

  • 将现场的Trapframe复制到用户空间是因为TLB Mod 真正的异常处理过程书写在用户态,而用户态无法访问内核空间(也就是KSTACKTOP下方),但其中保存的异常现场信息是异常处理函数需要知道的,故将这部分内容拷贝到用户态的异常处理栈中(也就是UXSTACKTOP下),满足了用户态也可以访问的需求

Thinking 4.8

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

  • 解放内核,页面拷贝过程是还是很费时的,不让内核执行大量的拷贝工作
  • 减少了内核态出现失误的可能,防止整个系统崩溃
  • 用户态进行新页面的分配映射也更加灵活方便

Thinking 4.9

请思考并回答以下几个问题:

  • 为什么需要将 syscall_set_tlb_mod_entry 的调用放置在 syscall_exofork 之前?

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

  • 保证在子进程创造出来之前就设置好写时复制机制异常处理函数的入口

    1
    2
    3
    4
    5
    child = syscall_exofork();

    if (env->env_user_tlb_mod_entry != (u_int)cow_entry) {
    try(syscall_set_tlb_mod_entry(0, cow_entry));
    }

    若是这么一个顺序,子进程被设置为RUNNABLE且返回之后(第一行代码执行结束),子进程的写时复制机制异常处理函数入口还未被设置,如果设置过程中写到了需要触发了TLB MOD,会找不到跳转的位置而出现错误。

    更换顺序后,先设置入口再执行suscall_exofork可以让子进程延用父进程设置的入口从而无需再设置一次

  • 如在再写时保护机制完成之后再设置异常处理函数入口问题就更大了,因为父进程在duppage的过程中更容易触发TLB MOD,相比于第一问的极小的触发TLB MOD的代码段,这个设计的问题更大

  • 标题: OS lab4 思考题
  • 作者: rainbowYao
  • 创建于 : 2024-07-02 21:21:46
  • 更新于 : 2024-09-18 09:28:38
  • 链接: https://redefine.ohevan.com/2024/07/02/OS-lab4-思考题/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
此页目录
OS lab4 思考题