操作系统ucore lab4实验报告

Posted Bendawang

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了操作系统ucore lab4实验报告相关的知识,希望对你有一定的参考价值。

操作系统lab4实验报告

本次实验将接触的是内核线程的管理。内核线程是一种特殊的进程,内核线程与用户进程的区别有两个:内核线程只运行在内核态而用户进程会在在用户态和内核态交替运行;所有内核线程直接使用共同的ucore内核内存空间,不需为每个内核线程维护单独的内存空间而用户进程需要维护各自的用户内存空间。

在本次实验完成之后,为了加深理解,我这里简单将之前的所有代码又重新阅读并梳理了一遍,简单作了下总结。

这里主要是从kern_init函数的物理内存管理初始化开始的,截图如下:

技术分享

按照函数的次序我进行了简单的总结如下:

  • 1、pmm_init()
    • (1) 初始化物理内存管理器。
    • (2) 初始化空闲页,主要是初始化物理页的 Page 数据结构,以及建立页目录表和页表。
    • (3) 初始化 boot_cr3 使之指向了 ucore 内核虚拟空间的页目录表首地址,即一级页表的起始物理地址。
    • (4) 初始化第一个页表 boot_pgdir。
    • (5) 初始化了GDT,即全局描述符表。
  • 2、pic_init()
    • 初始化8259A中断控制器
  • 3、idt_init()
    • 初始化IDT,即中断描述符表
  • 4、vmm_init()
    • 主要就是实验了一个 do_pgfault()函数达到页错误异常处理功能,以及虚拟内存相关的 mm,vma 结构数据的创建/销毁/查找/插入等函数
  • 5、proc_init()
    • 这个函数启动了创建内核线程的步骤,完成了 idleproc 内核线程和 initproc 内核线程的创建或复制工作,这是本次实验分析的重点,后面将详细分析。
  • 6、ide_init()
    • 完成对用于页换入换出的硬盘(简称 swap 硬盘)的初始化工作
  • 7、swap_init()
    • swap_init() 函数首先建立完成页面替换过程的主要功能模块,即 swap_manager ,其中包含了页面置换算法的实现

练习0 填写已有实验

同样我们运用meld软件进行比较。大致截图如下:

技术分享

经过比较和修改,我将我所需要修改的文件罗列如下:

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

练习1 分配并初始化一个进程控制块

操作系统是以进程为中心设计的,所以其首要任务是为进程建立档案,进程档案用于表示、标识或描述进程,即进程控制块。这里需要完成的就是一个进程控制块的初始化。

而这里我们分配的是一个内核线程的PCB,它通常只是内核中的一小段代码或者函数,没有用户空间。而由于在操作系统启动后,已经对整个核心内存空间进行了管理,通过设置页表建立了核心虚拟空间(即boot_cr3指向的二级页表描述的空间)。所以内核中的所有线程都不需要再建立各自的页表,只需共享这个核心虚拟空间就可以访问整个物理内存了。

首先在kern/process/proc.h中定义了PCB即进程控制块的结构体proc_struct,如下:

struct proc_struct {
    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
    list_entry_t list_link;                     // Process link list 
    list_entry_t hash_link;                     // Process hash list
};

这里简单介绍下各个参数:

  • state:进程所处的状态。

    PROC_UNINIT // 未初始状态
    PROC_SLEEPING // 睡眠(阻塞)状态
    PROC_RUNNABLE // 运行与就绪态
    PROC_ZOMBIE // 僵死状态

  • pid:进程id号。

  • kstack:记录了分配给该进程/线程的内核桟的位置。
  • need_resched:是否需要调度
  • parent:用户进程的父进程。
  • mm:即实验三中的描述进程虚拟内存的结构体
  • context:进程的上下文,用于进程切换。
  • tf:中断帧的指针,总是指向内核栈的某个位置。中断帧记录了进程在被中断前的状态。
  • cr3:记录了当前使用的页表的地址

而这里要求我们完成一个alloc_proc函数来负责分配一个新的struct proc_struct结构,根据提示我们需要初始化一些变量,具体的代码如下:

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;             //未初始化的的进程id为-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);//进程名
    }
    return proc;
}

第一条设置了进程的状态为“初始”态,这表示进程已经“出生”了;
第二条语句设置了进程的pid为-1,这表示进程的“身份证号”还没有办好;
第三条语句表明由于该内核线程在内核中运行,故采用为ucore内核已经建立的页表,即设置为在 ucore 内核页表的起始地址 boot_cr3

练习2 为新创建的内核线程分配资源

alloc_proc实质只是找到了一小块内存用以记录进程的必要信息,并没有实际分配这些资源,而练习2完成的do_fork才是真正完成了资源分配的工作,当然,do_fork也只是创建当前内核线程的一个副本,它们的执行上下文、代码、数据都一样,但是存储位置不同。
根据提示及阅读源码可知,它完成的工作主要如下:

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

补全后的代码如下:详细注释见代码中

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;
    //1:调用alloc_proc()函数申请内存块,如果失败,直接返回处理
    if ((proc = alloc_proc()) == NULL) {
        goto fork_out;
    }
    //2.将子进程的父节点设置为当前进程
    proc->parent = current;
    //3.调用setup_stack()函数为进程分配一个内核栈
    if (setup_kstack(proc) != 0) {
        goto bad_fork_cleanup_proc;
    }
    //4.调用copy_mm()函数复制父进程的内存信息到子进程
    if (copy_mm(clone_flags, proc) != 0) {
        goto bad_fork_cleanup_kstack;
    }
    //5.调用copy_thread()函数复制父进程的中断帧和上下文信息
    copy_thread(proc, stack, tf);
    //6.将新进程添加到进程的hash列表中
    bool intr_flag;
    local_intr_save(intr_flag);
    {
        proc->pid = get_pid();
        hash_proc(proc); //建立映射
        nr_process ++;  //进程数加1
        list_add(&proc_list, &(proc->list_link));//将进程加入到进程的链表中
    }
    local_intr_restore(intr_flag);
    //      7.一切就绪,唤醒子进程
    wakeup_proc(proc);
    //      8.返回子进程的pid
    ret = proc->pid;
fork_out:
    return ret;

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

练习3 理解proc_run和它调用的函数如何完成进程切换的

这里我从 proc_init() 函数开始说起的。由于之前的 proc_init() 函数已经完成了 idleproc 内核线程和 initproc 内核线程的初始化。所以在 kern_init() 最后,它通过 cpu_idle() 唤醒了0号 idle 进程,在分析 proc_run 函数之前,我们先分析调度函数 schedule() 。
schedule()代码如下:

void
schedule(void) {
    bool intr_flag;
    list_entry_t *le, *last;
    struct proc_struct *next = NULL;
    local_intr_save(intr_flag);
    {
        current->need_resched = 0;
        last = (current == idleproc) ? &proc_list : &(current->list_link);
        le = last;
        do {
            if ((le = list_next(le)) != &proc_list) {
                next = le2proc(le, list_link);
                if (next->state == PROC_RUNNABLE) {
                    break;
                }
            }
        } while (le != last);
        if (next == NULL || next->state != PROC_RUNNABLE) {
            next = idleproc;
        }
        next->runs ++;
        if (next != current) {
            proc_run(next);
        }
    }
    local_intr_restore(intr_flag);
}

很容易阅读到它的代码逻辑,它是一个 FIFO 调度器,执行过程如下:

  • 1、设置当前内核线程 current->need_resched 为 0;
  • 2、在 proc_list 队列中查找下一个处于就绪态的线程或进程 next;
  • 3、找到这样的进程后,就调用 proc_run 函数,保存当前进程 current 的执行现场(进程上下文),恢复新进程的执行现场,完成进程切换。

schedule 函数通过查找 proc_list 进程队列,在这里只能找到一个处于就绪态的 initproc 内核线程。于是通过 proc_run和进一步的 switch_to 函数完成两个执行现场的切换。

好,现在进入到重点的proc_run函数,代码如下:

void proc_run(struct proc_struct *proc) {
    if (proc != current) {
        bool intr_flag;
        struct proc_struct *prev = current, *next = proc;
        local_intr_save(intr_flag);
        {
            current = proc;
            load_esp0(next->kstack + KSTACKSIZE);
            lcr3(next->cr3);
            switch_to(&(prev->context), &(next->context));
        }
        local_intr_restore(intr_flag);
    }
}

那么我们来分析分析这个代码:

  • 1、让 current 指向 next 内核线程 initproc;
  • 2、设置任务状态段 ts 中特权态 0 下的栈顶指针 esp0 为 next 内核线程 initproc 的内核栈的栈顶,即 next->kstack + KSTACKSIZE ;
  • 3、设置 CR3 寄存器的值为 next 内核线程 initproc 的页目录表起始地址 next->cr3,这实际上是完成进程间的页表切换;
  • 4、由 switch_to函数完成具体的两个线程的执行现场切换,即切换各个寄存器,当 switch_to 函数执行完“ret”指令后,就切换到 initproc 执行了。

接下来我们再来进一步分析一下这个switch_to函数,主要代码如下:

switch_to:                      # switch_to(from, to)
    # save from‘s registers
    movl 4(%esp), %eax          # eax points to from
    popl 0(%eax)                # save eip !popl
    movl %esp, 4(%eax)
    movl %ebx, 8(%eax)
    movl %ecx, 12(%eax)
    movl %edx, 16(%eax)
    movl %esi, 20(%eax)
    movl %edi, 24(%eax)
    movl %ebp, 28(%eax)

    # restore to‘s registers
    movl 4(%esp), %eax          # not 8(%esp): popped return address already
                                # eax now points to to
    movl 28(%eax), %ebp
    movl 24(%eax), %edi
    movl 20(%eax), %esi
    movl 16(%eax), %edx
    movl 12(%eax), %ecx
    movl 8(%eax), %ebx
    movl 4(%eax), %esp
    pushl 0(%eax)               # push eip
    ret

首先,保存前一个进程的执行现场,即movl 4(%esp), %eaxpopl 0(%eax)两行代码。
然后接下来的七条指令如下:

    movl %esp, 4(%eax)
    movl %ebx, 8(%eax)
    movl %ecx, 12(%eax)
    movl %edx, 16(%eax)
    movl %esi, 20(%eax)
    movl %edi, 24(%eax)
    movl %ebp, 28(%eax)

这些指令完成了保存前一个进程的其他 7 个寄存器到 context 中的相应域中。至此前一个进程的执行现场保存完毕。

再往后是恢复向一个进程的执行现场,这其实就是上述保存过程的逆执行过程,即从 context 的高地址的域 ebp 开始,逐一把相关域的值赋值给对应的寄存器。

最后的pushl 0(%eax)其实把 context 中保存的下一个进程要执行的指令地址 context.eip 放到了堆栈顶,这样接下来执行最后一条指令“ret”时,会把栈顶的内容赋值给 EIP 寄存器,这样就切换到下一个进程执行了,即当前进程已经是下一个进程了,从而完成了进程的切换。

















以上是关于操作系统ucore lab4实验报告的主要内容,如果未能解决你的问题,请参考以下文章

操作系统实验ucore lab4

操作系统实验ucore lab4

ucore实验

操作系统ucore lab5实验报告

操作系统ucore lab2实验报告

操作系统ucore lab7实验报告