实验4完成了内核线程,但到目前为止,所有的运行都在内核态执行。实验5将创建用户进程,让用户进程在用户态执行,且在需要ucore支持时,可通过系统调用来让ucore提供服务。为此需要构造出第一个用户进程,并通过系统调用sys_fork/sys_exec/sys_exit/sys_wait来支持运行不同的应用程序,完成对用户进程的执行过程的基本管理。
我的建议是照着result来完善代码。
这东西不能直接拿lab4做过的用,实在是败笔。
不嫌麻烦的可以参考kiprey
do_execv函数调用load_icode(位于kern/process/proc.c中)来加载并解析一个处于内存中的ELF执行文件格式的应用程序,建立相应的用户内存空间来放置应用程序的代码段、数据段等,且要设置好proc_struct结构中的成员变量trapframe中的内容,确保在执行此进程后,能够从应用程序设定的起始执行地址开始执行。需设置正确的trapframe内容。 请在实验报告中简要说明你的设计实现过程。 请在实验报告中描述当创建一个用户态进程并加载了应用程序后,CPU是如何让这个应用程序最终在用户态执行起来的。即这个用户态进程被ucore选择占用CPU执行(RUNNING态)到具体执行应用程序第一条指令的整个经过。
代码:
/* 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
*/
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;
ret = 0;
读取文件需要陷入内核态,在load_icode中我们需要完成tf_cs,tf_ds,tf_es,tf_ss,tf_esp,tf_eip,tf_eflags这几个变量的初始化。这注释都已经把答案写出来了。 😆
实验让我们描述用户态进程从被选择执行到具体执行的经过。
建议参考博客园,这里只给出部分关系图以及代码。
proc.c的cluster call internal:
proc.c内部的流程大致如上,不难找到:
related syscall for process:
SYS_exit : process exit, -->do_exit
SYS_fork : create child process, dup mm -->do_fork-->wakeup_proc
SYS_wait : wait process -->do_wait
SYS_exec : after fork, process execute a program -->load a program and refresh the mm
SYS_clone : create child thread -->do_fork-->wakeup_proc
SYS_yield : process flag itself need resecheduling, -- proc->need_sched=1, then scheduler will rescheule this process
SYS_sleep : process sleep -->do_sleep
SYS_kill : kill process -->do_kill-->proc->flags |= PF_EXITING
-->wakeup_proc-->do_wait-->do_exit
SYS_getpid : get the process's pid
创建子进程的函数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。
代码:
/* 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);
//找到src页面所在地址
void * kva_dst = page2kva(npage);
//找到dst页面所在地址
memcpy(kva_dst, kva_src, PGSIZE);
//复制
ret = page_insert(to, npage, start, perm);
//按照链表方式加入pte
assert(ret == 0);
}
start += PGSIZE;
请在实验报告中简要说明你对 fork/exec/wait/exit函数的分析。并回答如下问题:
执行:make grade。如果所显示的应用程序检测都输出ok,则基本正确。(使用的是qemu-1.0.1)
注意,前面如果完全正确,参考我上周写的lab4练习二最后一部分更改一下语句就可以拿到136分。
之后参考博客园且自己看提示信息,可以看出这东西确实有bug
我试了试,发现是init_main函数里面,
static int
init_main(void *arg) {
//注意此句
size_t nr_free_pages_store = nr_free_pages();
size_t kernel_allocated_store = kallocated();
int pid = kernel_thread(user_main, NULL, 0);
if (pid <= 0) {
panic("create user_main failed.\n");
}
//然而这里调用了do_wait,schedule,这两个函数调用了过多内存操作函数
//具体我没有细看,但应该是这里出了问题
while (do_wait(0, NULL) == 0) {
schedule();
}
cprintf("all user-mode processes have quit.\n");
assert(initproc->cptr == NULL && initproc->yptr == NULL && initproc->optr == NULL);
assert(nr_process == 2);
assert(list_next(&proc_list) == &(initproc->list_link));
assert(list_prev(&proc_list) == &(initproc->list_link));
//此时不再相等,故删去assert
//assert(nr_free_pages_store == nr_free_pages());
assert(kernel_allocated_store == kallocated());
cprintf("init check memory pass.\n");
return 0;
}
查看kern/mm/pmm.c中的nr_free_pages的函数调用图:
//nr_free_pages - call pmm->nr_free_pages to get the size (nr*PAGESIZE)
//of current free memory
size_t
nr_free_pages(void) {
size_t ret;
bool intr_flag;
local_intr_save(intr_flag);
{
ret = pmm_manager->nr_free_pages();
}
local_intr_restore(intr_flag);
return ret;
}
在init_main注释这一句即可:
//assert(nr_free_pages_store == nr_free_pages());
fork这些调用的都是sys内相应的函数,sys_fork这些又调用的syscall,syscall又调用的诸如do_fork一类。
do_fork之类函数过长,建议自己看
do_fork在练习四写过,这里粘贴一下:
在这个过程中,需要给新内核线程分配资源,并且复制原进程的状态。你需要完成在kern/process/proc.c中的do_fork函数中的处理过程。它的大致执行步骤包括:
do_exit以及do_wait直接引用博客园
wait的实现
wait的功能是等待子进程结束,从而释放子进程占用的资源。在ucore中wait对应的函数是do_wait。
exit的实现 exit的功能是释放进程占用的资源并结束运行进程。在ucore中exit对应的函数是do_exit。
那就直接mermaid了,丑就丑吧 🤐
给出实现源码,测试用例和设计报告(包括在cow情况下的各种状态转换(类似有限状态自动机)的说明)。 这个扩展练习涉及到本实验和上一个实验“虚拟内存管理”。在ucore操作系统中,当一个用户父进程创建自己的子进程时,父进程会把其申请的用户空间设置为只读,子进程可共享父进程占用的用户内存空间中的页面(这就是一个共享的资源)。当其中任何一个进程修改此用户内存空间中的某页面时,ucore会通过page fault异常获知该操作,并完成拷贝内存页面,使得两个进程都有各自的内存页面。这样一个进程所做的修改不会被另外一个进程可见了。请在ucore中实现这样的COW机制。 由于COW实现比较复杂,容易引入bug,请参考 https://dirtycow.ninja/ 看看能否在ucore的COW实现中模拟这个错误和解决方案。需要有解释。 这是一个big challenge.
不难理解是要干什么。
参考github
我们要修改pmm.c中的copy_range以及处理页错误的page_fault函数:
uint32_t perm = (*ptep & (PTE_U | PTE_P));
struct Page *page = pte2page(*ptep);
assert(page != NULL);
// 令新页面对父进程只读
page_insert(to, page, start, perm);
// 旧页面只读
page_insert(from, page, start, perm);
if (*ptep & PTE_P) {
// Read-only possibly caused by COW.
//确定可以写且可以更改
if (vma->vm_flags & VM_WRITE) {
// If ref of pages == 1, it is not shared, just make pte writable.
// else, alloc a new page, copy content and reset pte.
// also, remember to decrease ref of that page!
//看佬的注释很容易明白
struct Page* p = pte2page(*ptep);
assert(p != NULL);
assert(p->ref > 0);
if (p->ref > 1) {
//复制页面
struct Page *npage = alloc_page();
assert(npage != NULL);
void * src_kvaddr = page2kva(p);
void * dst_kvaddr = page2kva(npage);
memcpy(dst_kvaddr, src_kvaddr, PGSIZE);
// addr already ROUND down.
//插入
page_insert(mm->pgdir, npage, addr, ((*ptep) & PTE_USER) | PTE_W);
// page_ref_dec(p);
//
cprintf("Handled one COW fault at %x: copied\n", addr);
} else {
page_insert(mm->pgdir, p, addr, ((*ptep) & PTE_USER) | PTE_W);
cprintf("Handled one COW fault: reused\n");
}
} else {
cprintf("Not a COW read-only fault.\n");
goto failed;
}
} else {
// 处理缺页
}
结果:
由于一些原因,我并没有按照佬的md做出来Handled one COW fault: reused,所以就鸽了。。