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)、学习页写入异常处理流程等,深入理解了进程间的协作以及创建子进程,并且理解了微内核架构下内核态与用户态相互联系和转换,收获很多。