Lab4实验报告
一、思考题
Thinking 4.1

答:
- 内核调用
SAVE_ALL
宏函数保存现场,同时这个宏函数只使用$k1
、$k2
两个寄存器操作,而不破坏其他寄存器的值。 - 不可以。在陷入内核调用后,
$a0
寄存器中的值已被改变为系统调用号。虽然$a1
-$a3
未被使用,但其也可能被改变。若想得到原始信息,应从栈中获取。 - 将用户进程的寄存器
$a0
-$a3
以及用户栈中的参数a4,a5拷贝至内核寄存器$a0
-$a3
和内核栈。 - 设置
tf->cp0_epc
+4,以便从内核调用返回用户态是可以正确执行调用后的下一条指令。
Thinking 4.2

答:
一个存在于env_free_list
的进程控制块在被env_alloc
函数调用时会被分配一个独一无二的envid
,在其被env_free
函数调用时会重新插入进env_free_list
。当这个进程控制块被env_alloc
函数再次调用时,其会被分配另一个envid
。实质上这个进程控制块控制两个不同的进程。而当第一个envid
被传入envid2env
函数时,寻找的目标进程是第一个进程,函数会找到这个进程控制块,而此时该进程控制块控制着另一个进程,因此找到的是第二个进程。如果没有这步判断,函数找到的进程控制块可能不对应envid
。
Thinking 4.3

答:
mkenvid()
函数代码如下:
1 | u_int mkenvid(struct Env *e) { |
可见因为有 (++i) << (1 + LOG2NENV))
,该函数不可能返回0 。
而在envid2env()
函数中
1 | if(envid == 0) |
如果传入的envid
值为0,则会返回当前进程的进程控制块的指针。因此可以通过传入0值来直接获得当前进程控制块的指针,便于访问。
Thinking 4.4

答:
在子进程被调度时,上下文环境初于fork()
函数内,虽然会返回值,但子进程没有真正意义上完整地调用fork()
函数,因此fork()
函数只在父进程中被调用了一次,但是在父进程中返回子进程的envid
,在子进程中返回0,故选择C。
Thinking 4.5

内存布局图如下:
1 | o 4G -----------> +----------------------------+------------0x100000000 |
env_init()
函数部分内容如下:
1 | base_pgdir = (Pde *)page2kva(p); |
- 由该函数可知,
UNEVS
和UPAGES
这两段内容在函数内被映射到base_pgdir
中,在之后的env_setup_vm()
函数中被复制到所有env
的env_pgdir
中,因此不需要复制。UVPT
这段内容是进程的页表,不可以从父进程复制。 - 对于
USTACKTOP
-UTOP
之间的内容,在[USTACKTOP, UXSTACKTOP - BY2PG)
是无效内存,而在[UXSTACKTOP - BY2PG, UXSTACKTOP)
是用户缺页异常处理栈,不同进程该栈不同,因此该段内容不需要复制。 - 对于
USTACKTOP
以下的内容,需要复制到子进程中。
Thinking 4.6

答:
vpt
和vpd
定义如下:
1 |
- 容易看出
vpt
是指向用户地址空间中页表地址的指针,vpd
是指向用户地址空间中页目录地址的指针。 - 使用:以
vpt
为例,vpt
是页表地址,将其与虚拟地址中获得的偏移量相加得到指向指定页表项的指针,即可访问页表项。 vpd
指向UVPT + (PDX(UVPT) << PGSHIFT)
,而这个地址在UVPT
以上4MB空间内,这说明页目录被映射到页表中的某一个项,代表着页目录中必有一个页表项映射到自身,这体现了自映射设计。- 不可以。页表维护是操作系统完成的,而进程处于用户态,只有访问的权限。
Thinking 4.7

答:
- 在内核态中,CPU因为中断屏蔽位被置为1而不支持异常重入。但由于MOS的微内核结构,对于
COW
的处理是部分在用户态下完成的,因此处理的全过程不能保证中断屏蔽位是1,因此可能出现异常重入。在运行do_tlb_mod
函数时,外部请求时钟中断,由于此时CPU
处于内核态会屏蔽中断,do_tlb_mod
函数会被原子地执行完,设置好异常栈和cp0
,返回用户态后,时钟中断发生,进行进程调度;当再一次执行这一进程时,首先由env_run
使得CPU
上下文恢复至时钟中断发生前的状态,此后立即在用户态下进行COW
的缺页异常处理;这一过程可以视作一次 “中断重入” ;总之,在do_tlb_mod
的处理过程中,如果触发了新的do_tlb_mod
,就会出现这种中断重入。在用户态中,页写入异常处理函数是由用户态程序通过调用sys_set_user_tlb_mod_entry()
指定自己的页写入异常处理函数。如果用户程序自定义的页写入异常处理函数写了一些新的全局变量,则会导致异常重入。 - 由于MOS的微内核结构,缺页错误交给用户进程处理,而用户进程无法访问内核空间的数据,因此需要将现场保存在用户空间。
Thinking 4.8

减小内核的体积,符合微内核架构思想。减少关中断时间,提高了中断处理的效率。减少了内核出错的可能,利于系统的稳定。
Thinking 4.9

- 子进程执行的起始点是从
syscall_exofork()
返回用户态后的第一条指令,因此应在这之前父进程设置好自己的缺页处理机制。如果放在syscall_exofork()
之后,则子进程会再次执行syscall_set_tlb_mod_entry()
为env_user_tlb_mod_entry
赋值并且为cow_entry
分配空间,但是这实际上在子进程执行前就已经完成,所有造成了额外的资源开销。 - 如果放在写时复制保护机制完成后才执行,则会导致父进程进行
duppage
时遇到COW
缺页异常时没有处理函数处理,导致页写入异常处理机制无法建立。
二、实验难点
1.系统调用流程

syscall_*
调用msyscall
函数,使系统陷入内核态- CPU进入内核态,通过异常分发程序的分发进入异常服务函数
handle_sys
。 handle_sys
函数根据传入的系统调用号执行相应的sys_*
函数- 结束系统调用,返回用户态。
2.fork流程

- 父进程通过
syscall_set_tlb_mod_entry
设置自己的TLB MOD
异常处理函数。 - 调用
syscall_exofork
函数创建子进程。 - 父进程使用
duppage
将需要给子进程共享的页复制给子进程,标记COW
并建立写时复制机制。 - 父进程通过
syscall_set_tlb_mod_entry
设置子进程的TLB MOD
异常处理函数。 - 父进程通过
syscall_set_env_status
设置子进程的状态为可运行,此时子进程就已经可以运行了。
3.页写入异常处理流程

- 用户程序触发页写入异常,陷入内核,
exception_handlers
分发异常。 - 进入异常处理函数
handle_mod
。 - 跳转到
do_tlb_mod
函数,其将现场保存在异常处理栈中,设置a0
和EPC
寄存器的值,返回用户态。 - 跳转到
env_user_tlb_mod_entry
域存储的用户异常处理函数的地址(实验中为cow_entry
)。 - 页写入异常处理完成,跳转回用户程序。
三、心得体会
在本次实验中我掌握了系统调用的概念以及流程、了解进程间通信机制(IPC)、学习页写入异常处理流程等,深入理解了进程间的协作以及创建子进程,并且理解了微内核架构下内核态与用户态相互联系和转换,收获很多。