zoukankan      html  css  js  c++  java
  • 《ucore lab4》实验报告

    资源

    1. ucore在线实验指导书
    2. 我的ucore实验代码

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

    题目

    alloc_proc函数(位于kern/process/proc.c中) 负责分配并返回一个新的struct proc_struct结构,用于存储新建立的内核线程的管理信息。ucore需要对这个结构进行最基本的初始化,你需要完成这个初始化过程。

    【提示】 在alloc_proc函数的实现中,需要初始化的proc_struct结构中的成员变量至少包括:state/pid/runs/kstack/need_resched/parent/mm/context/tf/cr3/flags/name。

    请在实验报告中简要说明你的设计实现过程。请回答如下问题:
    请说明proc_struct中 struct context context 和 struct trapframe *tf 成员变量含义和在本实验中的作用是啥?(提示通过看代码和编程调试可以判断出来)

    解答

    我的设计实现过程

    alloc_proc函数主要是初始化进程控制块,亦即初始化proc_struct结构体的各成员变量。

    • state:进程所处的状态。由于分配进程控制块时,进程还处于创建阶段,因此设置其状态的PROC_UNINIT,表示尚未完成初始化。
    • pid:先设置pid为无效值-1,用户调完alloc_proc函数后再根据实际情况设置pid。
    • cr3:设置为前面已经创建好的页目录表boot_pgdir的物理地址。注意是物理地址,实际编码时应写成PADDR(boot_pgdir)。
    • need_resched:标记是否需要调度其他进程。初始化为0,表示不需调度其他进程。
    • kstack:内核栈地址,先初始化为0,后续根据需要来设置
    • tf:中断帧,先初始化为NULL,后续根据需要来设置

    回答问题:context和tf的含义及作用是什么

    1. context是进程上下文,即进程执行时各寄存器的取值。用于进程切换时保存进程上下文比如本实验中,当idle进程被CPU切换出去时,可以将idle进程上下文保存在其proc_struct结构体的context成员中,这样当CPU运行完init进程,再次运行idle进程时,能够恢复现场,继续执行。
    struct context {
        uint32_t eip;
        uint32_t esp;
        uint32_t ebx;
        uint32_t ecx;
        uint32_t edx;
        uint32_t esi;
        uint32_t edi;
        uint32_t ebp;
    };
    
    1. tf是中断帧,具体定义如下。
    struct trapframe {
        struct pushregs tf_regs;
        uint16_t tf_gs;
        uint16_t tf_padding0;
        uint16_t tf_fs;
        uint16_t tf_padding1;
        uint16_t tf_es;
        uint16_t tf_padding2;
        uint16_t tf_ds;
        uint16_t tf_padding3;
        uint32_t tf_trapno;
        /* below here defined by x86 hardware */
        uint32_t tf_err;
        uintptr_t tf_eip;
        uint16_t tf_cs;
        uint16_t tf_padding4;
        uint32_t tf_eflags;
        /* below here only when crossing rings, such as from user to kernel */
        uintptr_t tf_esp;
        uint16_t tf_ss;
        uint16_t tf_padding5;
    } __attribute__((packed));
    
    1. trap_frame与context的区别是什么?

      • 从内容上看,trap_frame包含了context的信息,除此之外,trap_frame还保存有段寄存器、中断号、错误码err和状态寄存器eflags等信息。
      • 从作用时机来看,context主要用于进程切换时保存进程上下文,trap_frame主要用于发生中断或异常时保存进程状态。
      • 当进程进行系统调用或发生中断时,会发生特权级转换,这时也会切换栈,因此需要保存栈信息(包括ss和esp)到trap_frame,但不需要更新context。
    2. trap_frame与context在创建进程时所起的作用:

      • 当创建一个新进程时,我们先分配一个进程控制块proc,并设置好其中的tf及context变量;
      • 然后,当调度器schedule调度到该进程时,首先进行上下文切换,这里关键的两个上下文信息是context.eip和context.esp,前者提供新进程的起始入口,后者保存新进程的trap_frame地址。
      • 上下文切换完毕后,CPU会跳转到新进程的起始入口。在新进程的起始入口中,根据trap_frame信息设置通用寄存器和段寄存器的值,并执行真正的处理函数。可见,tf与context共同用于进程的状态保存与恢复。
      • 综上,由上下文切换到执行新进程的处理函数fn,中间经历了多次函数调用:forkret() -> forkrets(current->tf) -> __trapret -> kernel_thread_entry -> init_main.

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

    题目

    创建一个内核线程需要分配和设置好很多资源。kernel_thread函数通过调用do_fork函数完成具体内核线程的创建工作。do_kernel函数会调用alloc_proc函数来分配并初始化一个进程控制块,但alloc_proc只是找到了一小块内存用以记录进程的必要信息,并没有实际分配这些资源。ucore一般通过do_fork实际创建新的内核线程。do_fork的作用是,创建当前内核线程的一个副本,它们的执行上下文、代码、数据都一样,但是存储位置不同。在这个过程中,需要给新内核线程分配资源,并且复制原进程的状态。你需要完成在kern/process/proc.c中do_fork函数中的处理过程。它的大致执行步骤包括:
    - 调用alloc_proc,首先获得一块用户信息块。
    - 为进程分配一个内核栈。
    - 复制原进程的内存管理信息到新进程(但内核线程不必做此事)
    - 复制原进程上下文到新进程
    - 将新进程添加到进程列表
    - 唤醒新进程
    - 返回新进程号

    请在实验报告中简要说明你的设计实现过程。请回答如下问题:
    请说明ucore是否做到给每个新fork的线程一个唯一的id?请说明你的分析和理由。

    解答

    我的设计实现过程

    根据注释提供的步骤,很容易完成do_fork函数的实现。这里需要注意的是:如果前面的步骤失败,比如alloc_proc分配进程控制块失败或建立内核栈失败,那么需要释放已申请的资源。

    回答问题:ucore是否为每个新fork的线程提供唯一的pid?

    首先,本实验不提供线程释放的功能,意味着pid只分配不回收。当fork的线程总数小于MAX_PID时,每个线程的pid是唯一的。当fork的线程总数大于MAX_PID时,后面fork的线程的pid可能与前面的线程重复(暂不确定)。

    注:get_pid函数没完全看懂,next_safe的含义不理解?

    代码修改

    对照答案时,发现自己的代码有几个优化的地方:

    1. 没有设置proc->parent,应将其设置为current

    2. 由于do_fork已经设置了标签,setup_kstack执行失败后直接跳转到bad_fork_cleanup_proc即可,copy_mm失败后直接跳转到bad_fork_cleanup_kstack即可。

    3. copy_thread的第二个输入参数esp应该使用do_fork的第二个输入参数stack。

    4. 将当前进程插入到proc_list和hash_list时需要去使能中断。(为什么?)

    5. 我是将proc插入到proc_list的末尾,而答案是插入到proc_list的开头。为何?是不是因为插入到开头的话,schedule选择要执行的线程时会快些?

    我的代码:

        if (NULL == (proc = alloc_proc())) {
            goto fork_out;
        }
    
        if (0 != setup_kstack(proc)) {
            kfree(proc);
            goto fork_out;
        }
    
        if (0 != copy_mm(clone_flags, proc)) {
            kfree((void *)proc->kstack);
            kfree(proc);
            goto fork_out;
        }
    
        proc->pid = get_pid();
    
        int esp = 0;
        asm volatile ("movl %%esp, %0" : "=r" (esp));
    
        copy_thread(proc, esp, tf);
    
        list_add_before(&proc_list, &proc->list_link);
    
        hash_proc(proc);
    
        wakeup_proc(proc);
    
        nr_process++;
    

    答案的代码:

        if ((proc = alloc_proc()) == NULL) {
            goto fork_out;
        }
    
        proc->parent = current;
    
        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);
            list_add(&proc_list, &(proc->list_link));
            nr_process ++;
        }
        local_intr_restore(intr_flag);
    
        wakeup_proc(proc);
    

    练习3:阅读代码,理解 proc_run 函数和它调用的函数如何完成进程切换的。

    题目

    请在实验报告中简要说明你对proc_run函数的分析。并回答如下问题:
    - 在本实验的执行过程中,创建且运行了几个内核线程?
    - 语句 local_intr_save(intr_flag);....local_intr_restore(intr_flag); 在这里有何作用?请说明理由。

    完成代码编写后,编译并运行代码:make qemu,如果可以得到如附录A所示的显示内容(仅供参考,不是标准答案输出) ,则基本正确。

    解答

    分析proc_run函数

    1. 首先判断要切换到的进程是不是当前进程,若是则不需进行任何处理。

    2. 调用local_intr_save和local_intr_restore函数去使能中断,避免在进程切换过程中出现中断。(疑问:进程切换过程中处理中断会有什么问题?)

    3. 更新current进程为proc

    4. 更新任务状态段的esp0的值(疑问:为什么更新esp0?)

    5. 重新加载cr3寄存器,使页目录表更新为新进程的页目录表

    6. 上下文切换,把当前进程的当前各寄存器的值保存在其proc_struct结构体的context变量中,再把要切换到的进程的proc_struct结构体的context变量加载到各寄存器。

    7. 完成上下文切换后,CPU会根据eip寄存器的值找到下一条指令的地址并执行。根据copy_thread函数可知eip寄存器指向forkret函数,forkret函数的实现为forkrets(current->tf);

    8. forkrets函数的实现如下。首先是把输入变量current->tf复制给%esp,此时栈上保存了tf的值,亦即各寄存器的值。然后在trapret函数中使用popal和popl指令将栈上的内容逐一赋值给相应寄存器。最后执行iret,把栈顶的数据(也就是tf_eip、tf_cs和tf_eflags)依次赋值给eip、cs和eflags寄存器。

    .globl __trapret
    __trapret:
        # restore registers from stack
        popal
    
        # restore %ds, %es, %fs and %gs
        popl %gs
        popl %fs
        popl %es
        popl %ds
    
        # get rid of the trap number and error code
        addl $0x8, %esp
        iret
    
    .globl forkrets
    forkrets:
        # set stack to this new process's trapframe
        movl 4(%esp), %esp
        jmp __trapret
    
    1. 根据kernel_thread函数,可知tf_eip指向kernel_thread_entry,其函数实现如下所示。由于kernel_thread函数中把要执行的函数地址fn保存在ebx寄存器,把输入参数保存到edx寄存器,因此kernel_thread_entry函数先通过pushl %edx将输入参数压栈,然后通过call *%ebx调用函数fn。
    .globl kernel_thread_entry
    kernel_thread_entry:        # void kernel_thread(void)
    
        pushl %edx              # push arg
        call *%ebx              # call fn
    
        pushl %eax              # save the return value of fn(arg)
        call do_exit            # call do_exit to terminate current thread
    
    1. 根据proc_init函数,可知调用kernel_thread时,输入的fn函数即init_main,输入参数为"Hello world!!"。init_main函数的功能是打印输入字符串及其他内容,其实现如下所示。
    init_main(void *arg) {
        cprintf("this initproc, pid = %d, name = "%s"
    ", current->pid, get_proc_name(current));
        cprintf("To U: "%s".
    ", (const char *)arg);
        cprintf("To U: "en.., Bye, Bye. :)"
    ");
        return 0;
    }
    

    回答问题1:本实验创建且运行了几个内核线程

    答:本实验创建且运行了两个内核线程,分别是idle和init线程。

    回答问题2:local_intr_save和local_intr_restore的作用

    答:避免在进程切换过程中处理中断。

    扩展练习Challenge:实现支持任意大小的内存分配算法(待完成)

    这不是本实验的内容,其实是上一次实验内存的扩展,但考虑到现在的slab算法比较复杂,有必要实现一个比较简单的任意大小内存分配算法。可参考本实验中的slab如何调用基于页的内存分配算法(注意,不是要你关注slab的具体实现) 来实现first-fit/best-fit/worst-fit/buddy等支持任意大小的内存分配算法。

    【注意】 下面是相关的Linux实现文档,供参考
    - [SLOB](http://en.wikipedia.org/wiki/SLOB http://lwn.net/Articles/157944/)
    - SLAB

  • 相关阅读:
    ActiveMQ (二):JMS
    Java消息队列--ActiveMq 初体验
    利用 UltraEdit 重新排版 XML 结构数据
    Java中的Arrays工具类
    数组的下标与长度
    数组的一维与多维
    MySQL数据库的下载与安装
    MySQL数据库的发展历程
    Java中的数组(Array)
    break与continue关键字
  • 原文地址:https://www.cnblogs.com/wuhualong/p/ucore_lab4_report.html
Copyright © 2011-2022 走看看