lab4实验报告

思考题

Thinking 4.1

题面

1
2
3
4
5
思考并回答下面的问题:
• 内核在保存现场的时候是如何避免破坏通用寄存器的?
• 系统陷入内核调用后可以直接从当时的 $a0-$a3 参数寄存器中得到用户调用 msyscall留下的信息吗?
• 我们是怎么做到让 sys 开头的函数“认为”我们提供了和用户调用 msyscall 时同样的参数的?
• 内核处理系统调用的过程对 Trapframe 做了哪些更改?这种修改对应的用户态的变化是什么?

回答

  1. 内核通过调用SAVE_ALL宏将非重入的下的异常的通用寄存器和其他特殊寄存器的值存储在栈空间的对应地址处。
  2. 可以,因为其本身在对应寄存器就有存储值。
  3. 通过将参数传入$a-$a3和栈空间中,使得sys开头的函数可以调用msyscall传入的相同参数。
  4. 将栈中存储的EPC寄存器值自增4,也就是跳转到下一条指令,因为在系统调用后,接下来将会下一条指令,又因为用户程序需要确保系统调用并不会出现在延迟槽中,因此直接加4即可得到下一条指令地址,作为存储返回值的v0寄存器也可能发生改变。

Thinking 4.2

题面

1
思考 envid2env 函数: 为什么 envid2env 中需要判断 e->env_id != envid的情况?如果没有这步判断会发生什么情况?

回答

具体的envid的生成过程中:

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

其中为了方便获取,其低十位直接取的是索引,因此需要重新判断以避免获取的进程已经销毁的情况。

Thinking 4.3

题面

1
思考下面的问题,并对这个问题谈谈你的理解:请回顾 kern/env.c 文件中 mkenvid() 函数的实现,该函数不会返回 0,请结合系统调用和 IPC 部分的实现与envid2env() 函数的行为进行解释。

回答

代码如下:

1
2
3
4
5
//kern/env.c
u_int mkenvid(struct Env *e) {
static u_int i = 0;
return ((++i) << (1 + LOG2NENV)) | (e - envs);
}

其中,((++i) << (1 + LOG2NENV))始终不为0,因此返回值始终不会是0.而在envid2env()函数中有如下代码:

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

而这也说明了在该函数中,其默认envid为0代表当前进程,同样在系统调用和IPC实现的通过调用envid2env()函数也默许了这种规定,如此一来,也方便了对于当前进程的获取与使用,因此mkenvid自然需要将这个特殊的envid给空出来.主要也是为了便于用户进程在内核变量不可知的情况下来调用系统调用进而调用内核接口。

Thinking 4.4

题面

1
2
3
4
5
关于 fork 函数的两个返回值,下面说法正确的是:
A、fork 在父进程中被调用两次,产生两个返回值
B、fork 在两个进程中分别被调用一次,产生两个不同的返回值
C、fork 只在父进程中被调用了一次,在两个进程中各产生一个返回值
D、fork 只在子进程中被调用了一次,在两个进程中各产生一个返回值

回答

根据fork()相关原理,应该选择C选项.

Thinking 4.5

题面

1
我们并不应该对所有的用户空间页都使用 duppage 进行映射。那么究竟哪些用户空间页应该映射,哪些不应该呢?请结合 kern/env.c 中 env_init 函数进行的页面映射、include/mmu.h 里的内存布局图以及本章的后续描述进行思考。

回答

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void env_init(void) {
int i;
LIST_INIT(&env_free_list);
TAILQ_INIT(&env_sched_list);
for (i = NENV - 1; i >= 0; i--) {
envs[i].env_status = ENV_FREE;
LIST_INSERT_HEAD(&env_free_list, &envs[i], env_link);
}
struct Page *p;
panic_on(page_alloc(&p));
p->pp_ref++;

base_pgdir = (Pde *)page2kva(p);
map_segment(base_pgdir, 0, PADDR(pages), UPAGES,
ROUND(npage * sizeof(struct Page), PAGE_SIZE), PTE_G);
map_segment(base_pgdir, 0, PADDR(envs), UENVS, ROUND(NENV * sizeof(struct Env), PAGE_SIZE),
PTE_G);
}

首先,依据内存布局图,只有低2GB为用户空间,即kuseg[0-ULIM)需要去考虑是否使用duppage()函数进行映射,由于USTACKTOP-UTOP间的user exception stack用来进行页写入异常的,而并不会在处理COW异常时调用fork(),且USTACKTOP-UTOP中除了user exception stack以外的空间是为了处理页写入时做缓冲区使用的,因此USTACKTOP-UTOP都不需要进行父子进程间的duppage映射.而UTOP-ULIM之间的空间是所有进程共享的,因此也不需要进行父子间的duppage映射.
因此只有0-USTACKTOP上的页面是需要使用duppage进行映射的.

Thinking 4.6

题面

1
2
3
4
5
在遍历地址空间存取页表项时你需要使用到 vpd 和 vpt 这两个指针,请参考 user/include/lib.h 中的相关定义,思考并回答这几个问题:
• vpt 和 vpd 的作用是什么?怎样使用它们?
• 从实现的角度谈一下为什么进程能够通过这种方式来存取自身的页表?
• 它们是如何体现自映射设计的?
• 进程能够通过这种方式来修改自己的页表项吗?

回答

定义如下:

1
2
#define vpt ((const volatile Pte *)UVPT)
#define vpd ((const volatile Pde *)(UVPT + (PDX(UVPT) << PGSHIFT)))
  1. 其中vpt代表的是虚空间上第一个二级页表的基地址,也就是页表的基地址;而vpd则代表的是虚空间上页目录的基地址.对于vpt,可以通过vpt[VPN(va)]获取va对应的页表项;而对于vpd,则可以通过vpd[PDX(va)]获取va对应的页目录项.
  2. OS初始化页表时,将页表和页目录的一部分映射到了虚空间中,使得这些虚地址直接指向页表和页目录的实际物理地址,这样使得进程可以通过vptvpd访问自身的页表项,进而实现相应的存取.
  3. 定义可以很好的体现自映射,也就是页目录本质上也是一个页表,考虑整体其作为一个页表,考虑其中的每一个页表项,其可以视为页目录项,用来映射到相应的二级页表.
  4. 不可以,这些区域对于用户是只读不写的,需要陷入内核才可对页表项进行相应的修改.

Thinking 4.7

题面

1
2
3
4
在 do_tlb_mod 函数中,你可能注意到了一个向异常处理栈复制 Trapframe运行现场的过程,请思考并回答这几个问题:
• 这里实现了一个支持类似于“异常重入”的机制,而在什么时候会出现这种“异常重
入”?
• 内核为什么需要将异常的现场 Trapframe 复制到用户空间?

回答

  1. 代码如下:
    struct Trapframe tmp_tf = *tf;
    if (tf->regs[29] < USTACKTOP || tf->regs[29] >= UXSTACKTOP) {
            tf->regs[29] = UXSTACKTOP;
    }
    tf->regs[29] -= sizeof(struct Trapframe);
    *(struct Trapframe *)tf->regs[29] = tmp_tf;
    
    此处的最后一行代码将原来的Trapframe运行现场通过解引用赋值给指针的方式完成了对于先前运行现场的保存,而这一般是在用户程序写入一个COW页后又写入一个COW页的时候会可能触发。至于为什么是可能,那是因为页写入异常处理函数一般是由用户态程序自行指定的,那么每个用户程序都可通过syscall_set_tlb_mod_entry()函数来设置定制的页写入异常处理函数,假如都是传入cow_entry()函数,或许问题不大,因为其并不会出现异常重入的情况,但是要是有别的函数被注册了,那就另当别论了。因此,为了防患于未然,实现异常重入机制还是很有必要的。
  2. 由于最终的异常处理是在用户态下进行的,而用户态下又无法访问高2GB内容,因此为了可以正确跳转到相应的用户异常处理程序,需要在异常的现场Trapframe设置好相应的用户异常处理程序入口后,写入用户空间,便于完成用户异常处理

Thinking 4.8

题面

1
在用户态处理页写入异常,相比于在内核态处理有什么优势?

回答

减少了内核出现错误的可能,也就是在用户态下程序的崩溃或者出错,并不会影响内核系统的稳定。同时用户态下进行新页面的分配映射等操作更加方便。

Thinking 4.9

题面

1
2
3
请思考并回答以下几个问题:
• 为什么需要将 syscall_set_tlb_mod_entry 的调用放置在 syscall_exofork 之前?
• 如果放置在写时复制保护机制完成之后会有怎样的效果?

回答

  1. 其实syscall_set_tlb_mod_entry并不一定需要放置在syscall_exofork之前,只需要放置在写时复制机制建立即可。因为根据下图,子进程在真正运行前还需要父进程通过duppage函数共享页面,同时设定写时复制机制并且修改子进程状态,使之进入就绪态。而在设置完写时复制机制后,父子进程中的全局变量近乎相同,则为了正常的完成用户异常写入处理,syscall_set_tlb_mod_entry必须在写时复制机制建立前完成。至于在syscall_exofork之前或之后并无所谓。

借用指导书中的图可知:

2. 放置在写时复制保护机制完成后则父子进程在对COW页进行写入时,并不会触发用户异常处理程序,而是会报错。

课下练习

Exercise 4.1

题面

1
填写 user/lib/syscall_wrap.S 中的 msyscall 函数,使得用户部分的系统调用机制可以正常工作。

回答

根据提示此处需要完成的任务是调用syscall指令后返回,因此需要在单写一行syscall指令后在下一行添上jr ra返回调用前存入的地址处。

Exercise 4.2

题面

1
根据 kern/syscall_all.c 中的提示,完成 do_syscall 函数,使得内核部分的系统调用机制可以正常工作。

回答

代码如下:

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
void do_syscall(struct Trapframe *tf) {
//定义一个函数
int (*func)(u_int, u_int, u_int, u_int, u_int);
//获取此前传入的$a0的值,也就是系统调用号,用于执行相应的系统调用
int sysno = tf->regs[4];
if (sysno < 0 || sysno >= MAX_SYSNO) {
//不在[0, MAX_SYSNO)范围内的系统调用号可以视为无效
//将返回值置异常,直接返回
tf->regs[2] = -E_NO_SYS;
return;
}

/* Step 1: Add the EPC in 'tf' by a word (size of an instruction). */
/* Exercise 4.2: Your code here. (1/4) */
//将EPC寄存器值自增4,也就是一个指令的长度
tf->cp0_epc += 4;
/* Step 2: Use 'sysno' to get 'func' from 'syscall_table'. */
/* Exercise 4.2: Your code here. (2/4) */
//依据相应的系统调用号在syscall_table中索引得到相应的系统调用
func = syscall_table(sysno);
/* Step 3: First 3 args are stored in $a1, $a2, $a3. */
//获取相应的参数
u_int arg1 = tf->regs[5];
u_int arg2 = tf->regs[6];
u_int arg3 = tf->regs[7];

/* Step 4: Last 2 args are stored in stack at [$sp + 16 bytes], [$sp + 20 bytes]. */
u_int arg4, arg5;
/* Exercise 4.2: Your code here. (3/4) */
//此处注意最后两个参数是存放在栈底偏16、20字节的位置
//也就是需要通过解引用获取相应地址对应的值的形式来获取
//这里是先将tf->regs[]的u_long转化成(u_int *)的形式,注意先转换,不然很有可能出现地址不对齐的情况,
//对应读取内存的话,可能会触发mips中的4号异常,注意观察调试时的异常号来确定异常类型
//然后再根据指针的偏移获取到相应位置的地址值,然后再通过解引用来获取值
arg4 = *((u_int *)(tf->regs[29]) + 4);
arg5 = *((u_int *)(tf->regs[29]) + 5);
/* Step 5: Invoke 'func' with retrieved arguments and store its return value to $v0 in 'tf'.
*/
/* Exercise 4.2: Your code here. (4/4) */
//类似回调函数的形式,执行对应系统调用的函数获取相应返回值
tf->regs[2] = func(arg1, arg2, arg3, arg4, arg5);
}

Exercise 4.3

题面

1
2
实现 kern/env.c 中的 envid2env 函数。
实现通过一个进程的 id 获取该进程控制块的功能。提示:可以利用 include/env.h 中的宏函数 ENVX() ,用于获取目标 Env 块在 env 数组中的下标。

回答

代码如下:

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
int envid2env(u_int envid, struct Env **penv, int checkperm) {
struct Env *e;
/* Step 1: Assign value to 'e' using 'envid'. */
/* Hint:
* If envid is zero, set 'penv' to 'curenv' and return 0.
* You may want to use 'ENVX'.
*/
/* Exercise 4.3: Your code here. (1/2) */
//根据相应的索引获取对应的进程控制块
e = &envs[ENVX(envid)];
//根据提示填写
if (envid == 0) {
*penv = curenv;
return 0;
}
//如果获取的进程压根不在运行或者已被销毁,返回-E_BAD_ENV
if (e->env_status == ENV_FREE || e->env_id != envid) {
return -E_BAD_ENV;
}

/* Step 2: Check when 'checkperm' is non-zero. */
/* Hints:
* Check whether the calling env has sufficient permissions to manipulate the
* specified env, i.e. 'e' is either 'curenv' or its immediate child.
* If violated, return '-E_BAD_ENV'.
*/
/* Exercise 4.3: Your code here. (2/2) */
//根据checkperm位对可进行操作的权限进行检查,不符合同样返回-E_BAD_ENV
if (checkperm != 0 ) {
if ( e != curenv && e->env_parent_id != curenv->env_id) {
return -E_BAD_ENV;
}
}
/* Step 3: Assign 'e' to '*penv'. */
*penv = e;
return 0;
}

Exercise 4.4

题面

1
实现 kern/syscall_all.c 中的 int sys_mem_alloc(u_int envid,u_int va, u_int perm) 函数。

回答

代码如下:

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
//include/error.h
#define try(expr) \
do { \
int _r = (expr); \
if (_r != 0) \
return _r; \
} while (0)

//kern/syscall_all.c
int sys_mem_alloc(u_int envid, u_int va, u_int perm) {
struct Env *env;
struct Page *pp;
/* Step 1: Check if 'va' is a legal user virtual address using 'is_illegal_va'. */
/* Exercise 4.4: Your code here. (1/3) */
//判断虚地址是否有效,至于这里为什么不用try,那是因为try宏是需要相应的传入函数返回值自行包含了异常值的情况
//而is_illegal_va函数仅仅是判断虚地址是否非法,具体的异常值-E_INVAL是需要通过判断非法后进行返回
if (is_illegal_va(va)) {
return -E_INVAL;
}
/* Step 2: Convert the envid to its corresponding 'struct Env *' using 'envid2env'. */
/* Hint: **Always** validate the permission in syscalls! */
/* Exercise 4.4: Your code here. (2/3) */
//由于envid2env函数自身包含了异常的返回值,因此可以使用try宏来简化编码
//此处需要尤其注意,在系统调用的函数中的checkperm始终都不能为0
//也就是一定需要进行判断给定进程id获取的进程是否为当前进程或者为当前进程的子进程
//根据给定的进程号获取相应的进程控制块
try(envid2env(envid, &env, 1));
/* Step 3: Allocate a physical page using 'page_alloc'. */
/* Exercise 4.4: Your code here. (3/3) */
//由于page_alloc函数的返回值也包含了异常值了,因此也可以直接使用try来调用
//为该进程分配一片物理页作为开辟的物理空间
try(page_alloc(&pp));
/* Step 4: Map the allocated page at 'va' with permission 'perm' using 'page_insert'. */
//在相应进程空间中建立虚实地址间的映射关系
return page_insert(env->env_pgdir, env->env_asid, pp, va, perm);
}

Exercise 4.5

题面

1
实现 kern/syscall_all.c 中的 int sys_mem_map(u_int srcid,u_int srcva, u_int dstid, u_int dstva, u_int perm) 函数。

回答

代码如下:

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
int sys_mem_map(u_int srcid, u_int srcva, u_int dstid, u_int dstva, u_int perm) {
struct Env *srcenv;
struct Env *dstenv;
struct Page *pp;
/* Step 1: Check if 'srcva' and 'dstva' are legal user virtual addresses using
* 'is_illegal_va'. */
/* Exercise 4.5: Your code here. (1/4) */
//判断两个进程给定的对应虚地址的合法性
if (is_illegal_va(srcva) || is_illegal_va(dstva)) {
return -E_INVAL;
}
/* Step 2: Convert the 'srcid' to its corresponding 'struct Env *' using 'envid2env'. */
/* Exercise 4.5: Your code here. (2/4) */
//调用try宏来获取源进程
try(envid2env(srcid, &srcenv, 1));
/* Step 3: Convert the 'dstid' to its corresponding 'struct Env *' using 'envid2env'. */
/* Exercise 4.5: Your code here. (3/4) */
//同样的方式来获取目标进程
try(envid2env(dstid, &dstenv, 1));
/* Step 4: Find the physical page mapped at 'srcva' in the address space of 'srcid'. */
/* Return -E_INVAL if 'srcva' is not mapped. */
/* Exercise 4.5: Your code here. (4/4) */
//调用page_lookup函数获取进程空间中srcva对应的物理页
//注意到在page_lookup中假若没有找到相应的页表项或者找到的并不是有效的,也就是没有建立好映射关系
//那就会返回NULL,此时应该返回-E_INVAL来说明srcva的错误
pp = page_lookup(srcenv->env_pgdir, srcva, NULL);
if (pp == NULL) {
return -E_INVAL;
}
/* Step 5: Map the physical page at 'dstva' in the address space of 'dstid'. */
return page_insert(dstenv->env_pgdir, dstenv->env_asid, pp, dstva, perm);
}

Exercise 4.6

题面

1
实现 kern/syscall_all.c 中的 int sys_mem_unmap(u_int envid, u_intva) 函数。

回答

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int sys_mem_unmap(u_int envid, u_int va) {
struct Env *e;
/* Step 1: Check if 'va' is a legal user virtual address using 'is_illegal_va'. */
/* Exercise 4.6: Your code here. (1/2) */
//判断地址合理性
if (is_illegal_va(va)) {
return -E_INVAL;
}
/* Step 2: Convert the envid to its corresponding 'struct Env *' using 'envid2env'. */
/* Exercise 4.6: Your code here. (2/2) */
//调用try宏来实现对于envid2env函数的监控
try(envid2env(envid, &e, 1));
/* Step 3: Unmap the physical page at 'va' in the address space of 'envid'. */
//调用page_remove函数来完成相应的删除映射工作
//至于此处为啥没有异常处理,那是因为原来没有建立映射的话,不就直接就完成了工作了()
page_remove(e->env_pgdir, e->env_asid, va);
return 0;
}

Exercise 4.7

题面

1
实现 kern/syscall_all.c 中的 void sys_yield(void) 函数。

回答

代码如下:

1
2
3
4
5
6
//注意该函数也是noreturn的属性,调用其的函数中在其后续运行的语句将会无效
void __attribute__((noreturn)) sys_yield(void) {
// Hint: Just use 'schedule' with 'yield' set.
/* Exercise 4.7: Your code here. */
schedule(1);
}

Exercise 4.8

题面

1
实现 kern/syscall_all.c 中的 int sys_ipc_recv(u_int dstva) 函数和int sys_ipc_try_send(u_int envid, u_int value, u_int srcva, u_int perm) 函数。请注意在修改进程控制块的状态后,应同步维护调度队列。

回答

代码如下:

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
int sys_ipc_recv(u_int dstva) {
/* Step 1: Check if 'dstva' is either zero or a legal address. */
//判断合法性
if (dstva != 0 && is_illegal_va(dstva)) {
return -E_INVAL;
}
/* Step 2: Set 'curenv->env_ipc_recving' to 1. */
/* Exercise 4.8: Your code here. (1/8) */
//设置当前进程为准备接受数据的状态
curenv->env_ipc_recving = 1;
/* Step 3: Set the value of 'curenv->env_ipc_dstva'. */
/* Exercise 4.8: Your code here. (2/8) */
//接收到的共享页面需要与自身虚空间中的哪个虚页面建立映射
curenv->env_ipc_dstva = dstva;
/* Step 4: Set the status of 'curenv' to 'ENV_NOT_RUNNABLE' and remove it from
* 'env_sched_list'. */
/* Exercise 4.8: Your code here. (3/8) */
//阻塞当前进程,注意此处需要手动将其从调度队列中删除,为了保证调度队列的一致性,
//因此在`schedule()`函数运行时对于当前进程阻塞的情况不需要去手动删除了
curenv->env_status = ENV_NOT_RUNNABLE;
TAILQ_REMOVE(&env_sched_list, curenv, env_sched_link);
/* Step 5: Give up the CPU and block until a message is received. */
//放弃CPU,阻塞直到接收到数据
((struct Trapframe *)KSTACKTOP - 1)->regs[2] = 0;
schedule(1);
}

int sys_ipc_try_send(u_int envid, u_int value, u_int srcva, u_int perm) {
struct Env *e;
struct Page *p;
/* Step 1: Check if 'srcva' is either zero or a legal address. */
/* Exercise 4.8: Your code here. (4/8) */
//判断地址合法性
if (srcva != 0 && is_illegal_va(srcva)) {
return -E_INVAL;
}
/* Step 2: Convert 'envid' to 'struct Env *e'. */
/* This is the only syscall where the 'envid2env' should be used with 'checkperm' UNSET,
* because the target env is not restricted to 'curenv''s children. */
/* Exercise 4.8: Your code here. (5/8) */
//通过try宏和envid2env函数获取envid对应的PCB
try(envid2env(envid, &e, 0));
/* Step 3: Check if the target is waiting for a message. */
/* Exercise 4.8: Your code here. (6/8) */
//如果接受进程并不是在准备接受数据的状态,那么就返回异常值-E_IPC_NOT_RECV
if (e->env_ipc_recving != 1) {
return -E_IPC_NOT_RECV;
}
/* Step 4: Set the target's ipc fields. */
e->env_ipc_value = value;
e->env_ipc_from = curenv->env_id;
e->env_ipc_perm = PTE_V | perm;
e->env_ipc_recving = 0;

/* Step 5: Set the target's status to 'ENV_RUNNABLE' again and insert it to the tail of
* 'env_sched_list'. */
/* Exercise 4.8: Your code here. (7/8) */
//完成了相应的数据设置后,将该进程状态改为就绪状态并将其重新插回就绪队列中,插入尾部
e->env_status = ENV_RUNNABLE;
TAILQ_INSERT_TAIL(&env_sched_list, e, env_sched_link);
/* Step 6: If 'srcva' is not zero, map the page at 'srcva' in 'curenv' to 'e->env_ipc_dstva'
* in 'e'. */
/* Return -E_INVAL if 'srcva' is not zero and not mapped in 'curenv'. */
if (srcva != 0) {
/* Exercise 4.8: Your code here. (8/8) */
//在srcva不为0的情况下,利用page_lookup找到对应的物理页
//由于page_lookup函数不自带异常返回值的处理
//因此,需要单独判断
p = page_lookup(curenv->env_pgdir, srcva, NULL);
if (p == NULL) {
return -E_INVAL;
}
//利用page_insert来完成curenv中的相应物理页与e中的接受虚地址e->env_ipc_dstva的映射关系的建立
//同时perm是相应的给定参数
try(page_insert(e->env_pgdir, e->env_asid, p, e->env_ipc_dstva, perm));
}
return 0;
}

Exercise 4.9

题面

1
请根据上述步骤以及代码中的注释提示,填写 kern/syscall_all.c 中的sys_exofork 函数。

回答

代码如下:

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
int sys_exofork(void) {
struct Env *e;
/* Step 1: Allocate a new env using 'env_alloc'. */
/* Exercise 4.9: Your code here. (1/4) */
//在完成此函数的时候需要默认的一点是,当前进程curenv应该是作为新创建进程的父进程
//不然该函数将毫无意义
//因此此处的parentid应该是当前结点的env_id
try(env_alloc(&e, curenv->env_id));
/* Step 2: Copy the current Trapframe below 'KSTACKTOP' to the new env's 'env_tf'. */
/* Exercise 4.9: Your code here. (2/4) */
//由于在栈结构中,栈顶一般指向的是填入数据末尾的下一段空空间
//因此需要先将其转化为对应的结构指针然后自减1,回到栈内容末尾
//并通过解引用的形式获取相应的结构体变量值
e->env_tf = *((struct Trapframe *)KSTACKTOP - 1);
/* Step 3: Set the new env's 'env_tf.regs[2]' to 0 to indicate the return value in child. */
/* Exercise 4.9: Your code here. (3/4) */
//由于子进程的fork返回值是通过相应栈空间中的$v0寄存器的值来存储
//因此需要进行相应的赋值
e->env_tf.regs[2] = 0;
/* Step 4: Set up the new env's 'env_status' and 'env_pri'. */
/* Exercise 4.9: Your code here. (4/4) */
//子进程尚未准备好需要置阻塞状态,且优先级应该同父进程一致
e->env_status = ENV_NOT_RUNNABLE;
e->env_pri = curenv->env_pri;
//返回的子进程id,说明父进程的fork返回值通过该函数的返回值决定
return e->env_id;
}

Exercise 4.10

题面

1
结合代码注释以及上述提示,填写 user/lib/fork.c 中的 duppage 函数。

回答

代码如下:

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
static void duppage(u_int envid, u_int vpn) {
int r;
u_int addr;
u_int perm;
/* Step 1: Get the permission of the page. */
/* Hint: Use 'vpt' to find the page table entry. */
/* Exercise 4.10: Your code here. (1/2) */
//通过PTE_FLAGS宏获取相应的权限位
perm = PTE_FLAGS(vpt[vpn]);
/* Step 2: If the page is writable, and not shared with children, and not marked as COW yet,
* then map it as copy-on-write, both in the parent (0) and the child (envid). */
/* Hint: The page should be first mapped to the child before remapped in the parent. (Why?)
*/
/* Exercise 4.10: Your code here. (2/2) */
//获取vpn对应的基地址,便于后续进行建立映射关系
addr = vpn << PGSHIFT;
//权限位可写且不可共享且不是处于写时复制状态
if ((perm & PTE_D) != 0 && (perm & PTE_LIBRARY) == 0 && (perm & PTE_COW) == 0) {
//修改权限位为不可写(便于后续父子进程运行过程中进行写入异常)且为写时复制状态
perm = (perm & (~PTE_D));
perm = (perm | PTE_COW);
//先进行原父进程向子进程的地址映射,带有修改后的权限位
if ((r = syscall_mem_map(0, (void *)addr, envid, (void *)addr, perm)) < 0) {
user_panic("user panic mem map error: %d", r);
}
//再更新父进程中相应地址对应的权限位
//由于duppage函数执行时间较长,期间若发生时钟中断,而恰好在父进程map结束且尚未进行子进程map时发生中断,
//若此时恰好对父进程相应的已经设置好的页进行写入,那么将触发写时复制机制,父进程将指向新页,而新页并没有PTE_COW
//此时再map子进程,子进程对应页上带有PTE_COW,而父进程却没有
//此后的程序运行过程中,若父进程对该页进行了修改,由于缺失PTE_COW,导致无法进行写时复制,也就是修改的页与子进程是同一页,也就破坏了写时复制机制
//导致子进程运行错误,也就是对子进程该页本不需改,却因为父进程而被一同修改了。
if ((r = syscall_mem_map(0, (void *)addr, 0, (void *)addr, perm)) < 0) {
user_panic("user panic mem map error: %d", r);
}
}
else {
//否则的话,不需要修改对应的权限位,直接将对应的权限位映射给子进程,原封不动
if ((r = syscall_mem_map(0, (void *)addr, envid, (void *)addr, perm)) < 0) {
user_panic("user panic mem map error: %d", r);
}
}
}

Exercise 4.11

题面

1
根据上述提示以及代码注释,完成 kern/tlbex.c 中的 do_tlb_mod 函数,设置好保存的现场中 EPC 寄存器的值。

回答

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void do_tlb_mod(struct Trapframe *tf) {
//将当前的现场压入异常处理栈中
struct Trapframe tmp_tf = *tf;
if (tf->regs[29] < USTACKTOP || tf->regs[29] >= UXSTACKTOP) {
tf->regs[29] = UXSTACKTOP;
}
tf->regs[29] -= sizeof(struct Trapframe);
*(struct Trapframe *)tf->regs[29] = tmp_tf;
//查询异常所在的二级页表项所在处
Pte *pte;
page_lookup(cur_pgdir, tf->cp0_badvaddr, &pte);
//假若提前设置了异常处理程序入口
if (curenv->env_user_tlb_mod_entry) {
//进行相应的寄存器赋值,其中regs[4]代表$a0,存储存储了异常处理栈中保存的现场的地址,并以其为参数传给相应的用户异常处理程序,进行异常处理
tf->regs[4] = tf->regs[29];
tf->regs[29] -= sizeof(tf->regs[4]);
// Hint: Set 'cp0_epc' in the context 'tf' to 'curenv->env_user_tlb_mod_entry'.
/* Exercise 4.11: Your code here. */
//将epc寄存器值设置为用户异常处理程序入口,便于后续跳转
tf->cp0_epc = curenv->env_user_tlb_mod_entry;
} else {
panic("TLB Mod but no user handler registered");
}
}

Exercise 4.12

题面

1
完成 kern/syscall_all.c 中的 sys_set_tlb_mod_entry 函数。

回答

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
int sys_set_tlb_mod_entry(u_int envid, u_int func) {
struct Env *env;

/* Step 1: Convert the envid to its corresponding 'struct Env *' using 'envid2env'. */
/* Exercise 4.12: Your code here. (1/2) */
//以检查的方式进行envid向对应的env控制块的转换
try(envid2env(envid, &env, 1));
/* Step 2: Set its 'env_user_tlb_mod_entry' to 'func'. */
/* Exercise 4.12: Your code here. (2/2) */
//设置对应的用户异常处理程序入口为对应的函数指针,也就是函数地址
env->env_user_tlb_mod_entry = func;
return 0;
}

Exercise 4.13

题面

1
填写 user/lib/fork.c 中的 cow_entry 函数。

回答

代码如下:

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
static void __attribute__((noreturn)) cow_entry(struct Trapframe *tf) {
u_int va = tf->cp0_badvaddr;
u_int perm;
/* Step 1: Find the 'perm' in which the faulting address 'va' is mapped. */
/* Hint: Use 'vpt' and 'VPN' to find the page table entry. If the 'perm' doesn't have
* 'PTE_COW', launch a 'user_panic'. */
/* Exercise 4.13: Your code here. (1/6) */
//获取相应二级页表项的权限位,判断对于相应物理页框的写读权限
perm = PTE_FLAGS(vpt[VPN(va)]);
//如果并不是写时复制,那么需要报错,说明调用有误
if ((perm & PTE_COW) == 0) {
user_panic("PTE_COW not found, va=%08x, perm=%08x", va, perm);
}
/* Step 2: Remove 'PTE_COW' from the 'perm', and add 'PTE_D' to it. */
/* Exercise 4.13: Your code here. (2/6) */
//删去写时复制位,并添加上可写位
perm &= (~PTE_COW);
perm |= PTE_D;
/* Step 3: Allocate a new page at 'UCOW'. */
/* Exercise 4.13: Your code here. (3/6) */
//在UCOW(0x003f f000)处申请分配一块临时的物理页,用于存放va所在页上的数据
try(syscall_mem_alloc(0, (void *) UCOW, perm));
/* Step 4: Copy the content of the faulting page at 'va' to 'UCOW'. */
/* Hint: 'va' may not be aligned to a page! */
/* Exercise 4.13: Your code here. (4/6) */
//拷贝数据
memcpy((void *) UCOW, (void *) ROUNDDOWN(va, PAGE_SIZE), PAGE_SIZE);
// Step 5: Map the page at 'UCOW' to 'va' with the new 'perm'.
/* Exercise 4.13: Your code here. (5/6) */
//将发生异常地址va映射到临时页面上,设定相应的新的权限位
try(syscall_mem_map(0, (void *)UCOW, 0, (void *)va, perm));
// Step 6: Unmap the page at 'UCOW'.
/* Exercise 4.13: Your code here. (6/6) */
//解除UCOW对于临时页面的内存映射,相当于进行了一个置换,用UCOW去申请一个新页面,将va映射到新页面后,完成使命,UCOW解除映射关系
try(syscall_mem_unmap(0, (void *) UCOW));
// Step 7: Return to the faulting routine.
int r = syscall_set_trapframe(0, tf);
user_panic("syscall_set_trapframe returned %d", r);
}

Exercise 4.14

题面

1
填写 kern/syscall_all.c 中的 sys_set_env_status 函数。

回答

代码如下:

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
int sys_set_env_status(u_int envid, u_int status) {
struct Env *env;
/* Step 1: Check if 'status' is valid. */
/* Exercise 4.14: Your code here. (1/3) */
//检查要修改为的状态只能是ENV_RUNNABLE和 ENV_NOT_RUNNABLE两种
if (status != ENV_RUNNABLE && status != ENV_NOT_RUNNABLE) {
return -E_INVAL;
}
/* Step 2: Convert the envid to its corresponding 'struct Env *' using 'envid2env'. */
/* Exercise 4.14: Your code here. (2/3) */
//获得相应的进程控制块
try(envid2env(envid, &env, 1));
/* Step 3: Update 'env_sched_list' if the 'env_status' of 'env' is being changed. */
/* Exercise 4.14: Your code here. (3/3) */
//只有再进程状态与变动状态不一致时需要去更新env_sched_list,否则不用做改动
if (status != env->env_status) {
if (status == ENV_NOT_RUNNABLE) {
TAILQ_REMOVE(&env_sched_list, env, env_sched_link);
}
else {
TAILQ_INSERT_TAIL(&env_sched_list, env, env_sched_link);
}
}
/* Step 4: Set the 'env_status' of 'env'. */
env->env_status = status;

/* Step 5: Use 'schedule' with 'yield' set if ths 'env' is 'curenv'. */
if (env == curenv) {
schedule(1);
}
return 0;
}

Exercise 4.15

题面

1
填写 user/lib/fork.c 中的 fork 函数。

回答

代码如下:

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
//user/lib/libos.c
...
const volatile struct Env *env;
extern int main(int, char **);
void libmain(int argc, char **argv) {
// set env to point at our env structure in envs[].
env = &envs[ENVX(syscall_getenvid())];

// call user main routine
main(argc, argv);

// exit gracefully
exit();
}

int fork(void) {
u_int child;
u_int i;
/* Step 1: Set our TLB Mod user exception entry to 'cow_entry' if not done yet. */
//此处的env并不是凭空出现的,其定义在user/lib/libos.c中的libmain()函数中,作为初始的进程
if (env->env_user_tlb_mod_entry != (u_int)cow_entry) {
syscall_set_tlb_mod_entry(0, cow_entry);
}
/* Step 2: Create a child env that's not ready to be scheduled. */
// Hint: 'env' should always point to the current env itself, so we should fix it to the
// correct value.
//通过syscall_exofork()函数获取对应的子进程号
child = syscall_exofork();
if (child == 0) {
//子进程在被syscall_exofork创建后,
//直接执行该if判断,由于提前将其进程上下文中的返回值$v0设置为0
//因此子进程中会直接入该if判断中执行如下操作,
//并返回子进程下的pid,0
env = envs + ENVX(syscall_getenvid());
return 0;
}
/* Step 3: Map all mapped pages below 'USTACKTOP' into the child's address space. */
// Hint: You should use 'duppage'.
/* Exercise 4.15: Your code here. (1/2) */
//此处只能通过如下朴素的遍历方式来判断相应二级页表项的有效性
//不能使用page_lookup()来寻找对应有效的物理页的原因是page_lookup()是内核态下的方法
//而该函数实现在用户态下,压根没有相应的声明和定义
for (i = 0; i <= PDX(USTACKTOP); i++) {
if ((vpd[i] & PTE_V) != 0) {
for (u_int j = 0; j < 1024; j++) {
u_long va = (i * 1024 + j) << PGSHIFT;
if (va >= USTACKTOP) {
break;
}
if ((vpt[VPN(va)] & PTE_V) != 0) {
//将父进程中有效的页面共享给子进程
duppage(child, VPN(va));
}
}
}
}
/* Step 4: Set up the child's tlb mod handler and set child's 'env_status' to
* 'ENV_RUNNABLE'. */
/* Hint:
* You may use 'syscall_set_tlb_mod_entry' and 'syscall_set_env_status'
* Child's TLB Mod user exception entry should handle COW, so set it to 'cow_entry'
*/
/* Exercise 4.15: Your code here. (2/2) */
//通过系统调用设置子进程的用户异常处理程序和运行状态
syscall_set_tlb_mod_entry(child, cow_entry);
syscall_set_env_status(child, ENV_RUNNABLE);
//返回子进程号,完成fork()函数
return child;
}

其中调用的syscall_exofork()函数并不是在user/lib/syscall_lib.c中,而是在user/include/lib.h中:

1
2
3
__attribute__((always_inline)) inline static int syscall_exofork(void) {
return msyscall(SYS_exofork, 0, 0, 0, 0, 0);
}

返回的是msyscall()函数的调用值,而其返回值一般存储在$v0寄存器中,因此对于父进程返回的是内核态运行的sys_exofork()函数的返回值——子进程的进程号,因为这里sys_exofork()函数return的本质实质上翻译为汇编代码,就是向$v0$v1写入相应的返回值;而对于子进程则返回的是其在父进程的sys_exofork()中设置的$v0的0值。因此本质上读取函数返回值就是去读取相应的$v0$v1的值。

lab4课下难点

本次lab主要学习了**系统调用、进程间的IPC通信机制和特殊的系统调用fork()**。

系统调用

简单的来说,就是为了内核的安全的同时,方便在用户态下进行一些权限操作而由操作系统提供的接口,便于用户态来通过该接口调用相应的内核函数执行操作。

具体流程如上图,在用户态中调用相应的**syscall_***函数,其只封装了msyscall函数:

1
2
3
xxx syscall_*(...) {
(return) msyscall(SYS_xxx, ...);
}

其中msyscall函数的第一个参数为系统调用号,是区别于不同系统调用的标识,后续几个参数则是该函数的参数,而它们该如何好好的传入内核态呢?这就要提到之前学到的栈帧的形式,msyscall中会调用syscall陷入内核态,其实就是跳转到了.text.exc_gen_entry中,在SAVE_ALL保存了当前现场后,会根据对应的异常号跳转到相应的处理函数,这里跳转到的就是8号系统异常,并在do_syscall中根据传入的第一个参数也就是系统调用号获取相应的内核系统调用函数指针,并将相应的参数从对应寄存器和地址中取出(由于目前的系统调用至多5个参数,因此至多只需要多存取2个参数,其中前4个参数分别存入$a0-$a3中),因此分别从$a1-$a3中获取剩下的第二至四个参数,而第五和第六个参数分别存在$sp+16$sp+20(字节)处,注意此处需要通过解引用的方式进行取值

此后do_syscall将获取的相应参数作为参数调用相应的函数完成系统调用

如下是几个基础系统调用函数

sys_mem_alloc函数

分配内存,用户程序可给该程序所允许的虚存空间显式地分配实际物理内存。站在编写者的视角也就是编写的程序在内存中申请了一片空间。而对于OS,则是一个进程请求将其运行空间中的某段地址与实际物理内存形成了映射。此处注意需要检查虚地址的合法性。

sys_mem_map函数

  1. 将源进程地址空间中相应内存映射到目标进程的相应地址空间的相应虚内存中,也就是这两者共享一页的物理内存。
  2. 操作逻辑大致如下:
    • 找到两个进程(使用envid2env函数)。
    • 获取源进程的虚拟页面对应的实际物理页面(用page_lookup函数)。
    • 将该物理页同目标进程的相应地址进行映射(用page_insert函数)。
    • 完成上述操作后检查虚地址合法性。

sys_mem_unmap函数

对标sys_mem_map函数,该系统调用是用来解除某个进程地址空间虚内存和物理内存间的映射关系。

sys_yield(void)函数

其用来实现用户进程对CPU的放弃,从而去调度其他进程。

IPC

借用指导书中的图来说,IPC就是通过发送方向接收方的PCB传递一个值或者共享一页来实现两个进程直接的通信,因此在struct Env 结构体中增加了如下的成员:

1
2
3
4
5
6
7
8
9
10
//进程传递的具体数值
u_int env_ipc_value; // the value sent to us
//发送方ID
u_int env_ipc_from; // envid of the sender
//接收方接收数据状态,1表示等待接受、0表示不可接受
u_int env_ipc_recving; // whether this env is blocked receiving
//接收到的页面需与自身的哪个需页面完成映射
u_int env_ipc_dstva; // va at which the received page should be mapped
//传递页面的权限位设置
u_int env_ipc_perm; // perm in which the received page should be mapped

并通过两个重要的系统调用来实现IPC机制:int sys_ipc_recv(u_int dstva)函数和int sys_ipc_try_send(u_int envid, u_int value, u_int srcva, u_int perm)函数。

前者负责接受信息:

  1. 先给自身的env_ipc_recving置位,表示准备接受消息了。

  2. env_ipc_dstva赋值,表明要将接受的页面与dstva完成映射。

  3. 阻塞当前进程,注意此处需要手动将其从调度队列中删除,为了保证调度队列的一致性,因此在schedule()函数运行时对于当前进程阻塞的情况不需要去手动删除了

  4. 放弃CPU(同时重新进行调度),等待发送方传递数据。

后者负责发送信息:

  1. 根据envid找到相应进程,若对应进程为可接收状态,则发送成功。
  2. 否则返回异常值-E_IPC_NOT_RECV,表示目标进程未处于接收状态。
  3. 清除接收进程接收状态,填入数据到PCB,传递物理页映射关系。
  4. 修改PCB中进程状态,使接受数据进程可继续运行。
  5. 当传入的srcva不为0时,会利用page_insert来完成curenv中的相应物理页与e中的接受虚地址e->env_ipc_dstva的映射关系的建立,不过,在用户程序中会大量使用srcva == 0的系统调用来只进行value的传递,而不用传递相应物理页面.

Fork

根据上述指导书中的流程图可以看出,fork系统调用就是父进程通过一系列操作创建一个与之近乎一致的子进程,来完成某些特定的操作的过程。

一开始,父进程调用syscall_set_tlb_mod_entry系统调用设置本身的TLB Mod异常处理函数,为之后的写时复制机制做准备,然后调用syscall_exofork()来创建一个子进程,此处的syscall_exofork()函数可大有来头,其中藏匿了fork()函数两个返回值的真相

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int sys_exofork(void) {
struct Env *e;
//在完成此函数的时候需要默认的一点是,当前进程curenv应该是作为新创建进程的父进程
//不然该函数将毫无意义
//因此此处的parentid应该是当前结点的env_id
try(env_alloc(&e, curenv->env_id));
//由于在栈结构中,栈顶一般指向的是填入数据末尾的下一段空空间
//因此需要先将其转化为对应的结构指针然后自减1,回到栈内容末尾
//并通过解引用的形式获取相应的结构体变量值
e->env_tf = *((struct Trapframe *)KSTACKTOP - 1);
//由于子进程的fork返回值是通过相应栈空间中的$v0寄存器的值来存储
//因此需要进行相应的赋值
e->env_tf.regs[2] = 0;
//子进程尚未准备好需要置阻塞状态,且优先级应该同父进程一致
e->env_status = ENV_NOT_RUNNABLE;
e->env_pri = curenv->env_pri;
//返回的子进程id,说明父进程的fork返回值通过该函数的返回值决定
return e->env_id;
}

其中调用的syscall_exofork()函数并不是在user/lib/syscall_lib.c中,而是在user/include/lib.h中:

1
2
3
__attribute__((always_inline)) inline static int syscall_exofork(void) {
return msyscall(SYS_exofork, 0, 0, 0, 0, 0);
}

返回的是msyscall()函数的调用值,而其返回值一般存储在$v0寄存器中,因此对于父进程返回的是内核态运行的sys_exofork()函数的返回值——子进程的进程号,因为这里sys_exofork()函数return的本质实质上翻译为汇编代码,就是向$v0$v1写入相应的返回值;而对于子进程则返回的是其在父进程的sys_exofork()中设置的$v0的0值。其实本质上读取函数返回值就是去读取相应的$v0$v1的值。

而为了父子进程能共用尽可能多的物理内存,引入写时复制技术(COW):fork时将地址空间中所有可写页标记为写时复制页;根据标记,当父子进程对写时复制页进行写入时,会产生TLB写入异常并由OS去处理该异常:1).为当前进程试图写入虚地址重新分配物理页;2).新页面复制原有页内容;3).返回用户程序(放心去写吧);处理完即可对新物理页进行写入;

通过设置标志位来完成”写时复制机制”,其中PTE_DDirty位,表示当前页面是否可写,为0则不可写;为了区分”只读”页和”写时复制”页,新增一个PTE_COW表示当前页面是否是”写时复制”页,因为”写时复制”页的PTE_D位也应该是0.

此处只对于可写(PTE_D不为0)、不共享(PTE_LIBRARY为0)以及不为写时复制页(PTE_COW为0)的页进行关闭可写位和置写时复制位的操作,而其余页则原封不动的共享给子进程即可(为什么呢?因为不可写的页只读不会被修改、共享的页本身就是可以被两者修改的、而已经置写时复制位的页则已经完成了任务)。

此后父进程再次调用syscall_set_tlb_mod_entry系统调用为子进程设置TLB mod异常的处理函数的入口地址并将子进程的状态由一开始的阻塞态通过syscall_set_env_status(child, ENV_RUNNABLE)设置为就绪态。

待子进程被调度到的时候,由于其进程上下文与父进程进行syscall_exofork()时一致,其将接着该函数运行,并由于在syscall_exofork()函数为子进程设置了返回值为0,因此子进程中的child值为0,将进入if判断语句,完成env设置为当前的子进程的操作后,返回0给调用fork()的子进程,由此在调用fork()函数的函数中来区别父子进程,进而实现所需的不同操作,完成fork()系统调用的使命。

心得体会

此次lab增加的基础系统调用、IPC和特殊的系统调用fork都是较为难理解的概念,我用了接近30个小时,才得以完成,其中更是有很多精致的设计,例如:写时复制机制等。

虽然题目多,概念晦涩,但是通过反复阅读指导书并且结合代码实现,搞懂了相应的机制真的让我很有成就感,尤其是曾经高高在上的fork的两个返回值的理念,如今也不再让我祛魅了。

总的来说,这次lab虽然难度较大,但是理解并完成后还是大有裨益,极为丰富的。