uCoreOs lab5 实验报告

实验目的

  • 了解第一个用户进程创建过程
  • 了解系统调用框架的实现机制
  • 了解ucore如何实现系统调用sys_fork/sys_exec/sys_exit/sys_wait来进行进程管理

实验内容

练习0:填写已有实验

本实验依赖实验1/2/3/4。请把你做的实验1/2/3/4的代码填入本实验中代码中有“LAB1”/“LAB2”/“LAB3”/“LAB4”的注释相应部分。注意:为了能够正确执行lab5的测试应用程序,可能需对已完成的实验1/2/3/4的代码进行进一步改进。

最麻烦的一部分

要修改的文件有proc.c default_pmm.c pmm.c swap_fifo.c vmm.c trap.c kdebug.c

另外,有部分需要在原代码基础上增添新内容

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
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
proc.c
//LAB5 YOUR CODE : (update LAB4 steps)
/*
* below fields(add in LAB5) in proc_struct need to be initialized
* uint32_t wait_state; // waiting state
* struct proc_struct *cptr, *yptr, *optr; // relations between processes
*/
+ proc->wait_state = 0;
+ proc->cptr = proc->optr = proc->yptr = NULL;

default_pmm.c
//LAB5 YOUR CODE : (update LAB4 steps)
/* Some Functions
* set_links: set the relation links of process. ALSO SEE: remove_links: lean the relation links of process
* -------------------
* update step 1: set child proc's parent to current process, make sure current process's wait_state is 0
* update step 5: insert proc_struct into hash_list && proc_list, set the relation links of process
*/
// 1. call alloc_proc to allocate a proc_struct
proc = alloc_proc();
if (!proc) {
goto fork_out;
}
proc->parent = current;
+ assert(current->wait_state == 0);//确保当前进程正在等待
// 2. call setup_kstack to allocate a kernel stack for child process
if (setup_kstack(proc) != 0) {
goto bad_fork_cleanup_proc;
}
// 3. call copy_mm to dup OR share mm according clone_flag
if (copy_mm(clone_flags, proc) != 0) {
goto bad_fork_cleanup_kstack;
}
// 4. call copy_thread to setup tf & context in proc_struct
copy_thread(proc, stack, tf);
// 5. insert proc_struct into hash_list && proc_list
bool intr_flag;
local_intr_save(intr_flag);
proc->pid = get_pid();
- //list_add(&proc_list, &proc->list_link);
hash_proc(proc);
- //++nr_process;
+ set_links(proc);
//将原来简单的计数改成来执行set_links函数,从而实现设置进程的相关链接
//因为要涉及到进程的调度
//在set_links中包含了list_add和set_links操作
//set_links函数在proc.c(145)中
// set_links - set the relation links of process
//static void
//set_links(struct proc_struct *proc) {
// list_add(&proc_list, &(proc->list_link));
// proc->yptr = NULL;
// if ((proc->optr = proc->parent->cptr) != NULL) {
// proc->optr->yptr = proc;
// }
// proc->parent->cptr = proc;
// nr_process ++;
//}
local_intr_restore(intr_flag);
// 6. call wakeup_proc to make the new child process RUNNABLE
wakeup_proc(proc);
// 7. set ret vaule using child proc's pid
ret = proc->pid;

trap.c

/* LAB5 YOUR CODE */
//you should update your lab1 code (just add ONE or TWO lines of code), let user app to use syscall to get the service of ucore
//so you should setup the syscall interrupt gate in here
extern uintptr_t __vectors[];
int i;
for(i = 0; i < 256; i ++){
SETGATE(idt[i], 0, GD_KTEXT, __vectors[i], DPL_KERNEL);
}
- //SETGATE(idt[T_SWITCH_TOK], 0, GD_KTEXT, __vectors[T_SWITCH_TOK], DPL_USER);
+ SETGATE(idt[T_SYSCALL], 1, GD_KTEXT, __vectors[T_SYSCALL], DPL_USER);
//这里主要是设置相应的中断门
lidt(&idt_pd);
/*
传入的第一个参数gate是中断的描述符表
传入的第二个参数istrap用来判断是中断还是trap
传入的第三个参数sel的作用是进行段的选择
传入的第四个参数off表示偏移
传入的第五个参数dpl表示这个中断的优先级
*/

/* LAB5 YOUR CODE */
/* you should upate you lab1 code (just add ONE or TWO lines of code):
* Every TICK_NUM cycle, you should set current process's current->need_resched = 1
*/
ticks++;
if(ticks%TICK_NUM == 0){//每次时钟中断之后ticks就会加一 当加到TICK_NUM次数时 打印并重新开始
- //print_ticks();//前面有定义 打印字符串
+ assert(current != NULL);
+ current->need_resched = 1;
//将时间片设置为需要调度 说明当前进程的时间片已经用完了
}

练习1: 加载应用程序并执行

do_execv函数调用load_icode(位于kern/process/proc.c中)来加载并解析一个处于内存中的ELF执行文件格式的应用程序,建立相应的用户内存空间来放置应用程序的代码段、数据段等,且要设置好proc_struct结构中的成员变量trapframe中的内容,确保在执行此进程后,能够从应用程序设定的起始执行地址开始执行。需设置正确的trapframe内容。

请在实验报告中简要说明你的设计实现过程。

请在实验报告中描述当创建一个用户态进程并加载了应用程序后,CPU是如何让这个应用程序最终在用户态执行起来的。即这个用户态进程被ucore选择占用CPU执行(RUNNING态)到具体执行应用程序第一条指令的整个经过。

实验过程

查看proc.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/* load_icode - load the content of binary program(ELF format) as the new content of current process
* @binary: the memory addr of the content of binary program
* @size: the size of the content of binary program
*/
static int
load_icode(unsigned char *binary, size_t size) {
...
//(6) setup trapframe for user environment
struct trapframe *tf = current->tf;
memset(tf, 0, sizeof(struct trapframe));
/* LAB5:EXERCISE1 YOUR CODE
* should set tf_cs,tf_ds,tf_es,tf_ss,tf_esp,tf_eip,tf_eflags
* NOTICE: If we set trapframe correctly, then the user level process can return to USER MODE from kernel. So
* tf_cs should be USER_CS segment (see memlayout.h)
* tf_ds=tf_es=tf_ss should be USER_DS segment
* tf_esp should be the top addr of user stack (USTACKTOP)
* tf_eip should be the entry point of this binary program (elf->e_entry)
* tf_eflags should be set to enable computer to produce Interrupt
*/
ret = 0;
...
}

根据注释补充代码得到

1
2
3
4
5
6
7
8
//以下为要填写的内容 首先清空进程原先的中断帧 然后再将 中断帧中的 代码段 和 数据段
//修改为 用户态的段选择子 栈指针设置为 用户栈顶 eip 设置为 用户程序的入口地址
//最后 确保在用户进程中能够响应中断
tf->tf_cs = USER_CS;
tf->tf_ds = tf->tf_es = tf->tf_ss = USER_DS;
tf->tf_esp = USTACKTOP;
tf->tf_eip = elf->e_entry;
tf->tf_eflags = FL_IF;//FL_IF为开启中断状态

回答问题

请在实验报告中描述当创建一个用户态进程并加载了应用程序后,CPU是如何让这个应用程序最终在用户态执行起来的。即这个用户态进程被ucore选择占用CPU执行(RUNNING态)到具体执行应用程序第一条指令的整个经过。

查看user/hello.c

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>
#include <ulib.h>

int
main(void) {
cprintf("Hello world!!.\n");
cprintf("I am process %d.\n", getpid());
cprintf("hello pass.\n");
return 0;
}

用户进程的虚拟地址空间

tools/user.ld描述了用户程序的用户虚拟空间的执行入口虚拟地址:

1
2
3
SECTIONS {
/* Load programs at this address: "." means the current address */
. = 0x800020;

tools/kernel.ld描述了操作系统的内核虚拟空间的起始入口虚拟地址:

1
2
3
SECTIONS {
/* Load the kernel at this address: "." means the current address */
. = 0xC0100000;

这样ucore将用户进程的虚拟地址空间分为了两个部分,
一部分是所有用户进程共享的内核地址空间,映射到同样的物理地址空间中。将内核代码放到此空间中,用户进程从用户态到内核态时,内核可以统一应对不同的内核程序。
另外一部分是用户虚拟地址空间,映射到不同且没有交集的物理内存空间中。用户进程的执行代码和数据放到用户地址空间时确保各个进程不会非法访问到其他进程的物理空间。

./kern/mm/memlayout.h中的虚存布局

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
/* *
* Virtual memory map: Permissions
* kernel/user
*
* 4G ------------------> +---------------------------------+
* | |
* | Empty Memory (*) |
* | |
* +---------------------------------+ 0xFB000000
* | Cur. Page Table (Kern, RW) | RW/-- PTSIZE
* VPT -----------------> +---------------------------------+ 0xFAC00000
* | Invalid Memory (*) | --/--
* KERNTOP -------------> +---------------------------------+ 0xF8000000
* | |
* | Remapped Physical Memory | RW/-- KMEMSIZE
* | |
* KERNBASE ------------> +---------------------------------+ 0xC0000000
* | Invalid Memory (*) | --/--
* USERTOP -------------> +---------------------------------+ 0xB0000000
* | User stack |
* +---------------------------------+
* | |
* : :
* | ~~~~~~~~~~~~~~~~ |
* : :
* | |
* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
* | User Program & Heap |
* UTEXT ---------------> +---------------------------------+ 0x00800000
* | Invalid Memory (*) | --/--
* | - - - - - - - - - - - - - - - |
* | User STAB Data (optional) |
* USERBASE, USTAB------> +---------------------------------+ 0x00200000
* | Invalid Memory (*) | --/--
* 0 -------------------> +---------------------------------+ 0x00000000
* (*) Note: The kernel ensures that "Invalid Memory" is *never* mapped.
* "Empty Memory" is normally unmapped, but user programs may map pages
* there if desired.
*
* */

创建并执行用户进程

lab5中第一个进程是由第二个内核线程initproc通过把hello应用程序执行码覆盖到initproc的用户执行空间来创建的,相关代码如下所示:

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
#define __KERNEL_EXECVE(name, binary, size) ({                          \
cprintf("kernel_execve: pid = %d, name = \"%s\".\n", \
current->pid, name); \
kernel_execve(name, binary, (size_t)(size)); \
})

#define KERNEL_EXECVE(x) ({ \
extern unsigned char _binary_obj___user_##x##_out_start[], \
_binary_obj___user_##x##_out_size[]; \
__KERNEL_EXECVE(#x, _binary_obj___user_##x##_out_start, \
_binary_obj___user_##x##_out_size); \
})

#define __KERNEL_EXECVE2(x, xstart, xsize) ({ \
extern unsigned char xstart[], xsize[]; \
__KERNEL_EXECVE(#x, xstart, (size_t)xsize); \
})

#define KERNEL_EXECVE2(x, xstart, xsize) __KERNEL_EXECVE2(x, xstart, xsize)

// user_main - kernel thread used to exec a user program
static int
user_main(void *arg) {
#ifdef TEST
KERNEL_EXECVE2(TEST, TESTSTART, TESTSIZE);
#else
KERNEL_EXECVE(exit);
#endif
panic("user_main execve failed.\n");
}

proc_init函数中,通过kernel_thread来创建第二个内核线程init_maininit_main又调用kernel_thread来创建子进程user_main 。user_main在缺省的情况下执行宏KERNEL_EXECVE(exit); ,而这个宏最后是调用kernel_execve 来调用SYS_exec系统调用。由于ld在链接exit应用程序执行码时定义了两全局变量:

1
2
_binary_obj___user_exit_out_start//exit执行码的起始位置
_binary_obj___user_exit_out_size//exit执行码的大小

kernel_execve把这两个变量作为SYS_exec系统调用的参数,让ucore来创建此用户进程。当ucore收到此系统调用后,将依次调用如下函数c

1
2
vector128(vectors.S)-->__alltraps(trapentry.S)-->trap(trap.c)-->trap_dispatch(trap.c)--
-->syscall(syscall.c)-->sys_exec(syscall.c)-->do_execve(proc.c)

do_exceve函数调用了load_icode去加载ELF二进制格式文件到内存并执行

do_execve函数主要做的工作就是先回收自身所占用户空间,然后调用load_icode,用新的程序覆盖内存空间,形成一个执行新程序的新进程

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
// do_execve - call exit_mmap(mm)&pug_pgdir(mm) to reclaim memory space of current process
// - call load_icode to setup new memory space accroding binary prog.
int
do_execve(const char *name, size_t len, unsigned char *binary, size_t size) {
struct mm_struct *mm = current->mm;
if (!user_mem_check(mm, (uintptr_t)name, len, 0)) {
return -E_INVAL;
}
if (len > PROC_NAME_LEN) {
len = PROC_NAME_LEN;
}

char local_name[PROC_NAME_LEN + 1];
memset(local_name, 0, sizeof(local_name));
memcpy(local_name, name, len);
//首先清空内存空间
if (mm != NULL) {
lcr3(boot_cr3);//切换为内核态
if (mm_count_dec(mm) == 0) {
exit_mmap(mm);//清空内存管理部分和对应页表
put_pgdir(mm);//清空页表
mm_destroy(mm);//清空内存
}
current->mm = NULL;//将当前页表指向NULL
}
int ret;
//然后向清空的内存中填充新的内容,调用load_icode函数
if ((ret = load_icode(binary, size)) != 0) {
goto execve_exit;
}
set_proc_name(current, local_name);
return 0;

execve_exit:
do_exit(ret);
panic("already exit: %e.\n", ret);
}

image-20200425202823845

load_icode函数的主要工作就是给用户进程建立一个能够让用户进程正常运行的用户环境。其完成的工作如下:

  1. 调用mm_create函数来申请进程的内存管理数据结构mm所需内存空间,并对mm进行初始化;

  2. 调用setup_pgdir来申请一个页目录表所需的一个页大小的内存空间,并把描述ucore内核虚空间映射的内核页表(boot_pgdir所指)的内容拷贝到此新目录表中,最后mm->pgdir指向此页目录表,这就是进程新的页目录表了,且能够正确映射内核。

  3. 根据应用程序执行码的起始位置来解析此ELF格式的执行程序,并调用mm_map函数根据ELF格式的执行程序说明的各个段(代码段、数据段、BSS段等)的起始位置和大小建立对应的vma结构,并把vma插入到mm结构中,从而表明了用户进程的合法用户态虚拟地址空间。

  4. 调用根据执行程序各个段的大小分配物理内存空间,并根据执行程序各个段的起始位置确定虚拟地址,并在页表中建立好物理地址和虚拟地址的映射关系,然后把执行程序各个段的内容拷贝到相应的内核虚拟地址中,至此应用程序执行码和数据已经根据编译时设定地址放置到虚拟内存中了;

  5. 需要给用户进程设置用户栈,为此调用mm_mmap函数建立用户栈的vma结构,明确用户栈的位置在用户虚空间的顶端,大小为256个页,即1MB,并分配一定数量的物理内存且建立好栈的虚地址<–>物理地址映射关系;

  6. 至此,进程内的内存管理vma和mm数据结构已经建立完成,于是把mm->pgdir赋值到cr3寄存器中,即更新了用户进程的虚拟内存空间,此时的initproc已经被hello的代码和数据覆盖,成为了第一个用户进程,但此时这个用户进程的执行现场还没建立好;

  7. 先清空进程的中断帧,再重新设置进程的中断帧,使得在执行中断返回指令“iret”后,能够让CPU转到用户态特权级,并回到用户态内存空间,使用用户态的代码段、数据段和堆栈,且能够跳转到用户进程的第一条指令执行,并确保在用户态能够响应中断;

以下是load_icode函数的内容

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
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
/* load_icode - load the content of binary program(ELF format) as the new content of current process
* @binary: the memory addr of the content of binary program
* @size: the size of the content of binary program
*/
static int
load_icode(unsigned char *binary, size_t size) {
if (current->mm != NULL) {
panic("load_icode: current->mm must be empty.\n");
}
//准备部分:当前进程必须为空,这样才能加载到内存。
//(在调用它的do_exceve函数中,如果没有bug,那么已经清空了)
int ret = -E_NO_MEM;
struct mm_struct *mm; //声明了一个页表
//(1) create a new mm for current process
//第1步:为当前的进程创建一块内存
if ((mm = mm_create()) == NULL) {//申请内存
goto bad_mm;
}
//这里调用了一个函数mm_create(kern/mm/vmm.c,43——60行):
// mm_create - alloc a mm_struct & initialize it.
//struct mm_struct *
//mm_create(void) {
// struct mm_struct *mm = kmalloc(sizeof(struct mm_struct));
//
// if (mm != NULL) {
// list_init(&(mm->mmap_list));
// mm->mmap_cache = NULL;
// mm->pgdir = NULL;
// mm->map_count = 0;
//
// if (swap_init_ok) swap_init_mm(mm);
// else mm->sm_priv = NULL;
//
// set_mm_count(mm, 0);
// lock_init(&(mm->mm_lock));
// }
// return mm;
//}
//可以看到这个函数首先申请了一块内存空间 如果内存空间申请成功了 那么
//就会把这个内存空间返回给外面调用它的mm变量 如果申请失败 那么新开
//辟的空间都不存在 即为NULL 且会返回它自己 因此外部的判断条件是mm
//不能等于NULL 如果等于NULL 说明创建空间失败了 否则 就能够说明创建成功

//(2) create a new PDT, and mm->pgdir= kernel virtual addr of PDT
//第2步:调用 setup_pgdir来申请一个页目录表所需的一个页大小的内存空间
if (setup_pgdir(mm) != 0) {
goto bad_pgdir_cleanup_mm;
}
//这里调用了一个函数setup_pgdir(kern/process/proc.c,288——299行)
// setup_pgdir - alloc one page as PDT
//static int
//setup_pgdir(struct mm_struct *mm) {
// struct Page *page;
// if ((page = alloc_page()) == NULL) {
// return -E_NO_MEM;
// }
// pde_t *pgdir = page2kva(page);
// memcpy(pgdir, boot_pgdir, PGSIZE);
// pgdir[PDX(VPT)] = PADDR(pgdir) | PTE_P | PTE_W;
// mm->pgdir = pgdir;
// return 0;
//}
//如果没有返回0 那么分配页目录表失败 因此程序需要判断为0的情况
//到一个错误的状态

//(3) copy TEXT/DATA section, build BSS parts in binary to memory space of process
//第3步:读取ELF格式的文件,在内存中复制该进程所需要的代码段等信息
struct Page *page;//申请一个页
//(3.1) get the file header of the bianry program (ELF format)
struct elfhdr *elf = (struct elfhdr *)binary;//获取ELF格式文件的表头
//在bootloader启动的过程中 已经将ucore内核和用户代码全部加载到内存
//因为没有文件管理系统 我们只需要关注这个代码在内存中的哪里 找到了
//开头就能根据它找到数据段
//(3.2) get the entry of the program section headers of the bianry program (ELF format)
struct proghdr *ph = (struct proghdr *)(binary + elf->e_phoff);
//(3.3) This program is valid?
if (elf->e_magic != ELF_MAGIC) {//这个ELF文件的格式是否是合法
ret = -E_INVAL_ELF;//返回一个ELF文件非法操作
goto bad_elf_cleanup_pgdir;
}

uint32_t vm_flags, perm;
struct proghdr *ph_end = ph + elf->e_phnum;
for (; ph < ph_end; ph ++) {
//(3.4) find every program section headers
if (ph->p_type != ELF_PT_LOAD) {
continue ;
}
if (ph->p_filesz > ph->p_memsz) {
ret = -E_INVAL_ELF;
goto bad_cleanup_mmap;
}
if (ph->p_filesz == 0) {
continue ;
}
//这个地方获取的是文件的各个段,包括代码段、数据段等。
//(3.5) call mm_map fun to setup the new vma ( ph->p_va, ph->p_memsz)
//根据获取的各个段的开头,以及虚拟地址创建VMA
//(管理进程所认为的合法空间)一开始给各个段赋予了一些属性:
vm_flags = 0, perm = PTE_U;
if (ph->p_flags & ELF_PF_X) vm_flags |= VM_EXEC;//可执行属性(代码段)
if (ph->p_flags & ELF_PF_W) vm_flags |= VM_WRITE;//可读可写(数据段)
if (ph->p_flags & ELF_PF_R) vm_flags |= VM_READ;
if (vm_flags & VM_WRITE) perm |= PTE_W;
if ((ret = mm_map(mm, ph->p_va, ph->p_memsz, vm_flags, NULL)) != 0) {
goto bad_cleanup_mmap;
}
//使用mm_map函数建立合法空间(kern/mm/vmm.c,159——165行)

unsigned char *from = binary + ph->p_offset;
size_t off, size;
uintptr_t start = ph->p_va, end, la = ROUNDDOWN(start, PGSIZE);

ret = -E_NO_MEM;

//(3.6) alloc memory, and copy the contents of every program section (from, from+end) to process's memory (la, la+end)
end = ph->p_va + ph->p_filesz;
//(3.6.1) copy TEXT/DATA section of bianry program
//这里是拷贝内容,memcpy是拷贝函数
while (start < end) {
if ((page = pgdir_alloc_page(mm->pgdir, la, perm)) == NULL) {
goto bad_cleanup_mmap;
}
off = start - la, size = PGSIZE - off, la += PGSIZE;
if (end < la) {
size -= la - end;
}
memcpy(page2kva(page) + off, from, size);//拷贝函数
start += size, from += size;
}

//(3.6.2) build BSS section of binary program
//执行程序的BSS段需要清空,这里全部设置为0
end = ph->p_va + ph->p_memsz;
if (start < la) {
/* ph->p_memsz == ph->p_filesz */
if (start == end) {
continue ;
}
off = start + PGSIZE - la, size = PGSIZE - off;
if (end < la) {
size -= la - end;
}
memset(page2kva(page) + off, 0, size);//设置为0
start += size;
assert((end < la && start == end) || (end >= la && start == la));
}
while (start < end) {
if ((page = pgdir_alloc_page(mm->pgdir, la, perm)) == NULL) {
goto bad_cleanup_mmap;
}
off = start - la, size = PGSIZE - off, la += PGSIZE;
if (end < la) {
size -= la - end;
}
memset(page2kva(page) + off, 0, size);
start += size;
}
}
//(4) build user stack memory
//除了数据段、代码段、进程还需要用户堆栈空间。这里是构造用户堆栈。
vm_flags = VM_READ | VM_WRITE | VM_STACK;
if ((ret = mm_map(mm, USTACKTOP - USTACKSIZE, USTACKSIZE, vm_flags, NULL)) != 0) {
goto bad_cleanup_mmap;
}
//重新建立mm_map堆栈
assert(pgdir_alloc_page(mm->pgdir, USTACKTOP-PGSIZE , PTE_USER) != NULL);
assert(pgdir_alloc_page(mm->pgdir, USTACKTOP-2*PGSIZE , PTE_USER) != NULL);
assert(pgdir_alloc_page(mm->pgdir, USTACKTOP-3*PGSIZE , PTE_USER) != NULL);
assert(pgdir_alloc_page(mm->pgdir, USTACKTOP-4*PGSIZE , PTE_USER) != NULL);

//(5) set current process's mm, sr3, and set CR3 reg = physical addr of Page Directory
//建立好映射关系
mm_count_inc(mm);
current->mm = mm;
current->cr3 = PADDR(mm->pgdir);
lcr3(PADDR(mm->pgdir));

//(6) setup trapframe for user environment
struct trapframe *tf = current->tf;
memset(tf, 0, sizeof(struct trapframe));
/* LAB5:EXERCISE1 YOUR CODE
* should set tf_cs,tf_ds,tf_es,tf_ss,tf_esp,tf_eip,tf_eflags
* NOTICE: If we set trapframe correctly, then the user level process can return to USER MODE from kernel. So
* tf_cs should be USER_CS segment (see memlayout.h)
* tf_ds=tf_es=tf_ss should be USER_DS segment
* tf_esp should be the top addr of user stack (USTACKTOP)
* tf_eip should be the entry point of this binary program (elf->e_entry)
* tf_eflags should be set to enable computer to produce Interrupt
*/
//完成一个优先级的转变,从内核态切换到用户态(特权级从0到3)实现部分
tf->tf_cs = USER_CS;
tf->tf_ds = tf->tf_es = tf->tf_ss = USER_DS;
tf->tf_esp = USTACKTOP;
tf->tf_eip = elf->e_entry;
tf->tf_eflags = FL_IF;
//*tf是一个是中断帧的指针,总是指向内核栈的某个位置:当进程从用户空间跳到内核空间时,中断帧记录了进程在被中断前的状态。当内核需要跳回用户空间时,需要调整中断帧以恢复让进程继续执行的各寄存器值。
//其定义在(kern/trap/trap.h,60——82行)。
//1、将tf_cs设置为用户态,这个定义在(kern/mm/memlayout.h,第21行),有一个宏定义已经定义了用户态和内核态。
//2、tf_ds=tf_es=tf_ss也需要设置为用户态:定义在(kern/mm/memlayout.h,第26行)
//3、需要将esp设置为用户栈的栈顶,直接使用之前建立用户栈时的参数USTACKTOP就可以。
//4、eip是程序的入口,elf类的e_entry函数直接声明了,直接使用。
//5、FL_IF打开中断。
ret = 0;
out:
return ret;
bad_cleanup_mmap:
exit_mmap(mm);
bad_elf_cleanup_pgdir:
put_pgdir(mm);
bad_pgdir_cleanup_mm:
mm_destroy(mm);
bad_mm:
goto out;
}

总的调用流程如下

image-20200425202949732

练习2: 父进程复制自己的内存空间给子进程(需要编码)

创建子进程的函数do_fork在执行中将拷贝当前进程(即父进程)的用户内存地址空间中的合法内容到新进程中(子进程),完成内存资源的复制。具体是通过copy_range函数(位于kern/mm/pmm.c中)实现的,请补充copy_range的实现,确保能够正确执行。

请在实验报告中简要说明如何设计实现”Copy on Write 机制“,给出概要设计,鼓励给出详细设计。

Copy-on-write(简称COW)的基本概念是指如果有多个使用者对一个资源A(比如内存块)进行读操作,则每个使用者只需获得一个指向同一个资源A的指针,就可以该资源了。若某使用者需要对这个资源A进行写操作,系统会对该资源进行拷贝操作,从而使得该“写操作”使用者获得一个该资源A的“私有”拷贝—资源B,可对资源B进行写操作。该“写操作”使用者对资源B的改变对于其他的使用者而言是不可见的,因为其他使用者看到的还是资源A。

实验过程

查看copy_range函数

根据注释将内容补全

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
/* copy_range - copy content of memory (start, end) of one process A to another process B
* @to: the addr of process B's Page Directory
* @from: the addr of process A's Page Directory
* @share: flags to indicate to dup OR share. We just use dup method, so it didn't be used.
*
* CALL GRAPH: copy_mm-->dup_mmap-->copy_range
*/
int
copy_range(pde_t *to, pde_t *from, uintptr_t start, uintptr_t end, bool share) {
assert(start % PGSIZE == 0 && end % PGSIZE == 0);
assert(USER_ACCESS(start, end));
// copy content by page unit.
do {
//call get_pte to find process A's pte according to the addr start
pte_t *ptep = get_pte(from, start, 0), *nptep;
if (ptep == NULL) {
start = ROUNDDOWN(start + PTSIZE, PTSIZE);
continue ;
}
//call get_pte to find process B's pte according to the addr start. If pte is NULL, just alloc a PT
if (*ptep & PTE_P) {
if ((nptep = get_pte(to, start, 1)) == NULL) {
return -E_NO_MEM;
}
uint32_t perm = (*ptep & PTE_USER);
//get page from ptep
struct Page *page = pte2page(*ptep);
// alloc a page for process B
struct Page *npage=alloc_page();
assert(page!=NULL);
assert(npage!=NULL);
int ret=0;
/* LAB5:EXERCISE2 YOUR CODE
* replicate content of page to npage, build the map of phy addr of nage with the linear addr start
*
* Some Useful MACROs and DEFINEs, you can use them in below implementation.
* MACROs or Functions:
* page2kva(struct Page *page): return the kernel vritual addr of memory which page managed (SEE pmm.h)
* page_insert: build the map of phy addr of an Page with the linear addr la
* memcpy: typical memory copy function
*
* (1) find src_kvaddr: the kernel virtual address of page
* (2) find dst_kvaddr: the kernel virtual address of npage
* (3) memory copy from src_kvaddr to dst_kvaddr, size is PGSIZE
* (4) build the map of phy addr of nage with the linear addr start
*/
+ void * kva_src = page2kva(page); //获取老页表的值
+ void * kva_dst = page2kva(npage); //获取新页表的值
+ memcpy(kva_dst, kva_src, PGSIZE); //复制操作
+ ret = page_insert(to, npage, start, perm);
//建立子进程页地址起始位置与物理地址的映射关系,prem是权限
assert(ret == 0);
}
start += PGSIZE;
} while (start != 0 && start < end);
return 0;
}

回答问题

实现Copy on Write 机制

Copy-on-write(简称COW)的基本概念是指如果有多个使用者对一个资源A(比如内存块)进行读操作,则每个使用者只需获得一个指向同一个资源A的指针,就可以该资源了。若某使用者需要对这个资源A进行写操作,系统会对该资源进行拷贝操作,从而使得该“写操作”使用者获得一个该资源A的“私有”拷贝—资源B,可对资源B进行写操作。该“写操作”使用者对资源B的改变对于其他的使用者而言是不可见的,因为其他使用者看到的还是资源A。

设计思路:

当使用fork建立线程时,不立即复制父进程的空间,而是把页设成只读模式,并增加一个计数器,该计数器记录有多少个页引用它,并将计数器+1。如果读页时可以共享,当第一次对某个页进行写操作,会因为该页的只读模式而发生缺页异常,此时再复制原来的页,对原来的页计数器-1。释放页的引用后,对该页的计数器-1,当计数器为0时,该页才可真正被释放。

练习3: 阅读分析源代码,理解进程执行 fork/exec/wait/exit 的实现,以及系统调用的实现

请在实验报告中简要说明你对 fork/exec/wait/exit函数的分析。并回答如下问题:

  • 请分析fork/exec/wait/exit在实现中是如何影响进程的执行状态的?
  • 请给出ucore中一个用户态进程的执行状态生命周期图(包执行状态,执行状态之间的变换关系,以及产生变换的事件或函数调用)。(字符方式画即可)

执行:make grade。如果所显示的应用程序检测都输出ok,则基本正确。(使用的是qemu-1.0.1)

实验过程

执行make grade

image-20200501110421557

满分

问题回答

请分析fork/exec/wait/exit在实现中是如何影响进程的执行状态的?

进程状态宏定义

1
2
3
4
5
process state       :     meaning               -- reason
PROC_UNINIT : uninitialized -- alloc_proc
PROC_SLEEPING : sleeping -- try_free_pages, do_wait, do_sleep
PROC_RUNNABLE : runnable(maybe running) -- proc_init, wakeup_proc,
PROC_ZOMBIE : almost dead -- do_exit
  • fork不会影响当前进程的执行状态,但是会将子进程的状态标记为就绪态(RUNNALB),使得可以在后续的调度中运行起来

    查看do_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
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    /* do_fork -     parent process for a new child process
    * @clone_flags: used to guide how to clone the child process
    * @stack: the parent's user stack pointer. if stack==0, It means to fork a kernel thread.
    * @tf: the trapframe info, which will be copied to child process's proc->tf
    */
    int
    do_fork(uint32_t clone_flags, uintptr_t stack, struct trapframe *tf) {
    int ret = -E_NO_FREE_PROC;
    struct proc_struct *proc;
    if (nr_process >= MAX_PROCESS) {
    goto fork_out;
    }
    ret = -E_NO_MEM;
    //LAB4:EXERCISE2 YOUR CODE
    /*
    * Some Useful MACROs, Functions and DEFINEs, you can use them in below implementation.
    * MACROs or Functions:
    * alloc_proc: create a proc struct and init fields (lab4:exercise1)
    * setup_kstack: alloc pages with size KSTACKPAGE as process kernel stack
    * copy_mm: process "proc" duplicate OR share process "current"'s mm according clone_flags
    * if clone_flags & CLONE_VM, then "share" ; else "duplicate"
    * copy_thread: setup the trapframe on the process's kernel stack top and
    * setup the kernel entry point and stack of process
    * hash_proc: add proc into proc hash_list
    * get_pid: alloc a unique pid for process
    * wakup_proc: set proc->state = PROC_RUNNABLE
    * VARIABLES:
    * proc_list: the process set's list
    * nr_process: the number of process set
    */

    // 1. call alloc_proc to allocate a proc_struct
    // 2. call setup_kstack to allocate a kernel stack for child process
    // 3. call copy_mm to dup OR share mm according clone_flag
    // 4. call copy_thread to setup tf & context in proc_struct
    // 5. insert proc_struct into hash_list && proc_list
    // 6. call wakup_proc to make the new child process RUNNABLE
    // 7. set ret vaule using child proc's pid

    //LAB5 YOUR CODE : (update LAB4 steps)
    /* Some Functions
    * set_links: set the relation links of process. ALSO SEE: remove_links: lean the relation links of process
    * -------------------
    * update step 1: set child proc's parent to current process, make sure current process's wait_state is 0
    * update step 5: insert proc_struct into hash_list && proc_list, set the relation links of process
    */
    // 1. call alloc_proc to allocate a proc_struct
    proc = alloc_proc();
    if (!proc) {
    goto fork_out;
    }
    proc->parent = current;
    assert(current->wait_state == 0);
    // 2. call setup_kstack to allocate a kernel stack for child process
    if (setup_kstack(proc) != 0) {
    goto bad_fork_cleanup_proc;
    }
    // 3. call copy_mm to dup OR share mm according clone_flag
    if (copy_mm(clone_flags, proc) != 0) {
    goto bad_fork_cleanup_kstack;
    }
    // 4. call copy_thread to setup tf & context in proc_struct
    copy_thread(proc, stack, tf);
    // 5. insert proc_struct into hash_list && proc_list
    bool intr_flag;
    local_intr_save(intr_flag);
    proc->pid = get_pid();
    //list_add(&proc_list, &proc->list_link);
    hash_proc(proc);
    set_links(proc);
    //++nr_process;
    local_intr_restore(intr_flag);
    // 6. call wakeup_proc to make the new child process RUNNABLE
    wakeup_proc(proc);
    // 7. set ret vaule using child proc's pid
    ret = proc->pid;

    fork_out:
    return ret;

    bad_fork_cleanup_kstack:
    put_kstack(proc);
    bad_fork_cleanup_proc:
    kfree(proc);
    goto fork_out;
    }

    具体实现步骤为

    1、分配并初始化进程控制块(alloc_proc 函数);

    2、为子进程分配并初始化内核栈(setup_stack 函数);

    3、根据 clone_flag标志复制或共享进程内存管理结构(copy_mm 函数);

    4、设置进程在内核(将来也包括用户态)正常运行和调度所需的中断帧和执行上下文(copy_thread 函数);

    5、把设置好的进程控制块放入hash_list 和 proc_list 两个全局进程链表中;

    6、自此,进程已经准备好执行了,把进程状态设置为**“就绪”(RUNNABLE)态**;

    7、设置返回码为子进程的 id 号。

  • exec不会影响当前进程的执行状态,但是会修改当前进程中执行的程序(将内容清空再替换)

    详细代码介绍及运行过程参考练习1

  • wait系统调用取决于是否存在可以释放资源(ZOMBIE)的子进程,如果有的话不会发生状态的改变,如果没有的话会将当前进程置为SLEEPING态,等待执行了exit的子进程将其唤醒;

    查看do_wait函数

    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
    // do_wait - wait one OR any children with PROC_ZOMBIE state, and free memory space of kernel stack
    // - proc struct of this child.
    // NOTE: only after do_wait function, all resources of the child proces are free.
    int
    do_wait(int pid, int *code_store) {
    struct mm_struct *mm = current->mm;
    if (code_store != NULL) {
    if (!user_mem_check(mm, (uintptr_t)code_store, sizeof(int), 1)) {
    return -E_INVAL;
    }
    }

    struct proc_struct *proc;
    bool intr_flag, haskid;
    repeat:
    haskid = 0;
    if (pid != 0) {
    proc = find_proc(pid);
    if (proc != NULL && proc->parent == current) {
    haskid = 1;
    if (proc->state == PROC_ZOMBIE) {//系统有可释放的资源,跳出循环
    goto found;
    }
    }
    }
    else {
    proc = current->cptr;
    for (; proc != NULL; proc = proc->optr) {
    haskid = 1;
    if (proc->state == PROC_ZOMBIE) {//系统有可释放的资源,跳出循环
    goto found;
    }
    }
    }
    if (haskid) {
    current->state = PROC_SLEEPING;
    current->wait_state = WT_CHILD;
    schedule();
    if (current->flags & PF_EXITING) {
    do_exit(-E_KILLED);
    }
    goto repeat;
    }
    return -E_BAD_PROC;

    found:
    if (proc == idleproc || proc == initproc) {
    panic("wait idleproc or initproc.\n");
    }
    if (code_store != NULL) {
    *code_store = proc->exit_code;
    }
    local_intr_save(intr_flag);
    {
    unhash_proc(proc);
    remove_links(proc);
    }
    local_intr_restore(intr_flag);
    put_kstack(proc);
    kfree(proc);
    return 0;
    }
  • exit会将当前进程的状态修改为ZOMBIE态,并且会将父进程唤醒(修改为RUNNABLE),然后主动让出CPU使用权

    查看do_exit代码

    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
    // do_exit - called by sys_exit
    // 1. call exit_mmap & put_pgdir & mm_destroy to free the almost all memory space of process
    // 2. set process' state as PROC_ZOMBIE, then call wakeup_proc(parent) to ask parent reclaim itself.
    // 3. call scheduler to switch to other process
    int
    do_exit(int error_code) {
    if (current == idleproc) {
    panic("idleproc exit.\n");
    }
    if (current == initproc) {
    panic("initproc exit.\n");
    }

    struct mm_struct *mm = current->mm;
    if (mm != NULL) {
    lcr3(boot_cr3);
    if (mm_count_dec(mm) == 0) {
    exit_mmap(mm);
    put_pgdir(mm);
    mm_destroy(mm);
    }
    current->mm = NULL;
    }
    current->state = PROC_ZOMBIE;//进程停止
    current->exit_code = error_code;

    bool intr_flag;
    struct proc_struct *proc;
    local_intr_save(intr_flag);
    {
    proc = current->parent;
    if (proc->wait_state == WT_CHILD) {
    wakeup_proc(proc);//唤醒父进程
    }
    while (current->cptr != NULL) {
    proc = current->cptr;
    current->cptr = proc->optr;

    proc->yptr = NULL;
    if ((proc->optr = initproc->cptr) != NULL) {
    initproc->cptr->yptr = proc;
    }
    proc->parent = initproc;
    initproc->cptr = proc;
    if (proc->state == PROC_ZOMBIE) {
    if (initproc->wait_state == WT_CHILD) {
    wakeup_proc(initproc);
    }
    }
    }
    }
    local_intr_restore(intr_flag);

    schedule();
    panic("do_exit will not return!! %d.\n", current->pid);
    }

请给出ucore中一个用户态进程的执行状态生命周期图(包执行状态,执行状态之间的变换关系,以及产生变换的事件或函数调用)。(字符方式画即可)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
process state       :     meaning               -- reason
PROC_UNINIT : uninitialized -- alloc_proc
PROC_SLEEPING : sleeping -- try_free_pages, do_wait, do_sleep
PROC_RUNNABLE : runnable(maybe running) -- proc_init, wakeup_proc,
PROC_ZOMBIE : almost dead -- do_exit

RUNNING----------------+
A | |
| | |
proc_run() exit()
| | |
| V V
alloc_page()--> UNINIT --wakeup_proc()--> RUNNABLE --exit()--> ZOMBIE
A A
| |
子进程exit() exit()
| |
| |
SLEEPING----------------+

实验总结

本次实验是关于用户进程的实现,涉及到用户进程的切换和进程状态之间的转换过程,实验中最麻烦的一部分是练习0的修改代码部分,如果没有修改好代码,就会使得后面的make grade无法满分,并且会使虚拟机CPU飙升,甚至宕机。其次各个函数的执行逻辑较为复杂,感觉不太好分析。。。
总而言之,这次实验使我对进程控制块的章节内容有了更进一步的理解和掌握。