OS-lab4

思考题
Thinking 4.1
内核在保存现场的时候是如何避免破坏通用寄存器的?
通过SAVE_ALL
宏的函数可以知道,保存现场时将所有通用寄存器的值全部保存到了trapframe
中
系统陷入内核调用后可以直接从当时的
$a0-$a3
参数寄存器中得到用户调用 msyscall留下的信息吗?
可以
我们是怎么做到让 sys 开头的函数“认为”我们提供了和用户调用 msyscall 时同样的参数的?
将参数保存在了堆栈里面
内核处理系统调用的过程对 Trapframe 做了哪些更改?这种修改对应的用户态的变化是什么?
将trapframe
中的epc
加4,这会在系统调用结束后直接执行用户程序的下一条指令,并将系统调用的返回值存放在$ra
中
Thinking 4.2
思考 envid2env 函数: 为什么 envid2env 中需要判断 e->env_id != envid的情况?如果没有这步判断会发生什么情况?
判断 e->env_id != envid
的目的是为了确保通过 envid
提取索引找到的 struct Env
结构体,不仅位于正确的数组位置上,而且就是该 envid
在生成时所代表的那个具体的环境实例,防止了使用过期的 envid
来操作后来创建的、恰好占据了同一个数组槽位的新环境。
Thinking 4.3
思考下面的问题,并对这个问题谈谈你的理解:请回顾 kern/env.c 文件中 mkenvid() 函数的实现,该函数不会返回 0,请结合系统调用和 IPC 部分的实现与envid2env() 函数的行为进行解释。
在IPC调用的时候,envid为0即为curenv。
Thinking 4.4
关于 fork 函数的两个返回值,下面说法正确的是:
A、fork 在父进程中被调用两次,产生两个返回值
B、fork 在两个进程中分别被调用一次,产生两个不同的返回值
C、fork 只在父进程中被调用了一次,在两个进程中各产生一个返回值
D、fork 只在子进程中被调用了一次,在两个进程中各产生一个返回值
C
Thinking 4.5
我们并不应该对所有的用户空间页都使用 duppage 进行映射。那么究竟哪些用户空间页应该映射,哪些不应该呢?请结合 kern/env.c 中 env_init 函数进行的页面映射、 include/mmu.h 里的内存布局图以及本章的后续描述进行思考。
0-USTACKTOP
是需要使用duppage
进行映射的
USTACKTOP-UTOP
之间的user exception stack
,不会在处理COW异常时调用fork()
,所以不需要进行映射
UTOP
以上的区域是内核或用户态的共享空间,用户态无权访问或只可读,所以无需进行映射
Thinking 4.6
在遍历地址空间存取页表项时你需要使用到 vpd 和 vpt 这两个指针,请参考user/include/lib.h 中的相关定义,思考并回答这几个问题:
• vpt 和 vpd 的作用是什么?怎样使用它们?
• 从实现的角度谈一下为什么进程能够通过这种方式来存取自身的页表?
• 它们是如何体现自映射设计的?
• 进程能够通过这种方式来修改自己的页表项吗?
1 |
从上面的定义可以看出,vpt
是页表首地址,可通过偏移量PTX(va)
获得页表项,vpd
是页目录首地址,可通过偏移量PDX(va)
获得页目录项。
页表首地址的计算公式显然就是自映射的计算公式,因此可以方便的通过页表基地址计算出页目录地址
不能,页表是内核态程序维护的,用户态进程对于页表是只读的
Thinking 4.7
在do_tlb_mod 函数中,你可能注意到了一个向异常处理栈复制 Trapframe运行现场的过程,请思考并回答这几个问题:
• 这里实现了一个支持类似于“异常重入”的机制,而在什么时候会出现这种“异常重入”?
• 内核为什么需要将异常的现场 Trapframe 复制到用户空间?
当写时复制发生时,需要调用用户态的函数发生中断并处理异常,这被称为“异常重入”
主要目的是让用户程序访问到异常发生时的状态信息,
Thinking 4.8
在用户态处理页写入异常,相比于在内核态处理有什么优势?
能够减少内核出现错误的可能,即使用户程序崩溃也不会影响到操作系统的稳定性;能够减少陷入内核、切换山下文的次数,提升操作系统性能
Thinking 4.9
请思考并回答以下几个问题:
• 为什么需要将 syscall_set_tlb_mod_entry 的调用放置在 syscall_exofork 之前?
• 如果放置在写时复制保护机制完成之后会有怎样的效果?
syscall_exofork()
调用后需要修改env
指针,这一过程涉及到写时复制机制,会触发COW
异常,而对于COW
的中断处理依赖于syscall_set_tlb_mod_entry
,因此我们需要将syscall_set_tlb_mod_entry
放在syscall_exofork
之前
难点理解
进程间通信
进程间通信由两个函数实现:sys_ipc_recv
与sys_ipc_try_send
,分别负责接收与发送。
在sys_ipc_recv
中,首先检查虚拟地址是否在用户空间中,其次检查是否为0,当虚拟地址为0时表示不需要传递额外信息
1 | if (dstva != 0 && is_illegal_va(dstva)) { |
接着,设置进程控制块,设置接受信息位env_ipc_recving
,设置接收到的页面需要与自身的哪个虚拟页面完成映射env_ipc_dstva
1 | curenv->env_ipc_recving = 1; |
之后,阻塞当前进程,并让该进程自主放弃CPU时间
1 | curenv->env_status = ENV_NOT_RUNNABLE; |
接下来观察发送信息函数sys_ipc_try_send
,注意其第一个参数u_int envid
为“接收信息的进程id”
该函数首先通过进程id找到接收信息进程,如果该进程的env_ipc_recving
为1,则视为信息发送成功,进行下一步操作
接下来需要对接收进程的进程块进行直接操作。首先是将value
进行传递,之后设置接收进程的源进程id、将传递的页有效化、将接收信息位置0
1 | e->env_ipc_value = value; |
传输结束,我们需要将接收进程的状态修改为可执行,同时加入调度队列,使进程等待调度
1 | e->env_status = ENV_RUNNABLE; |
最后,将当前进程中需要传递的页面映射到接收进程的目标虚拟地址,达到传递页面信息的目的
写时复制
为了实现写时复制机制,我们设置一个标志位PTE_D
来表示该页面是否可写,但还需区分“只读”和“可写但是是写时复制页”,因此再设置一个PTE_COW
位区分这两种情况
fork
返回值
首先我们要明确一下fork()
调用的返回值,在父进程中调用,如果子进程创建成功,则返回子进程id,若不成功返回一个小于0的数;子进程中fork
调用的返回值为0
fork函数的调用关系
调用
syscall_set_tlb_mod_entry
陷入内核态,设置当前进程 TLB Mod 异常处理函数为
cow_entry
调用
syscall_exofork
陷入内核态,创建子进程;将当前Trapframe
中的内容存在子进程的tf
中,即复制父进程的上下文;设置子进程状态为不可运行,并设置优先级
调用
duppage
我们创建了子进程,但其现在处于不可运行状态,并且不在调度队列中,因此我们可以进一步对其进行处理:父进程需要将地址空间中需要与子进程共享的页面映射给子进程,因此我们需要遍历父进程中所有的页表项。但其实不需要遍历所有的,仅需要遍历
USTACKTOP
以下的即可,因为以上的总会被共享。之后使用duppage
完成映射的过程调用
syscall_set_tlb_mod_entry
,设置子进程的TLB Mod
异常处理函数为cow_entry
调用
syscall_set_env_status
,让子进程加入调度队列并被调度
duppage
这里唯一需要展开的就是duppage
函数,它先调用sys_mem_map
将页面映射给子进程,再根据父进程中该页面的权限位设置子进程中该页面的权限位。对于原本不可写或共享或写时复制的页面,我们无需更改其权限,而对于原本可写且非共享且非写时复制的页面,我们需要取消其可写位并设置为写时复制位。
- Title: OS-lab4
- Author: OWPETER
- Created at : 2025-05-06 22:44:37
- Updated at : 2025-05-06 22:45:15
- Link: https://owpeter.github.io/2025/05/06/OS-lab4/
- License: This work is licensed under CC BY-NC-SA 4.0.