湖南大学操作系统实验ucore lab5

阅读前注意事项:

1、我的博客从lab2之后,如果没有特殊说明,所有标注的代码行数位置,以labcodes_answer(答案包)里的文件为准!!!因为你以后会发现做实验用meld软件比较费时费力,对于咱们学校的验收不如直接对着答案来;

2、感谢网上的各路前辈大佬们,本人在这学期初次完成实验的过程中,各位前辈们的博客给了我很多有用的指导;本人的博客内容在现有的内容上,做了不少细节的增补内容,有些地方属个人理解,如果有错在所难免,还请各位大佬们批评指正;

3、所有实验的思考题,我把它规整到了文章最后;

4、所有实验均默认不做challenge,对实验评分无影响;

5、湖南大学的实验顺序为1 4 5 6 7 2 3 8,在实验4-7过程中涉及到实验二三的页表虚存问题,当做黑盒处理,没有过多探索。

一、实验内容

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

 

二、目的

了解第一个用户进程创建过程

了解系统调用框架的实现机制

了解ucore如何实现系统调用sys_fork/sys_exec/sys_exit/sys_wait来进行进程管理

 

三、实验设计思想和流程

 

练习0:填写已有实验

 

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

 

使用meld工具,比对得出需要改动的代码文件为:

 

proc.c

default_pmm.c

pmm.c

swap_fifo.c

vmm.c

trap.c

 

需要改进的代码部分如下:

 

1、实验4的alloc_proc函数需要增加两行(kern/process/proc.c,118——119行)

proc->wait_state = 0;
proc->cptr = proc->optr = proc->yptr = NULL;

原因:观察到实验四中的TCB部分(kern/process/proc.h)中对于内核线程的结构体声明多出了两个部分:

uint32_t wait_state;			                // waiting state
struct proc_struct *cptr, *yptr, *optr;			// relations between processes

新加入的两个属性,第一个是进程等待状态,第二个是进程之间的相关指针初始化,在实验五中,涉及到了用户进程,自然需要涉及到调度的问题,所以进程等待状态和各种指针需要被初始化。

 

2、实验4的do_fork函数需要增加两行(kern/process/proc.c,411行、426行)

 

经过更改的函数如下:(实验4实现部分,对应kern/process/proc.c第406——431行)

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

其中,更改的部分语句一共有二,如上所示。

 

第一句:assert(current->wait_state == 0);

对应第一步中对于proc->wait_state属性的初始分配,需要确保当前进程正在等待。

 

第二句:set_links(proc); 这里调用了一个set link函数来设置进程之间的连接。

 

观察set links函数代码(kern/process/proc.c,140——149行)

static void set_links(struct proc_struct *proc) {
    list_add(&proc_list, &(proc->list_link));         //进程加入进程列表
    proc->yptr = NULL;                                //当前进程的younger sibling为空
    if ((proc->optr = proc->parent->cptr) != NULL) {
        proc->optr->yptr = proc;
    }		////当前进程的older sibling为当前进程
    proc->parent->cptr = proc;                            //父进程的子进程为当前进程
    nr_process ++;                                        //当前进程数量加一
}

set_links函数的作用就是设置当前进程的process relations。同样,它进行一些插入进程、调度、更改“当前进程数量”等对于共享数据的访问操作,因此它还在实验4中定义的互斥锁中。

 

3、实验1中的idt_init函数(kern/trap/trap.c,59——66行)

    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);//内核态优先级为0
    }
SETGATE(idt[T_SYSCALL], 1, GD_KTEXT, __vectors[T_SYSCALL], DPL_USER);
//内核态优先级为3
lidt(&idt_pd);

需要加上一行,主要是设置相应的中断门。

 

4、实验1中的trap_dispatch函数(kern/trap/trap.c,233——238行)

ticks ++;
  	if (ticks % TICK_NUM == 0) {
		assert(current != NULL);
		current->need_resched = 1;
	}
	break;

主要是将时间片设置为需要调度,说明当前进程的时间片已经用完了。

 

练习1:加载应用程序并运行

 

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

 

load_icode函数的主要工作就是给用户进程建立一个能够让用户进程正常运行的用户环境。此函数有一百多行,完成了如下重要工作:

 

1. 调用mm_create函数来申请进程的内存管理数据结构mm所需内存空间,并对mm进行初始化;

2. 调用setup_pgdir来申请一个页目录表所需的一个页大小的内存空间,并把描述ucore内核虚空间映射的内核页表(boot_pgdir所指)的内容拷贝到此新目录表中,最后mm->pgdir指向此页目录表,这就是进程新的页目录表了,且能够正确映射内核。

3. 根据应用程序执行码的起始位置来解析此ELF格式的执行程序,并调用mm_map函数根据ELF格式的执行程序说明的各个段(代码段、数据段、BSS段等)的起始位置和大小建立对应的vma结构,并把vma插入到mm结构中,从而表明了用户进程的合法用户态虚拟地址空间。

4. 调用根据执行程序各个段的大小分配物理内存空间,并根据执行程序各个段的起始位置确定虚拟地址,并在页表中建立好物理地址和虚拟地址的映射关系,然后把执行程序各个段的内容拷贝到相应的内核虚拟地址中,至此应用程序执行码和数据已经根据编译时设定地址放置到虚拟内存中了;

5. 需要给用户进程设置用户栈,为此调用mm_mmap函数建立用户栈的vma结构,明确用户栈的位置在用户虚空间的顶端,大小为256个页,即1MB,并分配一定数量的物理内存且建立好栈的虚地址<-->物理地址映射关系;

6. 至此,进程内的内存管理vma和mm数据结构已经建立完成,于是把mm->pgdir赋值到cr3寄存器中,即更新了用户进程的虚拟内存空间,此时的initproc已经被hello的代码和数据覆盖,成为了第一个用户进程,但此时这个用户进程的执行现场还没建立好;

7. 先清空进程的中断帧,再重新设置进程的中断帧,使得在执行中断返回指令“iret”后,能够让CPU转到用户态特权级,并回到用户态内存空间,使用用户态的代码段、数据段和堆栈,且能够跳转到用户进程的第一条指令执行,并确保在用户态能够响应中断;

 

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

 

do_execve函数主要做的工作就是先回收自身所占用户空间,然后调用load_icode,用新的程序覆盖内存空间,形成一个执行新程序的新进程。

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

static int load_icode(unsigned char *binary, size_t size) {
    if (current->mm != NULL) {
        panic("load_icode: current->mm must be empty.\n");
    }

//准备部分:当前进程必须为空,这样才能加载到内存。
(在调用它的do_exceve函数中,如果没有bug,那么已经清空了)
    int ret = -E_NO_MEM;
struct mm_struct *mm;		//声明了一个页表

//第1步:为当前的进程创建一块内存

    if ((mm = mm_create()) == NULL) {//申请内存
        goto bad_mm;
}

这里调用了一个函数mm_create(kern/mm/vmm.c,43——60行):

struct mm_struct * 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;
}
可以看到这个函数首先申请了一块内存空间,如果内存空间申请成功了,那么就会把这个内存空间返回给外面调用它的mm变量,如果申请失败,那么新开辟的空间都不存在,即为NULL,且会返回它自己,因此外部的判断条件是mm不能等于NULL,如果等于NULL,说明创建空间失败了,否则,就能够说明创建成功。

//(2) create a new PDT, and mm->pgdir= kernel virtual addr of PDT
第2步:调用 setup_pgdir来申请一个页目录表所需的一个页大小的内存空间
    
if (setup_pgdir(mm) != 0) {//申请页表
        goto bad_pgdir_cleanup_mm;
}

这里调用了一个函数setup_pgdir(kern/process/proc.c,288——299行)

static int 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;
}
如果没有返回0,那么分配页目录表失败,因此程序需要判断为0的情况,到一个错误的状态。

//第3步:读取ELF格式的文件,在内存中复制该进程所需要的代码段等信息
    //(3) copy TEXT/DATA section, build BSS parts in binary to memory space of process
    struct Page *page;			//申请一个页
    //(3.1) get the file header of the bianry program (ELF format)
struct elfhdr *elf = (struct elfhdr *)binary;		//获取ELF格式文件的表头

在bootloader启动的过程中,已经将ucore内核和用户代码全部加载到内存,因为没有文件管理系统,我们只需要关注这个代码在内存中的哪里,找到了开头就能根据它找到数据段。

    //(3.2) get the entry of the program section headers of the bianry program (ELF format)
struct proghdr *ph = (struct proghdr *)(binary + elf->e_phoff);
//(3.3) This program is valid?
    if (elf->e_magic != ELF_MAGIC) {		//这个ELF文件的格式是否是合法的?
        ret = -E_INVAL_ELF;				//返回一个ELF文件非法操作
        goto bad_elf_cleanup_pgdir;
    }

    uint32_t vm_flags, perm;
    struct proghdr *ph_end = ph + elf->e_phnum;
    for (; ph < ph_end; ph ++) {
    //(3.4) find every program section headers
        if (ph->p_type != ELF_PT_LOAD) {
            continue ;
        }
        if (ph->p_filesz > ph->p_memsz) {
            ret = -E_INVAL_ELF;
            goto bad_cleanup_mmap;
        }
        if (ph->p_filesz == 0) {
            continue ;
        }
//这个地方获取的是文件的各个段,包括代码段、数据段等。

//(3.5) call mm_map fun to setup the new vma ( ph->p_va, ph->p_memsz)
根据获取的各个段的开头,以及虚拟地址创建VMA(管理进程所认为的合法空间)
一开始给各个段赋予了一些属性:

        vm_flags = 0, perm = PTE_U;
        if (ph->p_flags & ELF_PF_X) vm_flags |= VM_EXEC;		//可执行属性(代码段)
        if (ph->p_flags & ELF_PF_W) vm_flags |= VM_WRITE;		//可读可写(数据段)
        if (ph->p_flags & ELF_PF_R) vm_flags |= VM_READ;
        if (vm_flags & VM_WRITE) perm |= PTE_W;
        if ((ret = mm_map(mm, ph->p_va, ph->p_memsz, vm_flags, NULL)) != 0) {
            goto bad_cleanup_mmap;
        }

//使用mm_map函数建立合法空间(kern/mm/vmm.c,159——165行)
int 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;
    }
//关于mm_map的解释是:

        unsigned char *from = binary + ph->p_offset;
        size_t off, size;
        uintptr_t start = ph->p_va, end, la = ROUNDDOWN(start, PGSIZE);

        ret = -E_NO_MEM;

//(3.6) alloc memory, and  copy the contents of every program section (from, from+end) to process's memory (la, la+end)
        end = ph->p_va + ph->p_filesz;
 		//(3.6.1) copy TEXT/DATA section of bianry program
//这里是拷贝内容,memcpy是拷贝函数
        while (start < end) {
            if ((page = pgdir_alloc_page(mm->pgdir, la, perm)) == NULL) {
                goto bad_cleanup_mmap;
            }
            off = start - la, size = PGSIZE - off, la += PGSIZE;
            if (end < la) {
                size -= la - end;
            }
            memcpy(page2kva(page) + off, from, size);//拷贝函数
            start += size, from += size;
        }

//(3.6.2) build BSS section of binary program
//执行程序的BSS段需要清空,这里全部设置为0
        end = ph->p_va + ph->p_memsz;
        if (start < la) {
            /* ph->p_memsz == ph->p_filesz */
            if (start == end) {
                continue ;
            }
            off = start + PGSIZE - la, size = PGSIZE - off;
            if (end < la) {
                size -= la - end;
            }
            memset(page2kva(page) + off, 0, size);//设置为0
            start += size;
            assert((end < la && start == end) || (end >= la && start == la));
        }
        while (start < end) {
            if ((page = pgdir_alloc_page(mm->pgdir, la, perm)) == NULL) {
                goto bad_cleanup_mmap;
            }
            off = start - la, size = PGSIZE - off, la += PGSIZE;
            if (end < la) {
                size -= la - end;
            }
            memset(page2kva(page) + off, 0, size);
            start += size;
        }
    }
//(4) build user stack memory
//除了数据段、代码段、进程还需要用户堆栈空间。这里是构造用户堆栈。
    vm_flags = VM_READ | VM_WRITE | VM_STACK;
    if ((ret = mm_map(mm, USTACKTOP - USTACKSIZE, USTACKSIZE, vm_flags, NULL)) != 0) 	{
        goto bad_cleanup_mmap;
}
//重新建立mm_map堆栈
    assert(pgdir_alloc_page(mm->pgdir, USTACKTOP-PGSIZE , PTE_USER) != NULL);
    assert(pgdir_alloc_page(mm->pgdir, USTACKTOP-2*PGSIZE , PTE_USER) != NULL);
    assert(pgdir_alloc_page(mm->pgdir, USTACKTOP-3*PGSIZE , PTE_USER) != NULL);
    assert(pgdir_alloc_page(mm->pgdir, USTACKTOP-4*PGSIZE , PTE_USER) != NULL);
    
//(5) set current process's mm, sr3, and set CR3 reg = physical addr of Page Directory
//建立好映射关系
    mm_count_inc(mm);
    current->mm = mm;
    current->cr3 = PADDR(mm->pgdir);
    lcr3(PADDR(mm->pgdir));

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

//完成一个优先级的转变,从内核态切换到用户态(特权级从0到3)实现部分
    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;

*tf是一个是中断帧的指针,总是指向内核栈的某个位置:当进程从用户空间跳到内核空间时,中断帧记录了进程在被中断前的状态。当内核需要跳回用户空间时,需要调整中断帧以恢复让进程继续执行的各寄存器值。
其定义在(kern/trap/trap.h,60——82行)。
1、将tf_cs设置为用户态,这个定义在(kern/mm/memlayout.h,第21行),有一个宏定义已经定义了用户态和内核态。
2、tf_ds=tf_es=tf_ss也需要设置为用户态:定义在(kern/mm/memlayout.h,第26行)
3、需要将esp设置为用户栈的栈顶,直接使用之前建立用户栈时的参数USTACKTOP就可以。
4、eip是程序的入口,elf类的e_entry函数直接声明了,直接使用。
5、FL_IF打开中断。


    ret = 0;
out:
    return ret;
bad_cleanup_mmap:
    exit_mmap(mm);
bad_elf_cleanup_pgdir:
    put_pgdir(mm);
bad_pgdir_cleanup_mm:
    mm_destroy(mm);
bad_mm:
    goto out;
}

总结:调用流程:

练习2:父进程复制自己的内存空间给子进程

 

创建子进程的函数do_fork在执行中将拷贝当前进程(即父进程)的用户内存地址空间中的合法内容到新进程中(子进程),完成内存资源的复制。具体是通过copy_range函数(位于kern/mm/pmm.c中)实现的,请补充copy_range的实现,确保能够正确执行。

 

Copy on Write:创建子进程的时候能够共享父进程的内存空间,节省内存占用。

 

copy_range函数的调用过程:do_fork()---->copy_mm()---->dup_mmap()---->copy_range()

 

首先是do_fork()的分析:(kern/process/proc.c,第373——442行)

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 YOUR CODE
    /*
     * 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
     *   wakup_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 wakup_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;
    }

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

其中,do_fork函数调用的copy_mm函数在实验四中没有实现,其他的过程和实验四一样,都是创建一个进程,并放入CPU中调度,而本次我们主要关注的是父子进程之间如何拷贝内存。

 

copy_mm函数(kern/process/proc.c,第309——338行)

static int copy_mm(uint32_t clone_flags, struct proc_struct *proc) {
    struct mm_struct *mm, *oldmm = current->mm;

    /* current is a kernel thread */
    if (oldmm == NULL) {		//当前进程内存为空,返回0,复制失败!!
        return 0;
    }
    if (clone_flags & CLONE_VM) {		//如果共享内存标记位为真,那么可以共享内存
        mm = oldmm;
        goto good_mm;
    }

    int ret = -E_NO_MEM;
    if ((mm = mm_create()) == NULL) {		//如果创建地址空间失败,报错
        goto bad_mm;
    }
    if (setup_pgdir(mm) != 0) {	//如果创建页表失败报错(kern/process/proc.c,288行)
        goto bad_pgdir_cleanup_mm;
    }

    lock_mm(oldmm);		//这是一个互斥锁,用于避免多个进程同时访问内存
    {
        ret = dup_mmap(mm, oldmm);		//下一层调用
    }
    unlock_mm(oldmm);

    if (ret != 0) {
        goto bad_dup_cleanup_mmap;
}
good_mm:
    mm_count_inc(mm);
    proc->mm = mm;
    proc->cr3 = PADDR(mm->pgdir);
    return 0;
bad_dup_cleanup_mmap:
    exit_mmap(mm);
    put_pgdir(mm);
bad_pgdir_cleanup_mm:
    mm_destroy(mm);
bad_mm:
    return ret;
}

我们可以看到,在这一层调用中有一个互斥锁,用于避免多个进程同时访问内存,在这里进行了下一层调用:

    lock_mm(oldmm);		//这是一个互斥锁,用于避免多个进程同时访问内存
    {
        ret = dup_mmap(mm, oldmm);		//下一层调用
    }
    unlock_mm(oldmm);

dup_mmap函数:(kern/mm/vmm.c,191——209行)

 

首先看传入的参数,是两个内存mm,这是为什么呢?

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

int dup_mmap(struct mm_struct *to, struct mm_struct *from) 
{
    assert(to != NULL && from != NULL);			//首先确保两块东西创建成功
    list_entry_t *list = &(from->mmap_list), *le = list;		//获取from的首地址
    while ((le = list_prev(le)) != list) {			//对所有段都遍历一遍
        struct vma_struct *vma, *nvma;
        vma = le2vma(le, list_link);		//获取某一段信息,并创建到新进程中
        nvma = vma_create(vma->vm_start, vma->vm_end, vma->vm_flags);
        if (nvma == NULL) {
            return -E_NO_MEM;
        }
        insert_vma_struct(to, nvma);			//把这一个段插入到子进程内存中
        bool share = 0;
        if (copy_range(to->pgdir, from->pgdir, vma->vm_start, vma->vm_end, share) != 0) {			//调用copy_range函数
            return -E_NO_MEM;
        }
    }
    return 0;
}

copy_range函数:(kern/mm/pmm.c,506——556行)

 

在上一个函数中(dup_mmap),只是完成了新进程中的段创建,但是段中还没有具体内容,需要在copy_range中具体复制父进程对应段中的具体内容。这个函数传入的参数都是段指针,告诉系统应该复制内存中需要复制内容的起止地址。

 

copy range函数补充实现如下:

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
        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);
        //get page from ptep
        struct Page *page = pte2page(*ptep);
//使用pte2page获取页表的值,相关注释在(kern/mm/pmm.c,439行)
        // alloc a page for process B
        struct Page *npage=alloc_page();
        assert(page!=NULL);
        assert(npage!=NULL);
        int ret=0;
        /* 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);	//获取老页表的值
        void * kva_dst = page2kva(npage);	//获取新页表的值
        memcpy(kva_dst, kva_src, PGSIZE);	//复制操作
        ret = page_insert(to, npage, start, perm);
//建立子进程页地址起始位置与物理地址的映射关系,prem是权限
        assert(ret == 0);
        }
        start += PGSIZE;
    } while (start != 0 && start < end);
    return 0;
}

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

 

fork:练习二和实验四均提到并实现过:(kern/process/proc.c,第373——442行,代码见上面的练习)

 

1、分配并初始化进程控制块(alloc_proc 函数);

2、分配并初始化内核栈(setup_stack 函数);

3、根据 clone_flag标志复制或共享进程内存管理结构(copy_mm 函数);

4、设置进程在内核(将来也包括用户态)正常运行和调度所需的中断帧和执行上下文(copy_thread 函数);

5、把设置好的进程控制块放入hash_list 和 proc_list 两个全局进程链表中;

6、自此,进程已经准备好执行了,把进程状态设置为“就绪”态;

7、设置返回码为子进程的 id 号。

 

do_execve:(kern/process/proc,652——685行)

 

首先为加载新的执行码做好用户态内存空间清空准备。如果mm不为NULL,则设置页表为内核空间页表。

接下来是加载应用程序执行码到当前进程的新创建的用户态虚拟空间中。之后就是调用。

(在练习1中分析过)

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

do_wait函数:(kern/process/proc.c,698——755行)

 

当执行wait功能的时候,会调用系统调用SYS_wait,而该系统调用的功能则主要由do_wait函数实现,主要工作就是父进程如何完成对子进程的最后回收工作,具体的功能实现如下:

1、 如果 pid!=0,表示只找一个进程 id 号为 pid 的退出状态的子进程,否则找任意一个处于退出状态的子进程;

2、 如果此子进程的执行状态不为PROC_ZOMBIE,表明此子进程还没有退出,则当前进程设置执行状态为PROC_SLEEPING(睡眠),睡眠原因为WT_CHILD(即等待子进程退出),调用schedule()函数选择新的进程执行,自己睡眠等待,如果被唤醒,则重复跳回步骤 1 处执行;

3、 如果此子进程的执行状态为 PROC_ZOMBIE,表明此子进程处于退出状态,需要当前进程(即子进程的父进程)完成对子进程的最终回收工作,即首先把子进程控制块从两个进程队列proc_list和hash_list中删除,并释放子进程的内核堆栈和进程控制块。自此,子进程才彻底地结束了它的执行过程,它所占用的所有资源均已释放。

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

do_exit函数:(kern/process/proc.c,448——499行)

 

1、先判断是否是用户进程,如果是,则开始回收此用户进程所占用的用户态虚拟内存空间

2、设置当前进程状态为PROC_ZOMBIE,然后设置当前进程的退出码为error_code。此时这个进程已经无法再被调度了,只能等待父进程来完成最后的回收工作。

3、如果当前父进程已经处于等待子进程的状态,即父进程的wait_state被置为WT_CHILD,则此时就可以唤醒父进程,让父进程来帮子进程完成最后的资源回收工作。

4、如果当前进程还有子进程,则需要把这些子进程的父进程指针设置为内核线程init,且各个子进程指针需要插入到init的子进程链表中。如果某个子进程的执行状态是 PROC_ZOMBIE,则需要唤醒 init来完成对此子进程的最后回收工作。

5、执行schedule()调度函数,选择新的进程执行。

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);
        }
        while (current->cptr != NULL) {
//如果当前进程还有子进程(孤儿进程),则需要把这些子进程的父进程指针设置为内核线程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);
}

 

四、实验体会和思考题

 

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

 

答:详见练习一。

 

2、Copy on Write机制

 

“Copy on Write”是指在fork一个进程时不立刻将父进程的数据段/代码段等复制到子进程的内存空间,而是当父进程或子进程中对相关内存做出修改时,才进行复制操作。

 

实现时,在fork一个进程时,可以省去load_icode中创建新页目录的操作,而是直接将父进程页目录的地址赋给子进程,为了防止误操作以及辨别是否需要复制,应该将尚未完成复制的部分的访问权限设为只读。

 

当执行读操作,父进程和子进程均不受影响。但当执行写操作时,会发生权限错误(因为此时的访问权限为只读)。这时候会进入到page fault的处理中去,在page fault的处理中,如果发现错误原因读/写权限问题,而访问的段的段描述符权限为可写,便可以知道是由于使用COW机制而导致的,这时再将父进程的数据段、代码段等复制到子进程内存空间上即可。

 

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

 

fork:如果创建新进程成功,则出现一个子进程一个父进程。在子进程中,fork函数返回0,在父进程中,fork返回新创建子进程的进程ID。

 

exit:会把一个退出码error_code传递给操作系统,操作系统通过执行内核函数do_exit来完成对当前进程的退出处理,工作是回收当前进程所占内存资源。

 

execve:完成用户进程的创建工作。首先为加载新的执行码做好用户态内存空间清空准备。再加载应用程序执行码到当前进程的新创建的用户态虚拟空间中。

 

wait:等待子进程的结束通知。wait_pid函数等待进程id号为pid的子进程结束通知,让ucore来完成对子进程的最后回收工作。

 

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

五、运行结果

如果make grade无法满分,尝试注释掉tools/grade.sh的221行到233行(在前面加上“#”)。

 

猜你喜欢

转载自blog.csdn.net/yyd19981117/article/details/86693538