练习0:填写已有实验
请把你做的实验1/2/3/4的代码填入本实验中代码中有“LAB1”,“LAB2”,“LAB3”,”LAB4”的注释相应部分。注意:为了能够正确执行lab5的测试应用程序,可能需对已完成的实验1/2/3/4的代码进行进一步改进。
1 2
| 这几个文件补上 说真的最烦这个步骤 但是这次 不只是直接复制 还要补充 vmm.c trap.c default_pmm.c pmm.c proc.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
| proc.c: static struct proc_struct * alloc_proc(void) { proc->state = PROC_UNINIT; proc->pid = -1; proc->runs = 0; proc->kstack = 0; proc->need_resched = NULL; proc->parent = NULL; proc->mm = NULL; memset(&(proc->context), 0, sizeof(struct context)); proc->tf = NULL; proc->cr3 = boot_cr3; proc->flags = 0; memset(&(proc->name), 0, PROC_NAME_LEN);
proc->wait_state = 0; proc->cptr = proc->yptr = proc-> optr = NULL; | cptr: proc is parent | | yptr: proc is younger sibling | | optr: proc is older sibling | } int do_fork(uint32_t clone_flags, uintptr_t stack, struct trapframe *tf) { if ((proc = alloc_proc()) == NULL) { goto fork_out; } proc->parent = current;
assert(current->wait_state == 0);
if (setup_kstack(proc) != 0) { goto bad_fork_cleanup_proc; } if (copy_mm(clone_flags, proc) != 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.c: static void trap_dispatch(struct trapframe *tf) { if (++ticks % TICK_NUM == 0) { assert(current != NULL); current->need_resched = 1; } } void idt_init(void) { int i; for (i = 0; i < sizeof(idt) / sizeof(struct gatedesc); i++) { SETGATE(idt[i], 0, GD_KTEXT, __vectors[i], DPL_KERNEL); } SETGATE(idt[T_SYSCALL], 1, GD_KTEXT, __vectors[T_SYSCALL], DPL_USER);
lidt(&idt_pd); }
|
练习1: 加载应用程序并执行
do_execv函数调用load_icode(位于kern/process/proc.c中)来加载并解析一个处于内存中的ELF执行文件格式的应用程序,建立相应的用户内存空间来放置应用程序的代码段、数据段等,且要设置好proc_struct结构中的成员变量trapframe中的内容,确保在执行此进程后,能够从应用程序设定的起始执行地址开始执行。需设置正确的trapframe内容。
请在实验报告中简要说明你的设计实现过程。
1 2 3 4 5 6 7 8 9 10
| 以下为要填写的内容 首先清空进程原先的中断帧 然后再将 中断帧中的 代码段 和 数据段 修改为 用户态的段选择子 栈指针设置为 用户栈顶 eip 设置为 用户程序的入口地址 最后 确保在用户进程中能够响应中断 static int load_icode(unsigned char *binary, size_t size) { 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态)到具体执行应用程序第一条指令的整个经过。
在此之前先理一下 用户态进程是怎么来的
- 创建了一个 硬构造了 第0个 内核线程 idleproc
- idleproc 通过
kernel_thread
创建了 第1个内核线程 initproc 在 lab 4 中只是打印字符串 - initproc 通过
kernel_execve
将 hello应用程序执行码覆盖到了 initproc 用户虚拟内存空间 来创建 用户态进程
1 2 3 4 5 6
| 创建一个用户态进程并加载了应用程序之后 调度器 schedule 调用 proc_run 设置 指针 current 为当前执行PCB 并加载 该进程的 内核栈和页目录表 调用 switch_to 因为 当前进程的 context 其中的 eip 被设置为 forkret(copy_thread 拷贝父进程的中断帧时设置的) 因此 switch_to ret 后 会跳转到 forkret 处 forkret 又会 将栈 设置为 当前进程的trapframe 然后跳到 __trapret 此时 __trapret 会根据当前进程的trapframe 恢复上下文 最后 退出中断 iret 从系统调用的函数调用路径 返回 切换到用户进程 hello 第一句语句 _start 处 开始执行
|
练习2: 父进程复制自己的内存空间给子进程
创建子进程的函数do_fork在执行中将拷贝当前进程(即父进程)的用户内存地址空间中的合法内容到新进程中(子进程),完成内存资源的复制。具体是通过copy_range函数(位于kern/mm/pmm.c中)实现的,请补充copy_range的实现,确保能够正确执行。
1 2 3 4 5 6 7 8
| 这个函数是用来 拷贝父进程的用户内存地址空间到子进程中 share 此处没用到 不管它 int copy_range(pde_t *to, pde_t *from, uintptr_t start, uintptr_t end, bool share) { void* src_kvaddr = page2kva(page); void* dst_kvaddr = page2kva(npage); memcpy(dst_kvaddr, src_kvaddr, PGSIZE); ret = page_insert(to, npage, start, perm); }
|
练习3: 阅读分析源代码,理解进程执行 fork/exec/wait/exit 的实现,以及系统调用的实现
请在实验报告中简要说明你对 fork/exec/wait/exit函数的分析。并回答如下问题:
- 请分析fork/exec/wait/exit在实现中是如何影响进程的执行状态的?
1 2 3 4 5 6
| fork 创建新的 PCB 进程状态为 UNINIT exec 将当前进程的内存布局清除 再调用 load_icode 读出 ELF映像中的内存布局 并填写 进程状态不改变 wait 当前进程若无子进程 则返回错误 若有子进程 则判定 是否为 ZOMBIE 子进程 有则释放子进程的资源 并返回子进程的返回状态码 若无 ZOMBIE 状态子进程 则进入 SLEEPING 状态 等子进程唤醒 exit 清除当前进程几乎所有资源(PCB和内核栈不清除) 将所有子进程(如果有的话)设置为 init 进程(内核) 将当前进程状态设置为 ZOMBIE 若有父进程在等待当前进程exit 则 唤醒父进程
|
- 请给出ucore中一个用户态进程的执行状态生命周期图(包执行状态,执行状态之间的变换关系,以及产生变换的事件或函数调用)
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| RUNNING----------------+ A | | | | | proc_run() exit() | | | | V V --alloc_page()--> UNINIT --wakeup_proc()--> RUNNABLE --exit()--> ZOMBIE A A | | 子进程exit() exit() | | | | SLEEPING----------------+
|
最后附上运行结果 做这个 lab 的时候 给绕的很晕 要特别注意 各个函数 分别修改了 中断帧 和 上下文的 eip esp 的哪些内容
比如 kernel_thread
将 tf->eip
设置为 kernel_thread_entry
当此进程被调度上去的时候
就会跳到 kernel_thread_entry
去执行 fn
这个函数
copy_thread
将 tf->eip
设置为 forkret
而 forkret
会执行这条指令 movl 4(%esp), %esp
将 context->esp
当做 栈顶
而 context->esp
又在 copy_thread 中 被设置为 中断帧的栈顶