操作系统 ucore lab5实验报告

ucore lab5(用户进程管理)

一、实验目的

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

二、实验内容

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

三、实验步骤及流程

3.0 练习0:填写已有实验

3.0.1实验要求

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

3.0.2代码改进

(1)alloc_proc函数(kern/process/proc.c)
观察到实验四中的PCB部分(kern/process/proc.h)中对于内核线程的结构体声明多出了两行,新加入的两个属性,第一个是进程等待状态,第二个是进程之间的相关指针初始化,在lab5中,涉及到了用户进程,自然需要涉及到调度的问题,所以进程等待状态和各种指针需要被初始化。新增加的两行代码如下(最后两行),主要是初始化进程等待状态、和进程的相关指针,例如父进程、子进程、同胞等等。其中的wait_state是进程控制块中新增的条目。

proc->state = PROC_UNINIT;//给进程设置为未初始化状态
        proc->pid = -1;//未初始化的进程,其pid为-1
        proc->runs = 0;//初始化时间片,刚刚初始化的进程,运行时间一定为零	
        proc->kstack = 0;//内核栈地址,该进程分配的地址为0,因为还没有执行,也没有被重定位,因为默认地址都是从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; //标志位为0
        memset(proc->name, 0, PROC_NAME_LEN);//进程名为0
        proc->wait_state = 0;//PCB新增的条目,初始化进程等待状态
        proc->cptr = proc->optr = proc->yptr = NULL;//指针初始化 

(2)do_fork函数(kern/process/proc.c)
增加两行代码分别为:
①assert(current->wait_state == 0):对应第一步中对于proc->wait_state属性的初始分配,需要确保当前进程正在等待。
②set_links(proc):这里调用了一个set links函数来设置进程之间的连接。查看set links函数代码(kern/process/proc.c),可以看出此函数的作用就是设置当前进程的process relations。同样,它进行一些插入进程、调度、更改“当前进程数量”等对于共享数据的访问操作,因此它还在lab4中定义的互斥锁中。
增加代码具体位置如下:

 proc->parent = current;//将子进程的父节点设置为当前进程
    assert(current->wait_state == 0);//确保当前进程正在等待
    if (setup_kstack(proc) != 0) {//2.调用setup_stack()函数为进程分配一个内核栈
        goto bad_fork_cleanup_proc;
    }
    if (copy_mm(clone_flags, proc) != 0) {//3.调用copy_mm()函数复制父进程的内存信息到子进程
        goto bad_fork_cleanup_kstack;
    }
    copy_thread(proc, stack, tf);//4.调用copy_thread()函数复制父进程的中断帧和上下文信息
    //5.将新进程添加到进程的(hash)列表中
    bool intr_flag;
    local_intr_save(intr_flag);//屏蔽中断,intr_flag置为1
    {
        proc->pid = get_pid();//获取当前进程PID
        hash_proc(proc); //建立hash映射
        set_links(proc);//设置进程链接
    }

(3)idt_init函数(kern/trap/trap.c)
增加一行代码,主要是设置相关中断门:

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);//设置相关中断门

(4)trap_dispatch函数(kern/trap/trap.c)
增加一行代码将时间片设置为需要调度,说明当前进程的时间片已经用完了。

ticks ++;
        if (ticks % TICK_NUM == 0) {
            assert(current != NULL);
            current->need_resched = 1;//将时间片设置为需要调度
        }

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

3.1.1实验要求

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

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

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

3.1.2关键数据结构及知识点

(1)do_exceve函数(kern/process/proc.c):
它调用了load_icode去加载ELF二进制格式文件到内存并执行:

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);//清空内存管理部分和对应页表
            put_pgdir(mm);//清空页表
            mm_destroy(mm);//清空内存
        }
        current->mm = NULL;//最后让它当前的页表指向空,方便放入自己的东西
    }
    int ret;
    if ((ret = load_icode(binary, size)) != 0) {//第二步:向清空的内存中填充新的内容,调用load_icode函数
        goto execve_exit;
    }
    set_proc_name(current, local_name);
    return 0;

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

(2)mm_create函数(kern/mm/vmm.c)
可以看到这个函数首先申请了一块内存空间,如果内存空间申请成功了,那么就会把这个内存空间返回给外面调用它的mm变量,如果申请失败,那么新开辟的空间都不存在,即为NULL,且会返回它自己,因此外部的判断条件是mm不能等于NULL,如果等于NULL,说明创建空间失败了,否则,就能够说明创建成功。

mm_create(void) {
    struct mm_struct *mm = kmalloc(sizeof(struct mm_struct));

    if (mm != NULL) {
        list_init(&(mm->mmap_list));
        mm->mmap_cache = NULL;
        mm->pgdir = NULL;
        mm->map_count = 0;

        if (swap_init_ok) swap_init_mm(mm);
        else mm->sm_priv = NULL;
        
        set_mm_count(mm, 0);
        lock_init(&(mm->mm_lock));
    }    
    return mm;
}

(3)setup_pgdir函数(kern/process/proc.c)
如果没有返回0,那么分配页目录表失败,因此程序需要判断为0的情况,到一个错误的状态。

setup_pgdir(struct mm_struct *mm) {
    struct Page *page;
    if ((page = alloc_page()) == NULL) {
        return -E_NO_MEM;
    }
    pde_t *pgdir = page2kva(page);
    memcpy(pgdir, boot_pgdir, PGSIZE);
    pgdir[PDX(VPT)] = PADDR(pgdir) | PTE_P | PTE_W;
    mm->pgdir = pgdir;
    return 0;
}

(4)mm_map函数(kern/mm/vmm.c)
此函数主要功能为建立合法空间。

mm_map(struct mm_struct *mm, uintptr_t addr, size_t len, uint32_t vm_flags,
       struct vma_struct **vma_store) {
    uintptr_t start = ROUNDDOWN(addr, PGSIZE), end = ROUNDUP(addr + len, PGSIZE);
    if (!USER_ACCESS(start, end)) {
        return -E_INVAL;
    }

    assert(mm != NULL);

    int ret = -E_INVAL;

    struct vma_struct *vma;
    if ((vma = find_vma(mm, start)) != NULL && end > vma->vm_start) {
        goto out;
    }
    ret = -E_NO_MEM;

    if ((vma = vma_create(start, end, vm_flags)) == NULL) {
        goto out;
    }
    insert_vma_struct(mm, vma);
    if (vma_store != NULL) {
        *vma_store = vma;
    }
    ret = 0;

out:
    return ret;
}
3.1.3代码实现(load_icode函数(kern/process/proc.c))

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

struct trapframe *tf = current->tf;
    memset(tf, 0, sizeof(struct trapframe));
    
    tf->tf_cs = USER_CS;//将tf_cs设置为用户态
    tf->tf_ds = tf->tf_es = tf->tf_ss = USER_DS;//tf_ds=tf_es=tf_ss也需要设置为用户态
    tf->tf_esp = USTACKTOP;//需要将esp设置为用户栈的栈顶,直接使用之前建立用户栈时的参数USTACKTOP就可以。
    tf->tf_eip = elf->e_entry;//eip是程序的入口,elf类的e_entry函数直接声明了,直接使用。
    tf->tf_eflags = FL_IF;//FL_IF打开中断
    ret = 0;

3.2 练习2:父进程复制自己的内存空间给子进程(需要编码)

3.2.1实验要求

创建子进程的函数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。

3.2.2关键数据结构及知识点

copy_range函数的调用过程:do_fork()---->copy_mm()---->dup_mmap()---->copy_range()
(1)do_fork函数(kern/process/proc.c)
do_fork函数调用的copy_mm函数在实验四中没有实现,其他的过程和lab4一样,都是创建一个进程,并放入CPU中调度,而本次我们主要关注的是父子进程之间如何拷贝内存。

if (copy_mm(clone_flags, proc) != 0) {//3.调用copy_mm()函数复制父进程的内存信息到子进程
        goto bad_fork_cleanup_kstack;

(2)copy_mm函数(kern/process/proc.c)
此函数在lab4中还未实现,在本次实验中可以看到已经实现,并使用互斥锁,用于避免多个进程同时访问内存,在这里进行了下一层调用:即调用了dup_mmap()函数。

 lock_mm(oldmm);//打开互斥锁,避免多个进程同时访问内存
    {
        ret = dup_mmap(mm, oldmm);//调用dup_mmap函数
    }
    unlock_mm(oldmm);//释放互斥锁

(3)dup_mmap函数(kern/mm/vmm.c)
首先看传入的参数,是两个内存mm,在上一个函数copy_mm中,传入的两个内存叫做mm和oldmm,其中,第一个mm只是调用了mm_create()声明,但没有初始化,更没有分配内容;第二个oldmm是current进程的内存空间,由此可见,前一个mm是待复制的内存,而复制的源内容在oldmm(父进程)内容中。

bool share = 0;
        if (copy_range(to->pgdir, from->pgdir, vma->vm_start, vma->vm_end, share) != 0) {//调用copy_range函数
            return -E_NO_MEM;
        }
3.2.3代码实现(copy_range函数(kern/mm/pmm.c))

在上一个函数中(dup_mmap),只是完成了新进程中的段创建,但是段中还没有具体内容,需要在copy_range中具体复制父进程对应段中的具体内容。这个函数传入的参数都是段指针,告诉系统应该复制内存中需要复制内容的起止地址。具体实现如下,可以看到其实copy_range函数就是调用一个memcpy将父进程的内存直接复制给子进程即可。

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));
    do {
        pte_t *ptep = get_pte(from, start, 0), *nptep;//获取页表内容,这里调用的是lab2的函数,获取页表
        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
        if (*ptep & PTE_P) {
            if ((nptep = get_pte(to, start, 1)) == NULL) {
                return -E_NO_MEM;
            }
        uint32_t perm = (*ptep & PTE_USER);
        struct Page *page = pte2page(*ptep);//使用pte2page获取页表的值
        struct Page *npage=alloc_page();
        assert(page!=NULL);
        assert(npage!=NULL);
        int ret=0;
        
        void * kva_src = page2kva(page);
        void * kva_dst = page2kva(npage); 
    
        memcpy(kva_dst, kva_src, PGSIZE);//复制父进程到子进程  

        ret = page_insert(to, npage, start, perm);
        assert(ret == 0);
        }
        start += PGSIZE;
    } while (start != 0 && start < end);
    return 0;
}

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

3.3.1实验要求

请在实验报告中简要说明你对 fork/exec/wait/exit函数的分析。并回答如下问题:
·请分析fork/exec/wait/exit在实现中是如何影响进程的执行状态的?
·请给出ucore中一个用户态进程的执行状态生命周期图(包执行状态,执行状态之间的变换关系,以及产生变换的事件或函数调用)。(字符方式画即可)

3.3.2关键数据结构及知识点

(1)fork函数(kern/process/proc.c)
此函数的调用过程为fork->SYS_fork->do_fork+wakeup_proc,在之前的lab2中已经对此函数进行了详细的解释,具体实现功能如下:
①分配并初始化进程控制块(alloc_proc 函数);
②分配并初始化内核栈(setup_stack 函数);
③根据 clone_flag标志复制或共享进程内存管理结构(copy_mm 函数);
④设置进程在内核(将来也包括用户态)正常运行和调度所需的中断帧和执行上下文(copy_thread 函数);
⑤把设置好的进程控制块放入hash_list 和 proc_list 两个全局进程链表中;
⑥自此,进程已经准备好执行了,把进程状态设置为“就绪”态;
⑦设置返回码为子进程的 id号。
注:wakeup_proc函数主要是将进程的状态设置为等待。
fork函数关键代码如下:

if ((proc = alloc_proc()) == NULL) {//1.调用alloc_proc()函数申请内存块
        goto fork_out;
    }

    proc->parent = current;//将子进程的父节点设置为当前进程
    assert(current->wait_state == 0);//确保当前进程正在等待
    if (setup_kstack(proc) != 0) {//2.调用setup_stack()函数为进程分配一个内核栈
        goto bad_fork_cleanup_proc;
    }
    if (copy_mm(clone_flags, proc) != 0) {//3.调用copy_mm()函数复制父进程的内存信息到子进程
        goto bad_fork_cleanup_kstack;
    }
    copy_thread(proc, stack, tf);//4.调用copy_thread()函数复制父进程的中断帧和上下文信息
    //5.将新进程添加到进程的(hash)列表中
    bool intr_flag;
    local_intr_save(intr_flag);//屏蔽中断,intr_flag置为1
    {
        proc->pid = get_pid();//获取当前进程PID
        hash_proc(proc); //建立hash映射
        set_links(proc);//设置进程链接
    }
    local_intr_restore(intr_flag);//恢复中断

    wakeup_proc(proc);//6.唤醒新进程

    ret = proc->pid;//7.返回当前进程的PID

(2)exec函数(kern/process/proc.c)
此函数调用过程为SYS_exec->do_execve,如下主要功能:
①首先为加载新的执行码做好用户态内存空间清空准备。如果mm不为NULL,则设置页表为内核空间页表,且进一步判断mm的引用计数减1后是否为0,如果为0,则表明没有进程再需要此进程所占用的内存空间,为此将根据mm中的记录,释放进程所占用户空间内存和进程页表本身所占空间。最后把当前进程的mm内存管理指针为空。
②接下来是加载应用程序执行码到当前进程的新创建的用户态虚拟空间中。之后就是调用load_icode从而使之准备好执行。
exec函数关键代码如下:

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);//清空内存管理部分和对应页表
            put_pgdir(mm);//清空页表
            mm_destroy(mm);//清空内存
        }
        current->mm = NULL;//最后让它当前的页表指向空,方便放入自己的东西
    }
    int ret;
    if ((ret = load_icode(binary, size)) != 0) {//第二步:向清空的内存中填充新的内容,调用load_icode函数
        goto execve_exit;
    }
    set_proc_name(current, local_name);
    return 0;

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

(3)wait函数(kern/process/proc.c)
此函数调用过程为SYS_wait->do_wait,主要工作就是父进程如何完成对子进程的最后回收工作,具体的功能实现如下:
①如果 pid!=0,表示只找一个进程 id 号为 pid 的退出状态的子进程,否则找任意一个处于退出状态的子进程;
②如果此子进程的执行状态不为PROC_ZOMBIE,表明此子进程还没有退出,则当前进程设置执行状态为PROC_SLEEPING(睡眠),睡眠原因为WT_CHILD(即等待子进程退出),调用schedule()函数选择新的进程执行,自己睡眠等待,如果被唤醒,则重复跳回步骤 1 处执行;
③如果此子进程的执行状态为 PROC_ZOMBIE,表明此子进程处于退出状态,需要当前进程(即子进程的父进程)完成对子进程的最终回收工作,即首先把子进程控制块从两个进程队列proc_list和hash_list中删除,并释放子进程的内核堆栈和进程控制块。自此,子进程才彻底地结束了它的执行过程,它所占用的所有资源均已释放。
wait函数关键代码如下:

if (pid != 0) {//如果pid!=0,则找到进程id为pid的处于退出状态的子进程 
        proc = find_proc(pid);
        if (proc != NULL && proc->parent == current) {
            haskid = 1;
            if (proc->state == PROC_ZOMBIE) {
                goto found;//找到进程
            }
        }
    }
    else {//如果pid==0,则随意找一个处于退出状态的子进程
        proc = current->cptr;
        for (; proc != NULL; proc = proc->optr) {
            haskid = 1;
//如果子进程不处于僵尸状态,那么会变成睡眠状态,因为需要等待子进程退出,之后调用schedule函数挂起自己,选择其他进程执行。如果为僵尸状态,那么会清除该进程。
            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;

(4)exit函数(kern/process/proc.c)
此函数调用过程为SYS_exit->exit,主要工作如下:
①先判断是否是用户进程,如果是,则开始回收此用户进程所占用的用户态虚拟内存空间
②设置当前进程状态为PROC_ZOMBIE,然后设置当前进程的退出码为error_code。此时这个进程已经无法再被调度了,只能等待父进程来完成最后的回收工作。
③如果当前父进程已经处于等待子进程的状态,即父进程的wait_state被置为WT_CHILD,则此时就可以唤醒父进程,让父进程来帮子进程完成最后的资源回收工作。
④如果当前进程还有子进程,则需要把这些子进程的父进程指针设置为内核线程init,且各个子进程指针需要插入到init的子进程链表中。如果某个子进程的执行状态是 PROC_ZOMBIE,则需要唤醒 init来完成对此子进程的最后回收工作。
⑤执行schedule()调度函数,选择新的进程执行。
exit函数关键代码如下:

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);//释放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);//如果父进程在等待子进程,则唤醒
        }
        while (current->cptr != NULL) {
/*如果当前进程还有子进程(孤儿进程),则需要把这些子进程的父进程指针设置为内核线程initproc,且各个子进程指针需要插入
到initproc的子进程链表中。如果某个子进程的执行状态是PROC_ZOMBIE,则需要唤醒initproc来完成对此子进程的最后回收工作。*/
            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);
}

四、思考题

Q1:(练习2)请在实验报告中简要说明如何设计实现”Copy on Write 机制“,给出概要设计,鼓励给出详细设计。
“Copy on Write”是指在fork一个进程时不立刻将父进程的数据段/代码段等复制到子进程的内存空间,而是当父进程或子进程中对相关内存做出修改时,才进行复制操作。
实现时,在fork一个进程时,可以省去load_icode中创建新页目录的操作,而是直接将父进程页目录的地址赋给子进程,为了防止误操作以及辨别是否需要复制,应该将尚未完成复制的部分的访问权限设为只读。
当执行读操作,父进程和子进程均不受影响。但当执行写操作时,会发生权限错误(因为此时的访问权限为只读)。这时候会进入到page fault的处理中去,在page fault的处理中,如果发现错误原因读/写权限问题,而访问的段的段描述符权限为可写,便可以知道是由于使用COW机制而导致的,这时再将父进程的数据段、代码段等复制到子进程内存空间上即可。

Q2:(练习3)请分析fork/exec/wait/exit在实现中是如何影响进程的执行状态的?
①fork:执行完毕后,如果创建新进程成功,则出现两个进程,一个是子进程,一个是父进程。在子进程中,fork函数返回0,在父进程中,fork返回新创建子进程的进程ID。我们可以通过fork返回的值来判断当前进程是子进程还是父进程
②exit:会把一个退出码error_code传递给ucore,ucore通过执行内核函数do_exit来完成对当前进程的退出处理,主要工作简单地说就是回收当前进程所占的大部分内存资源,并通知父进程完成最后的回收工作。
③execve:完成用户进程的创建工作。首先为加载新的执行码做好用户态内存空间清空准备。接下来的一步是加载应用程序执行码到当前进程的新创建的用户态虚拟空间中。
④wait:等待任意子进程的结束通知。wait_pid函数等待进程id号为pid的子进程结束通知。这两个函数最终访问sys_wait系统调用接口让ucore来完成对子进程的最后回收工作。

Q3:(练习3)请给出ucore中一个用户态进程的执行状态生命周期图(包执行状态,执行状态之间的变换关系,以及产生变换的事件或函数调用)。(字符方式画即可)
在这里插入图片描述

五、运行结果

执行:make grade。如下图所示应用程序检测都输出ok,得分150/150,所以结果正确。
在这里插入图片描述

六、实验心得

通过本次实验对用户进程管理有了更深入的学习与理解,通过验收以及助教老师的提问对这部分内容掌握的更加牢固,也理清了在用户进程管理过程中一些函数的调用关系,相信本次实验的内容与收获会对今后的学习起到很好的帮助。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值