BUAA-OS-2023-Lab4-Report
BUAA-OS-2023-Lab4-Report
Thinking 4.1
- 思考并回答下面的问题:
- 内核在保存现场的时候是如何避免破坏通用寄存器的?
- 系统陷入内核调用后可以直接从当时的$a0-$a3 参数寄存器中得到用户调用msyscall留下的信息吗?
- 我们是怎么做到让sys 开头的函数“认为”我们提供了和用户调用msyscall 时同样的参数的?
- 内核处理系统调用的过程对Trapframe 做了哪些更改?这种修改对应的用户态的变化是什么?
- 在系统发生异常时跳入异常分发程序时,会先调用
SAVE_ALL
宏定义,把所有寄存器(用户态现场)保存在内核栈 KSTACKTOP 下。最后将 sp寄存器指向保存的这个栈帧。这样既能保护用户态时通用寄存器的值不被改变,同样也能在内核态中便捷地调用每个寄存器的值 - 可以直接调用。系统调用
msyscall
时只通过SAVE_ALL
将寄存器的值复制到了内核栈中,但对寄存器中的值没有做任何处理,仍然保持着用户态进入时的状态。所以这时参数寄存器内数值仍然可用。 - 在
do_syscall
函数中,我们通过系统调用向量syscall_table
选择系统调用函数后,在函数内调用了func(arg1, arg2, arg3, arg4, arg5);
,此处的arg
都来源于用户态中保存的参数寄存器和栈帧内。所以相当于调用了相同参数的函数。 - 系统调用过程中:
-
tf->regs[2] = func(arg1, arg2, arg3, arg4, arg5);
更改了 v0 寄存器以保存系统调用执行后的返回值; -
tf->cp0_epc += 4;
更改了 cp0_epc 寄存器以使得返回用户态的执行语句变为执行系统调用的后一条指令
-
Thinking 4.2
- 思考 envid2env 函数: 为什么 envid2env 中需要判断 e->env_id != envid 的情况?如果没有这步判断会发生什么情况?
在产生 env_id
的函数 mkenvid
中,id 的低10位会保留为该 env 块在 envs 中的序号,高位是一个按序增大的数。当再次申请一个曾经被调度过的 env 块时,两次获取的 env_id
低10位相同,高位不同。 我们在 envid2env
中直接用 e = &envs[ENVX(envid)];
选取了 env 块,实际上此时只能保证取出的块与 id 的低10位一定相同,没有对10位以上进行检验。为了避免出现高位不同而无效的 env_id
,我们需要再判断一次 e->env_id != envid
。 如果没有这步判断,可能会导致之前申请这个 env 产生的 env_id
再次获取到这个 env 块。(然而实际上旧 id 已经失效了)
Thinking 4.3
- 思考下面的问题,并对这个问题谈谈你的理解:请回顾 kern/env.c 文件中 mkenvid() 函数的实现,该函数不会返回0,请结合系统调用和 IPC 部分的实现与 envid2env() 函数的行为进行解释
首先在 envid2env
函数中,为了进程能够便利地获得自身进程的 env
,我们规定了当输入 ID = 0 时,会返回当前进程的控制块。如果 mkenvid
函数产生了一个 ID = 0 的进程块,那么这个功能将无法判断当前是在查询 ID = 0
的进程块,还是在查询当前进程的进程块了 在 sys_ipc_send
中更新接收者信息时也会发生这样的状况,若发送者 env_id
为 0,那么接收者将混淆发送者,有可能会是自己发给自己的()
Thinking 4.4
- 关于fork 函数的两个返回值,下面说法正确的是: A、fork 在父进程中被调用两次,产生两个返回值 B、fork 在两个进程中分别被调用一次,产生两个不同的返回值 C、fork 只在父进程中被调用了一次,在两个进程中各产生一个返回值 D、fork 只在子进程中被调用了一次,在两个进程中各产生一个返回
选 C 。 fork
函数在父进程想要创建子进程时被执行,在执行过程中创建了一个新的进程。同时子进程会返回 0 ,父进程将会返回子进程的 id,方便父进程进行规划。
Thinking 4.5
- 我们并不应该对所有的用户空间页都使用duppage 进行映射。那么究竟哪些用户空间页应该映射,哪些不应该呢?请结合kern/env.c 中env_init 函数进行的页面映射、include/mmu.h 里的内存布局图以及本章的后续描述进行思考
UTEXT
之下的无效内存、USTACKTOP
之上的空白区与用户异常栈都不需要进行页面映射。在进入 duppage
函数内,只读页面与共享同写页面都不需要进行映射。除此之外,用户区的其他页面都需要进行映射。
Thinking 4.6
- 在遍历地址空间存取页表项时你需要使用到vpd 和vpt 这两个指针,请参考user/include/lib.h 中的相关定义,思考并回答这几个问题:
- vpt 和vpd 的作用是什么?怎样使用它们?
- 从实现的角度谈一下为什么进程能够通过这种方式来存取自身的页表?
- 它们是如何体现自映射设计的?
- 进程能够通过这种方式来修改自己的页表项吗?
vpt
指向了用户进程自身的用户页表基址; vpd
指向了用户进程的用户页目录基址。vpd
可以视作用户的 cur_pgdir
,vpt
则可以看作 cur_pgdir[0]
这是两个系统定义的便于读取用户态页表的宏,它们指向了位于 kuseg 中的页表虚拟地址。程序允许访问 kuseg 中的虚拟地址,从过 vpt 和 vpd ,用户就能获取自身的页表。 UVPT 本身就是用户页表的基地址,vpt 自然是页表基址。(PDX(UVPT) << PGSHIFT))
指向了用户页表所在的页目录项,这里的取法来自页表的自映射。 进程页表在这种情况下应当只允许读取,不允许这样修改,否则可能会出现错误的访问。
Thinking 4.7
- 在
do_tlb_mod
函数中,你可能注意到了一个向异常处理栈复制 Trapframe 运行现场的过程,请思考并回答这几个问题:- 这里实现了一个支持类似于“异常重入”的机制,而在什么时候会出现这种“异常重 入”?
- 内核为什么需要将异常的现场 Trapframe 复制到用户空间?
当处理 tlb_mod
时,可能会发生用户异常处理函数出错导致的异常,这时需要重入处理;同时由于没有关闭中断,所以若再次发生外部中断(时钟中断),也会触发异常的重入 如果在这里不保存 Trapframe 的话,do_tlb_mod 函数会跳转至用户设置的异常处理函数 env_user_tlb_mod_entry
中,但当该函数执行结束后,将无法恢复 EPC,所以需要先保存用户现场再返回
Thinking 4.8
- 在用户态处理页写入异常,相比于在内核态处理有什么优势?
在用户态处理,相较于内核态处理最明显的区别就在于其省去了跳转进入内核态的繁琐操作,为频繁的跳转省去了大量时间。 其次在用户态处理,给用户自定义页写入异常处理函数的入口,使得操作系统的处理更多样性、更灵活。
Thinking 4.9
请思考并回答以下几个问题:
- 为什么需要将
syscall_set_tlb_mod_entry
的调用放置在syscall_exofork
之前? - 如果放置在写时复制保护机制完成之后会有怎样的效果?
Upd:23.5.4
前两年的这个题好像放的是缺页中断的处理函数?我们这里变成了设置页写入异常的处理函数。如果在 syscall_exofork 中,写入了某个曾经被标记为 TLB_COW 的页面,此时的 tlb_Mod 将无法处理。虽然我觉得这种情况应该不会发生?确实不太明白 其实我觉得如果我们的 syscall_exofork 不是一个内联函数的话,这里就有点意义了,可以防止 syscall_exofork 的栈帧被父进程覆盖,导致子进程无法返回(但是做法也不对,应该把duppage也移动过来,实际上更寄) 如果放在写时复制机制完成后设置,一旦在这两步操作中出现了进程的 TLB_Mod,进程就会因为无法处理异常而 panic 对于第二问,如果先执行完写时复制机制,这时的所有原本可写 & 非共享的用户页都成为写时复制页,这就导致了 USTACKTOP 这一页也变成了写时复制页,在此之后的任意一个会创建栈帧的函数都不能执行了,因为这样会写入用户栈,并进入 TLB_Mod,此时又会因为没有设置 handler 导致 panic 可能是因为,如果先fork的话,还需要给子进程设置一下cow_entry;但如果先给父进程设置entry的话,生成子进程的exofork过程中调用了env_alloc,会自动复制父进程的cow_entry,就省去了额外在子进程中设置entry的步骤,写时复制机制可以直接运行(大概) 因为在 fork 函数中调用的 duppage 函数中也会发生页写入异常。如果不设置 mod_entry 就先进行 fork,会导致无法响应页写入异常。
心得体会
在 Lab4 中,我们学习了:
- 系统调用
syscall
的流程 - 进程间通信机制:ipc
- 用户进程的创建方式:
fork
函数 - 写时复制 COW 与页写入异常
主要实现了用户进程的创建,完善了不同进程间内存空间的处理机制。完成进程创建,尤其是进程内存的使用后, Lab5 中,基于进程的文件系统才能更好实现。