实验目的
实验内容
练习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 | proc.c |
练习1: 加载应用程序并执行
do_execv函数调用load_icode(位于kern/process/proc.c中)来加载并解析一个处于内存中的ELF执行文件格式的应用程序,建立相应的用户内存空间来放置应用程序的代码段、数据段等,且要设置好proc_struct结构中的成员变量trapframe中的内容,确保在执行此进程后,能够从应用程序设定的起始执行地址开始执行。需设置正确的trapframe内容。
请在实验报告中简要说明你的设计实现过程。
请在实验报告中描述当创建一个用户态进程并加载了应用程序后,CPU是如何让这个应用程序最终在用户态执行起来的。即这个用户态进程被ucore选择占用CPU执行(RUNNING态)到具体执行应用程序第一条指令的整个经过。
实验过程
查看proc.c
1 | /* load_icode - load the content of binary program(ELF format) as the new content of current process |
根据注释补充代码得到
1 | //以下为要填写的内容 首先清空进程原先的中断帧 然后再将 中断帧中的 代码段 和 数据段 |
回答问题
请在实验报告中描述当创建一个用户态进程并加载了应用程序后,CPU是如何让这个应用程序最终在用户态执行起来的。即这个用户态进程被ucore选择占用CPU执行(RUNNING态)到具体执行应用程序第一条指令的整个经过。
查看user/hello.c
1 |
|
用户进程的虚拟地址空间
在tools/user.ld
描述了用户程序的用户虚拟空间的执行入口虚拟地址:
1 | SECTIONS { |
在tools/kernel.ld
描述了操作系统的内核虚拟空间的起始入口虚拟地址:
1 | SECTIONS { |
这样ucore将用户进程的虚拟地址空间分为了两个部分,
一部分是所有用户进程共享的内核地址空间,映射到同样的物理地址空间中。将内核代码放到此空间中,用户进程从用户态到内核态时,内核可以统一应对不同的内核程序。
另外一部分是用户虚拟地址空间,映射到不同且没有交集的物理内存空间中。用户进程的执行代码和数据放到用户地址空间时确保各个进程不会非法访问到其他进程的物理空间。
./kern/mm/memlayout.h中的虚存布局
1 | /* * |
创建并执行用户进程
lab5中第一个进程是由第二个内核线程initproc
通过把hello应用程序执行码覆盖到initproc
的用户执行空间来创建的,相关代码如下所示:
1 |
|
在proc_init
函数中,通过kernel_thread
来创建第二个内核线程init_main
,init_main
又调用kernel_thread
来创建子进程user_main
。user_main在缺省的情况下执行宏KERNEL_EXECVE(exit);
,而这个宏最后是调用kernel_execve
来调用SYS_exec系统调用。由于ld在链接exit应用程序执行码时定义了两全局变量:
1 | _binary_obj___user_exit_out_start//exit执行码的起始位置 |
kernel_execve
把这两个变量作为SYS_exec系统调用的参数,让ucore来创建此用户进程。当ucore收到此系统调用后,将依次调用如下函数c
1 | vector128(vectors.S)-->__alltraps(trapentry.S)-->trap(trap.c)-->trap_dispatch(trap.c)-- |
do_exceve函数调用了load_icode去加载ELF二进制格式文件到内存并执行
do_execve函数主要做的工作就是先回收自身所占用户空间,然后调用load_icode,用新的程序覆盖内存空间,形成一个执行新程序的新进程
1 | // do_execve - call exit_mmap(mm)&pug_pgdir(mm) to reclaim memory space of current process |
load_icode函数的主要工作就是给用户进程建立一个能够让用户进程正常运行的用户环境。其完成的工作如下:
-
调用mm_create函数来申请进程的内存管理数据结构mm所需内存空间,并对mm进行初始化;
-
调用setup_pgdir来申请一个页目录表所需的一个页大小的内存空间,并把描述ucore内核虚空间映射的内核页表(boot_pgdir所指)的内容拷贝到此新目录表中,最后mm->pgdir指向此页目录表,这就是进程新的页目录表了,且能够正确映射内核。
-
根据应用程序执行码的起始位置来解析此ELF格式的执行程序,并调用mm_map函数根据ELF格式的执行程序说明的各个段(代码段、数据段、BSS段等)的起始位置和大小建立对应的vma结构,并把vma插入到mm结构中,从而表明了用户进程的合法用户态虚拟地址空间。
-
调用根据执行程序各个段的大小分配物理内存空间,并根据执行程序各个段的起始位置确定虚拟地址,并在页表中建立好物理地址和虚拟地址的映射关系,然后把执行程序各个段的内容拷贝到相应的内核虚拟地址中,至此应用程序执行码和数据已经根据编译时设定地址放置到虚拟内存中了;
-
需要给用户进程设置用户栈,为此调用mm_mmap函数建立用户栈的vma结构,明确用户栈的位置在用户虚空间的顶端,大小为256个页,即1MB,并分配一定数量的物理内存且建立好栈的虚地址<–>物理地址映射关系;
-
至此,进程内的内存管理vma和mm数据结构已经建立完成,于是把mm->pgdir赋值到cr3寄存器中,即更新了用户进程的虚拟内存空间,此时的initproc已经被hello的代码和数据覆盖,成为了第一个用户进程,但此时这个用户进程的执行现场还没建立好;
-
先清空进程的中断帧,再重新设置进程的中断帧,使得在执行中断返回指令“iret”后,能够让CPU转到用户态特权级,并回到用户态内存空间,使用用户态的代码段、数据段和堆栈,且能够跳转到用户进程的第一条指令执行,并确保在用户态能够响应中断;
以下是load_icode函数的内容
1 | /* load_icode - load the content of binary program(ELF format) as the new content of current process |
总的调用流程如下
练习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 | /* copy_range - copy content of memory (start, end) of one process A to another process B |
回答问题
实现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
满分
问题回答
请分析fork/exec/wait/exit在实现中是如何影响进程的执行状态的?
进程状态宏定义
1 | process state : meaning -- reason |
-
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 | process state : meaning -- reason |
实验总结
本次实验是关于用户进程的实现,涉及到用户进程的切换和进程状态之间的转换过程,实验中最麻烦的一部分是练习0的修改代码部分,如果没有修改好代码,就会使得后面的make grade无法满分,并且会使虚拟机CPU飙升,甚至宕机。其次各个函数的执行逻辑较为复杂,感觉不太好分析。。。
总而言之,这次实验使我对进程控制块的章节内容有了更进一步的理解和掌握。