前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >ucore-lab5

ucore-lab5

作者头像
Heeler-Deer
发布2023-03-10 14:19:53
6460
发布2023-03-10 14:19:53
举报
文章被收录于专栏:HD-学习笔记

练习解答

实验目的

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

实验内容

实验4完成了内核线程,但到目前为止,所有的运行都在内核态执行。实验5将创建用户进程,让用户进程在用户态执行,且在需要ucore支持时,可通过系统调用来让ucore提供服务。为此需要构造出第一个用户进程,并通过系统调用sys_fork/sys_exec/sys_exit/sys_wait来支持运行不同的应用程序,完成对用户进程的执行过程的基本管理。

练习0:填写已有实验

我的建议是照着result来完善代码。

这东西不能直接拿lab4做过的用,实在是败笔。

不嫌麻烦的可以参考kiprey

练习1:加载应用程序并执行(需要编码)

do_execv函数调用load_icode(位于kern/process/proc.c中)来加载并解析一个处于内存中的ELF执行文件格式的应用程序,建立相应的用户内存空间来放置应用程序的代码段、数据段等,且要设置好proc_struct结构中的成员变量trapframe中的内容,确保在执行此进程后,能够从应用程序设定的起始执行地址开始执行。需设置正确的trapframe内容。 请在实验报告中简要说明你的设计实现过程。 请在实验报告中描述当创建一个用户态进程并加载了应用程序后,CPU是如何让这个应用程序最终在用户态执行起来的。即这个用户态进程被ucore选择占用CPU执行(RUNNING态)到具体执行应用程序第一条指令的整个经过。

代码:

代码语言:javascript
复制
/* 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内部的流程大致如上,不难找到:

代码语言:javascript
复制
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

练习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。

代码:

代码语言:javascript
复制
/* 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;

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

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

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

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

注意,前面如果完全正确,参考我上周写的lab4练习二最后一部分更改一下语句就可以拿到136分。

之后参考博客园且自己看提示信息,可以看出这东西确实有bug

我试了试,发现是init_main函数里面,

代码语言:javascript
复制
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的函数调用图:

代码语言:javascript
复制
//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注释这一句即可:

代码语言:javascript
复制
//assert(nr_free_pages_store == nr_free_pages());

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

fork这些调用的都是sys内相应的函数,sys_fork这些又调用的syscall,syscall又调用的诸如do_fork一类。

do_fork之类函数过长,建议自己看

do_fork在练习四写过,这里粘贴一下:

在这个过程中,需要给新内核线程分配资源,并且复制原进程的状态。你需要完成在kern/process/proc.c中的do_fork函数中的处理过程。它的大致执行步骤包括:

  • 调用alloc_proc,首先获得一块用户信息块。
  • 为进程分配一个内核栈。
  • 复制原进程的内存管理信息到新进程(但内核线程不必做此事)
  • 复制原进程上下文到新进程
  • 将新进程添加到进程列表
  • 唤醒新进程
  • 返回新进程号

do_exit以及do_wait直接引用博客园

wait的实现

wait的功能是等待子进程结束,从而释放子进程占用的资源。在ucore中wait对应的函数是do_wait。

  1. 遍历进程链表proc_list,根据输入参数寻找指定pid或任意pid的子进程,如果没找到,直接返回错误信息。
  2. 如果找到子进程,而且其状态为ZOMBIE,则释放子进程占用的资源,然后返回。
  3. 如果找到子进程,但状态不为ZOMBIE,则将当前进程的state设置为SLEEPING、wait_state设置为WT_CHILD,然后调用schedule函数,从而进入等待状态。等再次被唤醒后,重复寻找状态为ZOMBIE的子进程。

exit的实现 exit的功能是释放进程占用的资源并结束运行进程。在ucore中exit对应的函数是do_exit。

  1. 释放页表项记录的物理内存,以及mm结构、vma结构、页目录表占用的内存。
  2. 将自己的state设置为ZOMBIE,然后唤醒父进程,并调用schedule函数,等待父进程回收剩下的资源,最终彻底结束子进程。
  • 请给出ucore中一个用户态进程的执行状态生命周期图(包括执行状态,执行状态之间的变换关系,以及产生变换的事件或函数调用)。

那就直接mermaid了,丑就丑吧 🤐

扩展练习 Challenge :实现 Copy on Write (COW)机制

给出实现源码,测试用例和设计报告(包括在cow情况下的各种状态转换(类似有限状态自动机)的说明)。 这个扩展练习涉及到本实验和上一个实验“虚拟内存管理”。在ucore操作系统中,当一个用户父进程创建自己的子进程时,父进程会把其申请的用户空间设置为只读,子进程可共享父进程占用的用户内存空间中的页面(这就是一个共享的资源)。当其中任何一个进程修改此用户内存空间中的某页面时,ucore会通过page fault异常获知该操作,并完成拷贝内存页面,使得两个进程都有各自的内存页面。这样一个进程所做的修改不会被另外一个进程可见了。请在ucore中实现这样的COW机制。 由于COW实现比较复杂,容易引入bug,请参考 https://dirtycow.ninja/ 看看能否在ucore的COW实现中模拟这个错误和解决方案。需要有解释。 这是一个big challenge.

不难理解是要干什么。

参考github

我们要修改pmm.c中的copy_range以及处理页错误的page_fault函数:

代码语言:javascript
复制
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);

代码语言:javascript
复制
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,所以就鸽了。。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2022年5月13日,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 练习解答
    • 实验目的
      • 实验内容
        • 练习0:填写已有实验
          • 练习1:加载应用程序并执行(需要编码)
            • 练习2: 父进程复制自己的内存空间给子进程(需要编码)
              • 练习3: 阅读分析源代码,理解进程执行 fork/exec/wait/exit 的实现,以及系统调用的实现(不需要编码)
                • 扩展练习 Challenge :实现 Copy on Write (COW)机制
                领券
                问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档