banner
Hi my new friend!

OS-Lab4

Scroll down

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
2
3
4
u_int mkenvid(struct Env *e) {
static u_int i = 0;
return ((++i) << (1 + LOG2NENV)) | (e - envs);
}

可见因为有 (++i) << (1 + LOG2NENV)),该函数不可能返回0 。

而在envid2env()函数中

1
2
3
4
5
if(envid == 0)
{
*penv = curenv;
return 0;
}

如果传入的envid值为0,则会返回当前进程的进程控制块的指针。因此可以通过传入0值来直接获得当前进程控制块的指针,便于访问。

Thinking 4.4

答:

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

Thinking 4.5

内存布局图如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
o     4G ----------->  +----------------------------+------------0x100000000
o | ... | kseg2
o KSEG2 -----> +----------------------------+------------0xc000 0000
o | Devices | kseg1
o KSEG1 -----> +----------------------------+------------0xa000 0000
o | Invalid Memory | /|\
o +----------------------------+----|-------Physical Memory Max
o | ... | kseg0
o KSTACKTOP-----> +----------------------------+----|-------0x8040 0000-------end
o | Kernel Stack | | KSTKSIZE /|\
o +----------------------------+----|------ |
o | Kernel Text | | PDMAP
o KERNBASE -----> +----------------------------+----|-------0x8001 0000 |
o | Exception Entry | \|/ \|/
o ULIM -----> +----------------------------+------------0x8000 0000-------
o | User VPT | PDMAP /|\
o UVPT -----> +----------------------------+------------0x7fc0 0000 |
o | pages | PDMAP |
o UPAGES -----> +----------------------------+------------0x7f80 0000 |
o | envs | PDMAP |
o UTOP,UENVS -----> +----------------------------+------------0x7f40 0000 |
o UXSTACKTOP -/ | user exception stack | BY2PG |
o +----------------------------+------------0x7f3f f000 |
o | | BY2PG |
o USTACKTOP ----> +----------------------------+------------0x7f3f e000 |
o | normal user stack | BY2PG |
o +----------------------------+------------0x7f3f d000 |
a | | |
a ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
a . . |
a . . kuseg
a . . |
a |~~~~~~~~~~~~~~~~~~~~~~~~~~~~| |
a | | |
o UTEXT -----> +----------------------------+------------0x0040 0000 |
o | reserved for COW | BY2PG |
o UCOW -----> +----------------------------+------------0x003f f000 |
o | reversed for temporary | BY2PG |
o UTEMP -----> +----------------------------+------------0x003f e000 |
o | invalid memory | \|/
a 0 ------------> +----------------------------+ ----------------------------

env_init()函数部分内容如下:

1
2
3
base_pgdir = (Pde *)page2kva(p);
map_segment(base_pgdir, 0, PADDR(pages), UPAGES, ROUND(npage * sizeof(struct Page), BY2PG), PTE_G);
map_segment(base_pgdir, 0, PADDR(envs), UENVS, ROUND(NENV * sizeof(struct Env), BY2PG, PTE_G);
  • 由该函数可知,UNEVSUPAGES这两段内容在函数内被映射到base_pgdir中,在之后的env_setup_vm()函数中被复制到所有envenv_pgdir中,因此不需要复制。UVPT这段内容是进程的页表,不可以从父进程复制。
  • 对于USTACKTOP-UTOP之间的内容,在[USTACKTOP, UXSTACKTOP - BY2PG)是无效内存,而在[UXSTACKTOP - BY2PG, UXSTACKTOP)是用户缺页异常处理栈,不同进程该栈不同,因此该段内容不需要复制。
  • 对于USTACKTOP以下的内容,需要复制到子进程中。

Thinking 4.6

答:

vptvpd定义如下:

1
2
#define vpt ((volatile Pte *)UVPT)
#define vpd ((volatile Pde *)(UVPT + (PDX(UVPT) << PGSHIFT)))
  • 容易看出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函数,其将现场保存在异常处理栈中,设置 a0EPC 寄存器的值,返回用户态。
  • 跳转到 env_user_tlb_mod_entry 域存储的用户异常处理函数的地址(实验中为cow_entry)。
  • 页写入异常处理完成,跳转回用户程序。

三、心得体会

在本次实验中我掌握了系统调用的概念以及流程、了解进程间通信机制(IPC)、学习页写入异常处理流程等,深入理解了进程间的协作以及创建子进程,并且理解了微内核架构下内核态与用户态相互联系和转换,收获很多。

其他文章