清华大学操作系统Lab5实验报告
课程主页:http://os.cs.tsinghua.edu.cn/oscourse/OS2018spring
实验指导书:https://chyyuu.gitbooks.io/ucore_os_docs/content/
github:https://github.com/chyyuu/ucore_os_lab
实验目的
- 了解第一个用户进程创建过程
- 了解系统调用框架的实现机制
- 了解ucore如何实现系统调用sys_fork/sys_exec/sys_exit/sys_wait来进行进程管理
实验内容
实验4完成了内核线程,但到目前为止,所有的运行都在内核态执行。实验5将创建用户进程,让用户进程在用户态执行,且在需要ucore支持时,可通过系统调用来让ucore提供服务。为此需要构造出第一个用户进程,并通过系统调用sys_fork/sys_exec/sys_exit/sys_wait来支持运行不同的应用程序,完成对用户进程的执行过程的基本管理。相关原理介绍可看附录B。
练习0:填写已有实验
除了将原有Lab中的代码转移到Lab5之外,还需要做一些修改。
在alloc_proc
中,初始化proc_struct中新加入的几个成员变量。
//LAB5 2015011346 : (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 = 0x0;
proc -> cptr = proc -> yptr = proc -> optr = NULL;
在do_fork
中,加入与进程控制有关的信息。
//LAB5 2015011346 : (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
*/
proc = alloc_proc();
if (proc == NULL) {
goto fork_out;
}
assert(proc -> wait_state == 0x0);
proc -> parent = current;
int kstack_success = setup_kstack(proc);
if (kstack_success != 0) {
goto bad_fork_cleanup_proc;
}
int copy_success = copy_mm(clone_flags, proc);
if (copy_success != 0) {
goto bad_fork_cleanup_kstack;
}
copy_thread(proc, stack, tf);
bool intr_flag;
local_intr_save(intr_flag);
proc -> pid = get_pid();
hash_proc(proc);
set_links(proc);
local_intr_restore(intr_flag);
wakeup_proc(proc);
ret = proc -> pid;
在trap_dispatch
中,修改timer interrupt,在中断时将当前正在运行的进程设置为可调度的,以便在下一个时间片重新选择进程。
case IRQ_OFFSET + IRQ_TIMER:
/* LAB5 2015011346 */
/* 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) {
ticks = 0;
current -> need_resched = 1;
}
练习1:加载应用程序并执行
load_icode
函数中,在特权级为0的内核栈中创建新的中断帧,通过弹出该中断帧可以赚到特权级为3的用户程序处执行。
/* LAB5:EXERCISE1 2015011346
* 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
*/
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;
请在实验报告中描述当创建一个用户态进程并加载了应用程序后,CPU是如何让这个应用程序最终在用户态执行起来的。即这个用户态进程被ucore选择占用CPU执行( RUNNING态)到具体执行应用程序第一条指令的整个经过。
这部分主要在load_icode
函数中实现。
- 为内存管理的数据结构
mm
分配空间并初始化,代码如下:
//(1) create a new mm for current process
if ((mm = mm_create()) == NULL) {
goto bad_mm;
}
- 通过
setup_pgdir
为用户空间创建页目录,并将内存管理数据结构mm
的pgdir
设置为页目录的虚地址。
//(2) create a new PDT, and mm->pgdir= kernel virtual addr of PDT
if (setup_pgdir(mm) != 0) {
goto bad_pgdir_cleanup_mm;
}
- 接下来将解析已经被载入内存的ELF格式的用户代码。解析ELF header,找到用户程序中program section headers。随后通过调用
mm_map
将不同段的起始地址和长度记录到虚拟内存空间管理的数据结构vma
中去。接下来根据program section的header中的信息,找到每个program section,并将其中的内容拷贝到用户进程的内存中(包括BSS section和TEXT/DATA section)。
//(3) copy TEXT/DATA section, build BSS parts in binary to memory space of process
struct Page *page;
//(3.1) get the file header of the bianry program (ELF format)
struct elfhdr *elf = (struct elfhdr *)binary;
//(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) {
ret = -E_INVAL_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)
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;
}
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
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
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);
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;
}
}
- 接下来通过调用mm_map函数为用户进程的user stack分配空间:
//(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;
}
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);
- 建立用户进程的内存管理数据结构
mm
中的内容,并在进程控制块中记录下用户进程的页目录地址,将用户进程的页目录地址赋给CR3寄存器。
//(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));
- 最后清空原来的中断帧,建立新的中断帧,代码就是填入的那段代码。通过iret指令从内核栈中弹出中断帧恢复各种段寄存器的值。这时段寄存器已经指向特权级为3的段,也就说完成了到用户进程的切换。
练习2:父进程复制自己的内存空间给子进程
在copy_range
函数中完成内存资源的复制。
/* LAB5:EXERCISE2 2015011346
* 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
*/
// (1) find src_kvaddr: the kernel virtual address of page
uint32_t src_kvaddr = page2kva(page);
// (2) find dst_kvaddr: the kernel virtual address of npage
uint32_t dst_kvaddr = page2kva(npage);
// (3) memory copy from src_kvaddr to dst_kvaddr, size is PGSIZE
memcpy(dst_kvaddr, src_kvaddr, PGSIZE);
// (4) build the map of phy addr of nage with the linear addr start
ret = page_insert(to, npage, start, perm);
assert(ret == 0);
请在实验报告中简要说明如何设计实现”Copy on Write 机制“,给出概要设计,鼓励给出详细设计。
“Copy on Write”是指在fork一个进程时不立刻将父进程的数据段/代码段等复制到子进程的内存空间,而是当父进程或子进程中对相关内存做出修改时,才进行复制操作。
实现时,在fork一个进程时,可以省去load_icode
中创建新页目录的操作,而是直接将父进程页目录的地址赋给子进程,为了防止误操作以及辨别是否需要复制,应该将尚未完成复制的部分的访问权限设为只读。
当执行读操作,父进程和子进程均不受影响。但当执行写操作时,会发生权限错误(因为此时的访问权限为只读)。这时候会进入到page fault的处理中去,在page fault的处理中,如果发现错误原因读/写权限问题,而访问的段的段描述符权限为可写,便可以知道是由于使用COW机制而导致的,这时再将父进程的数据段、代码段等复制到子进程内存空间上即可。
练习3:阅读分析源代码,理解进程执行 fork/exec/wait/exit 的实现,以及系统调用的实现。
系统调用共用一个中断号(即代码中的T_SYSCALL
)。当发生冲段或异常后,会进入到中断服务例程中去,最终在trap_dispatch
函数中调用syscall
函数,并通过系统调用号选择应该执行函数sys_fork/exec/wait/exit
中的一个,这些函数会解析系统调用时传入的参数,并将参数传递给do_fork/execv/wait/exit
执行具体操作。
请分析fork/exec/wait/exit在实现中是如何影响进程的执行状态的?
根据上面的系统调用处理过程的分析,我们只需了解do_fork/execve/wait/exit
中的实现。
-
do_fork
:sys_fork的相关函数。在该函数中,首先要为子进程创建进程控制块,设置好进程控制块中的上下文的中断帧等信息,为子进程创建用户栈、内核栈等。随后通过wakeup_proc
函数将子进程设置为RUNNABLE。之后该函数给父进程返回子进程的pid,给子进程返回0。随后在ucore循环执行进程调度schedule
时,就会将子进程考虑进去。详细说明见代码注释。
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;
// 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 wakeup_proc to make the new child process RUNNABLE
// 7. set ret vaule using child proc's pid
proc = alloc_proc();
if (proc == NULL) {
goto fork_out;
}
assert(proc -> wait_state == 0x0);
proc -> parent = current;
int kstack_success = setup_kstack(proc);
if (kstack_success != 0) {
goto bad_fork_cleanup_proc;
}
int copy_success = copy_mm(clone_flags, proc);
if (copy_success != 0) {
goto bad_fork_cleanup_kstack;
}
copy_thread(proc, stack, tf);
bool intr_flag;
local_intr_save(intr_flag);
proc -> pid = get_pid();
hash_proc(proc);
set_links(proc);
local_intr_restore(intr_flag);
wakeup_proc(proc);
ret = proc -> pid;
fork_out:
return ret;
bad_fork_cleanup_kstack:
put_kstack(proc);
bad_fork_cleanup_proc:
kfree(proc);
goto fork_out;
}
-
do_execve
:sys_exec的相关函数。sys_exec不创建新进程,而是用新的内容覆盖原来的进程内存空间。在do_execve
中,首先使用exit_mmap
、put_pgdir
、mm_destroy
来删除并释放掉当前进程内存空间的页表信息、内存管理信息。随后通过load_icode
将新的用户程序从ELF文件中加载进来执行。如果加载失败,则调用do_exit
退出当前进程。执行sys_exec后,当前进程的状态保持不变。详细说明见代码注释。
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) {
// 取消vma中记录的合法内存块
exit_mmap(mm);
// 删除页表
put_pgdir(mm);
// 删除mm记录的信息和占用的空间
mm_destroy(mm);
}
current->mm = NULL;
}
int ret;
// 调用load_icode加载新的进程内容
if ((ret = load_icode(binary, size)) != 0) {
goto execve_exit;
}
set_proc_name(current, local_name);
return 0;
// 如果exec执行不成功,则退出进程
execve_exit:
do_exit(ret);
panic("already exit: %e.\n", ret);
}
-
do_wait
:sys_wait的相关函数。在该函数中,循环查看子进程的状态,直到一个正在等待的子进程的状态变成Zombie状态,这时完成这个子进程的剩余资源回收工作,释放子进程的空间。详细说明见代码注释。
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;
// 循环询问正在等待的子进程的状态,直到有子进程状态变为ZOMBIE。
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;
// 如果发现一个子进程变成了ZOMBIE,则释放该子进程剩余的资源。
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;
}
-
do_exit
:sys_exit的相关函数。退出时,首先释放掉该进程占用的一部分内存(还有一部分可能由父进程释放)。然后将该进程标记为僵尸进程。如果它的父进程处于等待子进程退出的状态,则唤醒父进程,将自己的子进程交给initproc处理,并进行的进程调度。详细说明见代码注释。
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) {
// 释放由vma记录的内存地址块
exit_mmap(mm);
// 删除页表
put_pgdir(mm);
// 删除内存管理结构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);
{
// 如果当前进程的父进程处于等待子进程退出状态,则将父进程设置为RUNNABLE
proc = current->parent;
if (proc->wait_state == WT_CHILD) {
wakeup_proc(proc);
}
// 如果当前进程有子进程,则将子进程设置为initproc的子进程,并完成子进程中处于僵尸状态的进程的最后的回收工作
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中一个用户态进程的执行状态生命周期图(包执行状态,执行状态之间的变换关系,以及产生变换的事件或函数调用) 。
覆盖的知识点
- 进程切换的全过程
- 在父进程执行fork时的行为
- 子进程执行exit后的行为
与参考答案的区别
- 练习1:自己完成。
- 练习2:自己完成。
总结
感觉这次实验比之前的容易一点,官方说法应该是由于认真看了mooc和实验指导书,但实际也可能是因为压着DDL写的比较有动力。
同时很怀疑思考题的表述是否准确,如果不准确希望助教指正。(当然不扣分最好了,我还是认真写了的)。