操作系统ucore lab5实验报告

操作系统lab5实验报告

到实验四为止,ucore还一直在核心态“打转”,没有到用户态执行。创建用户进程,让用户进程在用户态执行,且在需要ucore支持时,可通过系统调用来让ucore提供服务。而本实验将进程的执行空间扩展到了用户态空间,出现了创建子进程执行应用程序等。即实验五主要是分析用户进程的整个生命周期来阐述用户进程管理的设计与实现。

练习0 填写已有实验

这里和前几个实验一样,照样运用meld软件进行对比,大致的截图如下:

这里简单将我们需要修改的地方罗列如下:

proc.c
default_pmm.c
pmm.c
swap_fifo.c
vmm.c
trap.c

另外根据试验要求,我们需要对部分代码进行改进,这里讲需要改进的地方的代码和说明罗列如下:

alloc_proc函数

改进后的alloc_proc函数如下:

static struct proc_struct *alloc_proc(void) {
    struct proc_struct *proc = kmalloc(sizeof(struct proc_struct));
    if (proc != NULL) {
        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;
}

比起改进之前多了这两行代码:

        proc->wait_state = 0;//初始化进程等待状态
        proc->cptr = proc->optr = proc->yptr = NULL;//进程相关指针初始化  

这里解释proc的几个新指针:

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)

就像注释所写的,这两行代码主要是初始化进程等待状态、和进程的相关指针,例如父进程、子进程、同胞等等。

因为这里涉及到了用户进程,自然需要涉及到调度的问题,所以进程等待状态和各种指针需要被初始化。

do_fork函数

改进后的do_fork函数如下:

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;
    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);//将原来简单的计数改成来执行set_links函数,从而实现设置进程的相关链接 

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

改动主要是上述代码中含注释的两行,第一行是为了确定当前的进程正在等待,第二行是将原来的计数换成了执行一个set_links函数,因为要涉及到进程的调度,所以简单的计数肯定是不行的。

idt_init函数

改进后的idt_init函数如下:

void idt_init(void) {
    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);
}

相比于之前,多了这一行代码:

SETGATE(idt[T_SYSCALL], 1, GD_KTEXT, __vectors[T_SYSCALL], DPL_USER);////这里主要是设置相应的中断门

trap_dispatch函数

改进后的部分函数如下:

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

相比与原来主要是多了这一行代码

current->need_resched = 1;

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

练习1 加载应用程序并执行

根据实验说明书,我们需要完善的函数是load_icode函数。

这里介绍下这个函数的功能:load_icode函数主要用来被do_execve调用,将执行程序加载到进程空间(执行程序本身已从磁盘读取到内存中),这涉及到修改页表、分配用户栈等工作。

该函数主要完成的工作如下:

  • 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 寄存器中,即更新了用户进程的虚拟内存空间,此时的 init 已经被 exit 的代码和数据覆盖,成为了第一个用户进程,但此时这个用户进程的执行现场还没建立好;
  • 7.先清空进程的中断帧,再重新设置进程的中断帧,使得在执行中断返回指令iret后,能够让 CPU转到用户态特权级,并回到用户态内存空间,使用用户态的代码段、数据段和堆栈,且能够跳转到用户进程的第一条指令执行,并确保在用户态能够响应中断;

简单的说,该load_icode 函数的主要工作就是给用户进程建立一个能够让用户进程正常运行的用户环境。

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

由于该完整函数太长,所以这里我只将我们补充的部分罗如下:

static int load_icode(unsigned char *binary, size_t size) {
    ......
    ......
    /* 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;//0xB0000000
    tf->tf_eip = elf->e_entry;//
    tf->tf_eflags = FL_IF;
    ......
    ......
}

根据注释这里我们主要完成的是proc_struct结构中tf结构体变量的设置,因为这里我们要设置好tf以便于从内核态切换到用户态然后执行程序,所以这里tf_cs即代码段设置为USER_CS、将tf->tf_dstf->tf_estf->tf_ss均设置为USER_DS。

至于之后的tf_esptf_eip的设置需要看这个图,这是一个完整的虚拟内存空间的分布图:

4G ------------------> +---------------------------------+
 *                            |                                 |
 *                            |         Empty Memory (*)        |
 *                            |                                 |
 *                            +---------------------------------+ 0xFB000000
 *                            |   Cur. Page Table (Kern, RW)    | RW/-- PTSIZE
 *     VPT -----------------> +---------------------------------+ 0xFAC00000
 *                            |        Invalid Memory (*)       | --/--
 *     KERNTOP -------------> +---------------------------------+ 0xF8000000
 *                            |                                 |
 *                            |    Remapped Physical Memory     | RW/-- KMEMSIZE
 *                            |                                 |
 *     KERNBASE ------------> +---------------------------------+ 0xC0000000
 *                            |        Invalid Memory (*)       | --/--
 *     USERTOP -------------> +---------------------------------+ 0xB0000000
 *                            |           User stack            |
 *                            +---------------------------------+
 *                            |                                 |
 *                            :                                 :
 *                            |         ~~~~~~~~~~~~~~~~        |
 *                            :                                 :
 *                            |                                 |
 *                            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 *                            |       User Program & Heap       |
 *     UTEXT ---------------> +---------------------------------+ 0x00800000
 *                            |        Invalid Memory (*)       | --/--
 *                            |  - - - - - - - - - - - - - - -  |
 *                            |    User STAB Data (optional)    |
 *     USERBASE, USTAB------> +---------------------------------+ 0x00200000
 *                            |        Invalid Memory (*)       | --/--
 *     0 -------------------> +---------------------------------+ 0x00000000

这样子就知道为啥要这样赋值了。

至于最后的tf->tf_eflags = FL_IF主要是打开中断。

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

如题,这个工作的完整由do_fork函数完成,具体是调用copy_range 函数,而这里我们的任务就是补全这个函数。

这个具体的调用过程是由do_fork函数调用copy_mm函数,然后copy_mm函数调用dup_mmap函数,最后由这个dup_mmap函数调用copy_range函数。

do_fork()---->copy_mm()---->dup_mmap()---->copy_range()

这里我们需要填写以下部分:

int copy_range(pde_t *to, pde_t *from, uintptr_t start, uintptr_t end, bool share) {
    ......
    ......
    void * kva_src = page2kva(page);//返回父进程的内核虚拟页地址
    void * kva_dst = page2kva(npage);//返回子进程的内核虚拟页地址
    memcpy(kva_dst, kva_src, PGSIZE);//复制父进程到子进程
    ret = page_insert(to, npage, start, perm);//建立子进程页地址起始位置与物理地址的映射关系(prem是权限)
    ......
    ......
}

这里就是调用一个memcpy将父进程的内存直接复制给子进程即可。

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

我们逐个进行分析

fork

首先当程序执行fork时,fork使用了系统调用SYS_fork,而系统调用SYS_fork则主要是由do_forkwakeup_proc来完成的。do_fork()完成的工作在lab4的时候已经做过详细介绍,这里再简单说一下,主要是完成了以下工作:

  • 1、分配并初始化进程控制块(alloc_proc 函数);
  • 2、分配并初始化内核栈(setup_stack 函数);
  • 3、根据 clone_flag标志复制或共享进程内存管理结构(copy_mm 函数);
  • 4、设置进程在内核(将来也包括用户态)正常运行和调度所需的中断帧和执行上下文(copy_thread 函数);
  • 5、把设置好的进程控制块放入hash_listproc_list 两个全局进程链表中;
  • 6、自此,进程已经准备好执行了,把进程状态设置为“就绪”态;
  • 7、设置返回码为子进程的 id 号。

wakeup_proc函数主要是将进程的状态设置为等待,即proc->wait_state = 0,此处不赘述。

exec

当应用程序执行的时候,会调用SYS_exec系统调用,而当ucore收到此系统调用的时候,则会使用do_execve()函数来实现,因此这里我们主要介绍do_execve()函数的功能,函数主要时完成用户进程的创建工作,同时使用户进程进入执行。

主要工作如下:

  • 1、首先为加载新的执行码做好用户态内存空间清空准备。如果mm不为NULL,则设置页表为内核空间页表,且进一步判断mm的引用计数减1后是否为0,如果为0,则表明没有进程再需要此进程所占用的内存空间,为此将根据mm中的记录,释放进程所占用户空间内存和进程页表本身所占空间。最后把当前进程的mm内存管理指针为空。
  • 2、接下来是加载应用程序执行码到当前进程的新创建的用户态虚拟空间中。之后就是调用load_icode从而使之准备好执行。(具体load_icode的功能在练习1已经介绍的很详细了,这里不赘述了)

wait

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

  • 1、 如果 pid!=0,表示只找一个进程 id 号为 pid 的退出状态的子进程,否则找任意一个处于退出状态的子进程;
  • 2、 如果此子进程的执行状态不为PROC_ZOMBIE,表明此子进程还没有退出,则当前进程设置执行状态为PROC_SLEEPING(睡眠),睡眠原因为WT_CHILD(即等待子进程退出),调用schedule()函数选择新的进程执行,自己睡眠等待,如果被唤醒,则重复跳回步骤 1 处执行;
  • 3、 如果此子进程的执行状态为 PROC_ZOMBIE,表明此子进程处于退出状态,需要当前进程(即子进程的父进程)完成对子进程的最终回收工作,即首先把子进程控制块从两个进程队列proc_listhash_list中删除,并释放子进程的内核堆栈和进程控制块。自此,子进程才彻底地结束了它的执行过程,它所占用的所有资源均已释放。

exit

当执行exit功能的时候,会调用系统调用SYS_exit,而该系统调用的功能主要是由do_exit函数实现。具体过程如下:

  • 1、先判断是否是用户进程,如果是,则开始回收此用户进程所占用的用户态虚拟内存空间;(具体的回收过程不作详细说明)
  • 2、设置当前进程的中hi性状态为PROC_ZOMBIE,然后设置当前进程的退出码为error_code。表明此时这个进程已经无法再被调度了,只能等待父进程来完成最后的回收工作(主要是回收该子进程的内核栈、进程控制块)
  • 3、如果当前父进程已经处于等待子进程的状态,即父进程的wait_state被置为WT_CHILD,则此时就可以唤醒父进程,让父进程来帮子进程完成最后的资源回收工作。
  • 4、如果当前进程还有子进程,则需要把这些子进程的父进程指针设置为内核线程init,且各个子进程指针需要插入到init的子进程链表中。如果某个子进程的执行状态是 PROC_ZOMBIE,则需要唤醒 init来完成对此子进程的最后回收工作。
  • 5、执行schedule()调度函数,选择新的进程执行。

所以说该函数的功能简单的说就是,回收当前进程所占的大部分内存资源,并通知父进程完成最后的回收工作。

关于系统调用

首先罗列下目前ucore所有的系统调用如下表:

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

一般来说,用户进程只能执行一般的指令,无法执行特权指令。采用系统调用机制为用户进程提供一个获得操作系统服务的统一接口层,简化用户进程的实现。

根据之前的分析,应用程序调用的 exit/fork/wait/getpid 等库函数最终都会调用 syscall 函数,只是调用的参数不同而已(分别是 SYS_exit / SYS_fork / SYS_wait / SYS_getid )

当应用程序调用系统函数时,一般执行INT T_SYSCALL指令后,CPU 根据操作系统建立的系统调用中断描述符,转入内核态,然后开始了操作系统系统调用的执行过程,在内核函数执行之前,会保留软件执行系统调用前的执行现场,然后保存当前进程的tf结构体中,之后操作系统就可以开始完成具体的系统调用服务,完成服务后,调用IRET返回用户态,并恢复现场。这样整个系统调用就执行完毕了。

实验结果

在lab5目录下运行make qemu,得到如下结果:

说明实验成功。

时间: 2024-10-25 12:05:10

操作系统ucore lab5实验报告的相关文章

操作系统ucore lab1实验报告

操作系统lab1实验报告 [练习1] 理解通过 make 生成执行文件的过程.(要求在报告中写出对下述问题的回答) 在此练习中,大家需要通过阅读代码来了解: 1. 操作系统镜像文件 ucore.img 是如何一步一步生成的?(需要比较详细地解释 Makefile 中 每一条相关命令和命令参数的含义,以及说明命令导致的结果) 2. 一个被系统认为是符合规范的硬盘主引导扇区的特征是什么? [练习1.1] 1.生成ucore.img需要kernel和bootblock 生成ucore.img的代码如下

操作系统ucore lab7实验报告

操作系统lab7实验报告 lab6完成了用户进程的调度框架和调度算法的具体实现,即到lab6位置,ucore系统已经可以同事调度运行多个程序.但是这又引来了一个新的问题,那就是当多个同时运行的进程要协同操作或是访问共享内存的时候,如何解决同步和有序竞争的问题. 本次实验的主要就是解决进程的同步问题 练习0:填写已有实验 同样使用一款名为meld的软件进行对比即可,大致截图如下: 这里把需要填充的文件罗列如下: proc.c default_pmm.c pmm.c swap_fifo.c vmm.

操作系统ucore lab2实验报告

操作系统lab2实验报告 实验二主要是完成Ucore操作系统的物理内存管理. 主要包括了解如何建立对物理内存的初步管理,即了解连续物理内存管理;最后了解页表相关的操作,即如何建立页表来实现虚拟内存到物理内存之间的映射,对段页式内存管理机制有一个比较全面的了解. 练习0:填写已有实验 lab2会依赖lab1,需要把做的lab1的代码填到lab2中缺失的位置上面.这道题就是一个工具的利用.这里我使用的是linux下的一个叫做meld的工具.如下图: 直接将两个文件夹拖入,然后点击compare就行了

操作系统ucore lab4实验报告

操作系统lab4实验报告 本次实验将接触的是内核线程的管理.内核线程是一种特殊的进程,内核线程与用户进程的区别有两个:内核线程只运行在内核态而用户进程会在在用户态和内核态交替运行:所有内核线程直接使用共同的ucore内核内存空间,不需为每个内核线程维护单独的内存空间而用户进程需要维护各自的用户内存空间. 在本次实验完成之后,为了加深理解,我这里简单将之前的所有代码又重新阅读并梳理了一遍,简单作了下总结. 这里主要是从kern_init函数的物理内存管理初始化开始的,截图如下: 按照函数的次序我进

操作系统ucore lab8实验报告

操作系统lab8实验报告 本次实验涉及的是文件系统,通过分析了解 ucore 文件系统的总体架构设计,完善读写文件操作,从新实现基于文件系统的执行程序机制(即改写do_execve),从而可以完成执行存储在磁盘上的文件和实现文件读写等功能.可以看到,在kern_init函数中,多了一个fs_init 函数的调用.fs_init 函数就是文件系统初始化的总控函数,它进一步调用了虚拟文件系统初始化函数 vfs_init,与文件相关的设备初始化函数 dev_init 和 Simple FS 文件系统的

操作系统ucore lab6实验报告

操作系统lab6实验报告 实验五完成了用户进程的管理,可在用户态运行多个进程.可是目前的进程调度策略是FIFO策略,而本实验则会实现Stride Scheduling调度算法. 练习0:填写已有实验 同样使用一款名为meld的软件进行对比即可,大致截图如下: 现在将需要修改的文件罗列如下: proc.c default_pmm.c pmm.c swap_fifo.c vmm.c trap.c 然后是一些需要简单修改的部分,根据注释的提示,主要是一下两个函数需要额外加以修改. alloc_proc

《ucore lab5》实验报告

资源 ucore在线实验指导书 我的ucore实验代码 练习1: 加载应用程序并执行(需要编码) 题目 do_execv函数调用load_icode(位于kern/process/proc.c中) 来加载并解析一个处于内存中的ELF执行文件格式的应用程序,建立相应的用户内存空间来放置应用程序的代码段.数据段等,且要设置好proc_struct结构中的成员变量trapframe中的内容,确保在执行此进程后,能够从应用程序设定的起始执行地址开始执行.需设置正确的trapframe内容. 请在实验报告

操作系统实验报告二

  操作系统实验报告二 姓名:许恺 学号:2014011329 日期:10月14日 题目1:编写线程池 关键代码如下: 1.Thread.h #pragma once #ifndef __THREAD_H #define __THREAD_H #include <vector> #include <string> #include <pthread.h> #pragma comment(lib,"x86/pthreadVC2.lib") using

[操作系统实验lab4]实验报告

昨天跟老师建议了OS实验改革的事情,感觉助教老师给的指导书挺坑哈,代码注释也不全.我也算沦落到看别人家的源码了... 我参考的源码注释是:https://github.com/benwei/MIT-JOS/ 这个源码质量暂且不评价,但这个注释质量真心不错!!!良心注释啊!!! 本不想去找源码注释啥来看的,毕竟可能一不小心就抄袭了源码的思想?不知道HT和WLM是怎么写的,他们做的都好快啊=.=难道只有我一个人做OS实验的周期是1~2周吗... 哎,不吐槽了,这篇文章留着慢慢更,不着急.感觉在鸣神的