ucore lab5

UCORE实验5

实验目的

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

实验内容

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

练习0:填写已有实验

本实验依赖实验1/2/3/4。请把你做的实验1/2/3/4的代码填入本实验中代码中有“LAB1”/“LAB2”/“LAB3”/“LAB4”的注释相应部分。注意:为了能够正确执行lab5的测试应用程序,可能需对已完成的实验1/2/3/4的代码进行进一步改进。

在alloc_proc函数中,添加对proc_struct::wait_state以及proc_struct::cptr/optr/yptr成员的初始化。
其中进程间各种关系如下:
parent: proc->parent (proc is children)
children: proc->cptr (proc is parent)
older sibling: proc->optr (proc is younger sibling)
younger sibling: proc->yptr (proc is older sibling)

static struct proc_struct *
alloc_proc(void) {
    struct proc_struct *proc = kmalloc(sizeof(struct proc_struct));
    if (proc != NULL) {
    //LAB4:EXERCISE1 202008010404
    /*
     * below fields in proc_struct need to be initialized
     *       enum proc_state state;                      // Process state
     *       int pid;                                    // Process ID
     *       int runs;                                   // the running times of Proces
     *       uintptr_t kstack;                           // Process kernel stack
     *       volatile bool need_resched;                 // bool value: need to be rescheduled to release CPU?
     *       struct proc_struct *parent;                 // the parent process
     *       struct mm_struct *mm;                       // Process's memory management field
     *       struct context context;                     // Switch here to run process
     *       struct trapframe *tf;                       // Trap frame for current interrupt
     *       uintptr_t cr3;                              // CR3 register: the base addr of Page Directroy Table(PDT)
     *       uint32_t flags;                             // Process flag
     *       char name[PROC_NAME_LEN + 1];               // Process name
     */
    //LAB5 202008010404 : (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->state = PROC_UNINIT;
        proc->pid = -1;
        proc->runs = 0;
        proc->kstack = 0;
        proc->need_resched = 0;
        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->optr = proc->yptr = NULL;
    }
    return proc;
}

在do_fork函数中,添加对当前进程等待状态的检查,以及使用set_links函数来设置进程之间的关系。

/* 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 202008010404
    /*
     * 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
     *   wakeup_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 wakeup_proc to make the new child process RUNNABLE
    //    7. set ret vaule using child proc's pid
    if ((proc = alloc_proc()) == NULL) {
        goto fork_out;
    }
   //LAB5 202008010404 : (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->parent = current;
    assert(current->wait_state == 0);//对wait_state的检查

    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;
fork_out:
    return ret;

bad_fork_cleanup_kstack:
    put_kstack(proc);
bad_fork_cleanup_proc:
    kfree(proc);
    goto fork_out;
}

在idt_init函数中,设置中断T_SYSCALL的触发特权级为DPL_USER,让用户态进程可以使用系统调用。

/* idt_init - initialize IDT to each of the entry points in kern/trap/vectors.S */
void
idt_init(void) {
     /* LAB1 202008010404 : STEP 2 */
     /* (1) Where are the entry addrs of each Interrupt Service Routine (ISR)?
      *     All ISR's entry addrs are stored in __vectors. where is uintptr_t __vectors[] ?
      *     __vectors[] is in kern/trap/vector.S which is produced by tools/vector.c
      *     (try "make" command in lab1, then you will find vector.S in kern/trap DIR)
      *     You can use  "extern uintptr_t __vectors[];" to define this extern variable which will be used later.
      * (2) Now you should setup the entries of ISR in Interrupt Description Table (IDT).
      *     Can you see idt[256] in this file? Yes, it's IDT! you can use SETGATE macro to setup each item of IDT
      * (3) After setup the contents of IDT, you will let CPU know where is the IDT by using 'lidt' instruction.
      *     You don't know the meaning of this instruction? just google it! and check the libs/x86.h to know more.
      *     Notice: the argument of lidt is idt_pd. try to find it!
      */
     /* LAB5 202008010404 */ 
     //you should update your lab1 code (just add ONE or TWO lines of code), let user app to use syscall to get the service of ucore
     //so you should setup the syscall interrupt gate in here
    extern uintptr_t __vectors[];
    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);
}

在trap_dispatch中,设置每100次时间中断后,当前正在执行的进程准备被调度。同时,去掉原来的"100ticks"输出

static void
trap_dispatch(struct trapframe *tf) {
    char c;

    int ret=0;

    switch (tf->tf_trapno) {
    case T_PGFLT:  //page fault
        if ((ret = pgfault_handler(tf)) != 0) {
            print_trapframe(tf);
            if (current == NULL) {
                panic("handle pgfault failed. ret=%d\n", ret);
            }
            else {
                if (trap_in_kernel(tf)) {
                    panic("handle pgfault failed in kernel mode. ret=%d\n", ret);
                }
                cprintf("killed by kernel.\n");
                panic("handle user mode pgfault failed. ret=%d\n", ret); 
                do_exit(-E_KILLED);
            }
        }
        break;
    case T_SYSCALL:
        syscall();
        break;
    case IRQ_OFFSET + IRQ_TIMER:
#if 0
    LAB3 : If some page replacement algorithm(such as CLOCK PRA) need tick to change the priority of pages,
    then you can add code here. 
#endif
        /* LAB1 202008010404 : STEP 3 */
        /* handle the timer interrupt */
        /* (1) After a timer interrupt, you should record this event using a global variable (increase it), such as ticks in kern/driver/clock.c
         * (2) Every TICK_NUM cycle, you can print some info using a funciton, such as print_ticks().
         * (3) Too Simple? Yes, I think so!
         */
        /* LAB5 202008010404 */
        /* 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 == 0) {
            assert(current != NULL);
            current->need_resched = 1;
        }
        break;
    case IRQ_OFFSET + IRQ_COM1:
        c = cons_getc();
        cprintf("serial [%03d] %c\n", c, c);
        break;
    case IRQ_OFFSET + IRQ_KBD:
        c = cons_getc();
        cprintf("kbd [%03d] %c\n", c, c);
        break;
    //LAB1 CHALLENGE 1 : YOUR CODE you should modify below codes.
    case T_SWITCH_TOU:
    case T_SWITCH_TOK:
        panic("T_SWITCH_** ??\n");
        break;
    case IRQ_OFFSET + IRQ_IDE1:
    case IRQ_OFFSET + IRQ_IDE2:
        /* do nothing */
        break;
    default:
        print_trapframe(tf);
        if (current != NULL) {
            cprintf("unhandled trap.\n");
            do_exit(-E_KILLED);
        }
        // in kernel, it must be a mistake
        panic("unexpected trap in kernel.\n");

    }
}

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

do_execv函数调用load_icode(位于kern/process/proc.c中)来加载并解析一个处于内存中的ELF执行文件格式的应用程序,建立相应的用户内存空间来放置应用程序的代码段、数据段等,且要设置好proc_struct结构中的成员变量trapframe中的内容,确保在执行此进程后,能够从应用程序设定的起始执行地址开始执行。需设置正确的trapframe内容。

请在实验报告中简要说明你的设计实现过程。

请在实验报告中描述当创建一个用户态进程并加载了应用程序后,CPU是如何让这个应用程序最终在用户态执行起来的。即这个用户态进程被ucore选择占用CPU执行(RUNNING态)到具体执行应用程序第一条指令的整个经过。

分析
完善load_icode函数的代码

在确定了用户进程的执行代码和数据,以及用户进程的虚拟空间布局后,我们可以来创建用户进程了。在本实验中第一个用户进程是由第二个内核线程initproc通过把hello应用程序执行码覆盖到initproc的用户虚拟内存空间来创建的,期间通过一系列调用,最终通过do_execve函数来完成用户进程的创建工作。此函数的主要工作流程如下:

①首先为加载新的执行码做好用户态内存空间清空准备。如果mm不为NULL,则设置页表为内核空间页表,且进一步判断mm的引用计数减1后是否为0,如果为0,则表明没有进程再需要此进程所占用的内存空间,为此将根据mm中的记录,释放进程所占用户空间内存和进程页表本身所占空间。最后把当前进程的mm内存管理指针为空。由于此处的initproc是内核线程,所以mm为NULL,整个处理都不会做。

②接下来的一步是加载应用程序执行码到当前进程的新创建的用户态虚拟空间中。这里涉及到读ELF格式的文件,申请内存空间,建立用户态虚存空间,加载应用程序执行码等。load_icode函数完成了整个复杂的工作。
do_execve函数:

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);

    //如果mm不为NULL进行相应的处理
    if (mm != NULL) {
        lcr3(boot_cr3);
        if (mm_count_dec(mm) == 0) {
            exit_mmap(mm);
            put_pgdir(mm);
            mm_destroy(mm);
        }
        current->mm = NULL;
    }

    //加载应用程序执行码到当前进程的新创建的用户态虚拟空间中
    int ret;
    if ((ret = load_icode(binary, size)) != 0) {
        goto execve_exit;
    }
    set_proc_name(current, local_name);
    return 0;

execve_exit:
    do_exit(ret);
    panic("already exit: %e.\n", ret);
}

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转到用户态特权级,并回到用户态内存空间,使用用户态的代码段、数据段和堆栈,且能够跳转到用户进程的第一条指令执行,并确保在用户态能够响应中断;

其中我们需要需设置正确的trapframe内容,使得在执行中断返回指令“iret”后,能够让CPU转到用户态特权级,即第7步的内容,load_icode函数部分代码如下:

//(6) setup trapframe for user environment
    struct trapframe *tf = current->tf;
    memset(tf, 0, sizeof(struct trapframe));
    /* LAB5:EXERCISE1 202008010404
     * 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;

根据注释,我们要将tf_cs设置为用户态代码段的段选择子,tf_ds、tf_es、tf_ss均设置为用户态数据段的段选择子,tf_esp设置为用户栈的栈顶,tf_eip设置为ELF文件的入口e_entry,tf_eflags使能中断位。

当创建一个用户态进程并加载了应用程序后,CPU是如何让这个应用程序最终在用户态执行起来的

这一问不单要了解本实验,还要和上一个实验给结合起来。

①创建一个新的用户态进程后,内核线程调用schedule函数,schedule函数在proc_list队列中查找下一个处于“就绪”态的用户态进程,然后调用proc_run来运行新进程。

②随后proc_run根据上一个实验的调用流程,找到了kernel_thread_entry。而kernel_thread_entry先将edx保存的输入参数(NULL)压栈,然后通过call指令,跳转到ebx指向的函数,即user_main,user_main先打印userproc的pid和name信息,然后调用kernel_execve。

③kernel_execve调用了exec系统调用,从而转入到了系统调用的处理例程。之后进行正常的中断处理例程,然后控制权转移到了syscall.c中的syscall函数,然后根据系统调用号转移给了sys_exec函数,在该函数中调用了do_execve函数来完成指定应用程序的加载。其调用流程为:vector128 -> __alltraps -> trap -> trap_dispatch -> syscall -> sys_exec -> do_execve。

④在do_execve中进行了若干设置,包括推出当前进程的页表,换用内核的PDT,调用load_icode函数完成对整个用户线程内存空间的初始化,包括堆栈的设置以及将ELF可执行文件的加载,之后通过current->tf指针修改了当前系统调用的trapframe,使得最终中断返回的时候能够切换到用户态,并且同时可以正确地将控制权转移到应用程序的入口处。

⑤在完成了do_exec函数之后,进行正常的中断返回的流程,由于中断处理例程的栈上面的eip已经被修改成了应用程序的入口处,而CS上的CPL是用户态,因此iret进行中断返回的时候会将堆栈切换到用户的栈,并且完成特权级的切换,并且跳转到要求的应用程序的入口处,开始执行应用程序的第一条代码。

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

分析
1.实现copy_range函数

do_fork()完成拷贝任务,其调用流程为:
do_fork()---->copy_mm()---->dup_mmap()---->copy_range()

copy_range:将进程A里的内存内容复制到进程B中。
五个参数:to(进程B的页目录的地址),from(进程A的页目录的地址),share(标志,以表明是用dup还是share,这里我们只使用dup(复制)方法,所以在这里这个参数没用),start和end指的是A内存空间地址的起点和终点。

一些有用的MACROs or Functions:
page2kva(struct Page *page): return the kernel virtual 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.
建立一个线性地址la与Page物理地址的映射。
memcpy: typical memory copy function.
典型的内存拷贝函数。

int
copy_range(pde_t *to, pde_t *from, uintptr_t start, uintptr_t end, bool share) {
    assert(start % PGSIZE == 0 && end % PGSIZE == 0);
    assert(USER_ACCESS(start, end));
    // copy content by page unit.
    do {
        //call get_pte to find process A's pte according to the addr start,调用get_pte函数根据内存空间地址的起点寻找A的PTE
        pte_t *ptep = get_pte(from, start, 0), *nptep;
        if (ptep == NULL) {
            start = ROUNDDOWN(start + PTSIZE, PTSIZE);
            continue ;
        }
        //call get_pte to find process B's pte according to the addr start. If pte is NULL, just alloc a PT,同样寻找B的PTE,如果PTE为NULL则分配一个PT
        if (*ptep & PTE_P) {
            if ((nptep = get_pte(to, start, 1)) == NULL) {
                return -E_NO_MEM;
            }
        uint32_t perm = (*ptep & PTE_USER);
        //get page from ptep,从ptep中获取页
        struct Page *page = pte2page(*ptep);
        // alloc a page for process B,为进程B分配页
        struct Page *npage=alloc_page();
        assert(page!=NULL);
        assert(npage!=NULL);
        int ret=0;

        /* LAB5:EXERCISE2 202008010404*/
        //复制page的内容到npage,建立npage的物理地址与线性地址开始的映射。

        void * kva_src = page2kva(page); //获取源(src)页面所在的虚拟地址(注意,此时的PDT是内核状态下的页目录表)
        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;
    } while (start != 0 && start < end);
    return 0;
}
2.实现"Copy on Write 机制"

当一个用户父进程创建自己的子进程时,父进程会把其申请的用户空间设置为只读,子进程可共享父进程占用的用户内存空间中的页面(这就是一个共享的资源)。当其中任何一个进程修改此用户内存空间中的某页面时,ucore会通过page fault异常获知该操作,并完成拷贝内存页面,使得两个进程都有各自的内存页面。这样一个进程所做的修改不会被另外一个进程可见了。

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

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

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

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

分析
对fork/exec/wait/exit函数以及系统调用的分析
系统调用名含义具体完成服务的函数
SYS_exitprocess exitdo_exit
SYS_forkcreate child process, dup mmdo_fork–>wakeup_proc
SYS_waitwait child processdo_wait
SYS_execafter fork, process execute a new programload a program and refresh the mm
SYS_yieldprocess flag itself need resechedulingproc->need_sched=1, then scheduler will rescheule this process
SYS_killkill processdo_kill–>proc->flags
SYS_getpidget the process’s pid

1.do_fork
①分配并初始化进程控制块(alloc_proc函数)。
②分配并初始化内核栈(setup_stack函数)。
③对当前进程等待状态的检查。
④根据clone_flag标志复制或共享进程内存管理结构(copy_mm函数)。
⑤设置进程在内核(将来也包括用户态)正常运行和调度所需的中断帧和执行上下文(copy_thread函数)。
⑥把设置好的进程控制块放入hash_list和proc_list两个全局进程链表中,并且设置set_links。
⑦自此,进程已经准备好执行了,把进程状态设置为“就绪”态。
⑧设置返回码为子进程的id号。

/* 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 202008010404
    /*
     * 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
     *   wakeup_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 wakeup_proc to make the new child process RUNNABLE
    //    7. set ret vaule using child proc's pid
    if ((proc = alloc_proc()) == NULL) {
        goto fork_out;
    }
   //LAB5 202008010404 : (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->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;
fork_out:
    return ret;

bad_fork_cleanup_kstack:
    put_kstack(proc);
bad_fork_cleanup_proc:
    kfree(proc);
    goto fork_out;
}

其中进程关系可视化为:

                     +----------------+
                     | parent process |
                     +----------------+
              parent ^         \       ^  parent
                    /           \       \
                   /             \ cptr  \
                  /         yptr  V       \      yptr
           +-------------+  -->  +-------------+  -->  NULL
           | old process |       | New Process |
NULL  <--  +-------------+  <--  +-------------+
      optr                  optr

2.do_execve
①调用exit_mmap(mm)和put_pgdir(mm)来回收当前进程内存空间的所有资源(仅保留了PCB)。
②调用load_icode根据可执行文件分配特定位置的虚拟内存空间。

// do_execve - call exit_mmap(mm)&put_pgdir(mm) to reclaim memory space of current process
//           - call load_icode to setup new memory space accroding binary prog.
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) {
            exit_mmap(mm); //删除该内存管理对应的PDT
            put_pgdir(mm);
            mm_destroy(mm);
        }
        current->mm = NULL;
    }
    //加载可执行文件代码,重设mm_struct,以及重置trapframe
    int ret;
    if ((ret = load_icode(binary, size)) != 0) {
        goto execve_exit;
    }
    //设置进程名字
    set_proc_name(current, local_name);
    return 0;

execve_exit:
    do_exit(ret);
    panic("already exit: %e.\n", ret);
}

3.do_wait
①检查当前进程分配的内存区域是否正常。
②查找特定/所有子进程中是否存在某个等待的父进程回收的子进程(PROC_ZOMBIE状态)。如果有则回收该进程返回,如果没有则设置当前进程为PROC_SLEEPING并执行schedule调度其他进程运行。当前进程的某个子进程结束运行后,当前进程会被唤醒,并在do_wait函数中回收子进程的PCB内存资源。

// 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;
    //如果pid不为0,则找到进程id为pid的处于僵死状态的子进程
    if (pid != 0) {
        proc = find_proc(pid);
        if (proc != NULL && proc->parent == current) {
            haskid = 1;
            if (proc->state == PROC_ZOMBIE) {
                goto found;
            }
        }
    }
    //如果pid为0,则任意找一个处于僵死状态的进程
    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); //将子进程从hash_list中删除
        remove_links(proc); //将子进程从proc_list中删除
    }
    local_intr_restore(intr_flag);
    put_kstack(proc); //释放子进程的内核堆栈
    kfree(proc); //释放子进程的PCB
    return 0;
}

4.do_exit
①调用exit_mmap,put_pgdir和mm_destroy来释放进程几乎所有的内存空间。
②将进程状态设置为僵死模式,然后调用wakeup_proc(parent)来让父进程回收这个进程。
③调用scheduler来切换到其他进程。

/ 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); //唤醒父进程,父进程准备回收该进程的PCB资源
        }
        while (current->cptr != NULL) { //如果该进程存在子进程,则将其所有子进程的父进程设为init
            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);
}

5.syscall系统调用
syscall是内核程序为用户程序提供内核服务的一种方式。ucore中实现系统调用涉及到:
(1)初始化系统调用对于的中断描述符
早在lab1其实就涉及到了这一部分。在ucore初始化函数kern_init中调用了idt_init函数来初始化中断描述符表,并设置一个特定中断号的中断门,专门用于用户进程访问系统调用。此事由ide_init函数完成:

void
idt_init(void) {
    extern uintptr_t __vectors[];
    //引用另一个文件中的__vectors
    for (int i = 0; i < 256; i++)
    SETGATE(idt[i], 0, GD_KTEXT, __vectors[i], DPL_KERNEL);
    //在IDT中建立中断描述符,其中存储了中断处理例程的代码段GD_KTEXT和偏移量__vectors[i],特权级为DPL_KERNEL。这样通过查询idt[i]就可定位到中断服务例程的起始地址。
    SETGATE(idt[T_SYSCALL], 0, GD_KTEXT, __vectors[T_SYSCALL], DPL_USER);
    //为系统调用中断设置用户态权限(DPL3)
    lidt(&idt_pd);
    //载入LDT,即将LDT存入LDTR
}

(2)建立系统调用的用户库准备
在操作系统中初始化好系统调用相关的中断描述符、中断处理起始地址等后,还需在用户态的应用程序中初始化好相关工作,简化应用程序访问系统调用的复杂性。为此在用户态建立了一个中间层,即简化的libc实现,在user/libs/ulib.[ch]和user/libs/syscall.[ch]中完成了对访问系统调用的封装。用户态最终的访问系统调用函数是syscall,实现如下:

static inline int
syscall(int num, ...) {
    va_list ap;
    va_start(ap, num);
    uint32_t a[MAX_ARGS];
    int i, ret;
    for (i = 0; i < MAX_ARGS; i ++) {
        a[i] = va_arg(ap, uint32_t);
    }
    va_end(ap);

    asm volatile (
        "int %1;"
        : "=a" (ret)
        : "i" (T_SYSCALL),
          "a" (num),
          "d" (a[0]),
          "c" (a[1]),
          "b" (a[2]),
          "D" (a[3]),
          "S" (a[4])
        : "cc", "memory");
    return ret;
}

该函数会设置%eax, %edx, %ecx, %ebx, %edi, %esi五个寄存器的值分别为syscall调用号、参数1、参数2、参数3、参数4、参数5,然后执行int中断进入中断处理例程。

(3)系统调用的执行过程
在中断处理例程中,程序会根据中断号,执行syscall函数(注意该syscall函数为内核代码,非用户库中的syscall函数)。内核syscall函数会一一取出六个寄存器的值,并根据系统调用号来执行不同的系统调用。而那些系统调用的实质就是其他内核函数的wrapper。以下为syscall函数实现的代码:

void
syscall(void) {
    struct trapframe *tf = current->tf;
    uint32_t arg[5];
    int num = tf->tf_regs.reg_eax;
    if (num >= 0 && num < NUM_SYSCALLS) {
        if (syscalls[num] != NULL) {
            arg[0] = tf->tf_regs.reg_edx;
            arg[1] = tf->tf_regs.reg_ecx;
            arg[2] = tf->tf_regs.reg_ebx;
            arg[3] = tf->tf_regs.reg_edi;
            arg[4] = tf->tf_regs.reg_esi;
            tf->tf_regs.reg_eax = syscalls[num](arg);
            return ;
        }
    }
    print_trapframe(tf);
    panic("undefined syscall %d, pid = %d, name = %s.\n",
            num, current->pid, current->name);
}

等相应的内核函数结束后,程序通过之前保留的trapframe返回用户态,一次系统调用结束。

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

①fork会修改其子进程的状态为PROC_RUNNABLE,而当前进程状态不变。
②exec不修改当前进程的状态,但会替换内存空间里所有的数据与代码。
③wait会先检测是否存在子进程。如果存在进入PROC_ZONBIE的子进程,则回收该进程并函数返回。但若存在尚处于PROC_RUNNABLE的子进程,则当前进程会进入PROC_SLEEPING状态,并等待子进程唤醒。
④exit会将当前进程状态设置为PROC_ZONBIE,并唤醒父进程,使其处于PROC_RUNNABLE的状态,之后主动让出CPU。

请给出ucore中一个用户态进程的执行状态生命周期图(包执行状态,执行状态之间的变换关系,以及产生变换的事件或函数调用)

QQ图片20220525200715

实验心得

到实验四为止,ucore还一直在核心态“打转”,没有到用户态执行。所以,实验五就是要提供用户态进程的创建和执行机制,给应用程序执行提供一个用户态运行环境。而用户态运行环境,就要从存储空间和执行指令这两方面构建。存储空间方面就是要限制用户进程可以访问的物理地址空间,且让各个用户进程之间的物理内存空间访问不重叠。执行指令方面就是要即限制用户进程可执行的指令,不能让用户进程执行特权指令,从而保证用户进程无法破坏系统。加入真要执行特权指令,完成某些功能,则需要向系统提出申请,让系统来帮助完成,这就是系统调用。而用户态的运行状态,其实和内核线程也大差不差。所以本实验就是让我们来体会用户态进程是如何实现的。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值