操作系统实验四实验报告

来源:互联网 发布:mac版飞秋 下载 编辑:程序博客网 时间:2024/04/29 03:29

实验四:内核线程管理


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

首先来看几个比较重要的数据结构,kern/process/proc.h中定义的进程控制块及kern/trap/trap.h中定义的中断帧

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}struct trapframe {    /* below here is a struct of general registers and defined by software */    // 当中断异常发生时,此处结构内的通用寄存器信息由软件负责压栈保存    struct pushregs tf_regs;                        /* below here are segement registers and defined by software */    // 当中断异常发生时,此处的段寄存器信息由软件负责压栈保存    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, defined by hardware */    // 仅发生特权级改变时,此处额外的信息由硬件压栈保存    uintptr_t tf_esp;    uint16_t tf_ss;                                 uint16_t tf_padding5;}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;}

这里可以看到struct contextstruct trapframe中有很多寄存器是一样的,前者用于进程上下文切换,后者用于中断上下文切换。注意这两者的含义是不一样的,在本实验中一个进程开始执行需要系统进行初始化,此时tf被用来保存中断帧,而进程执行时是通过context来完成切换的,详细见练习3的说明。

结构中一些重要的成员变量说明如下

  • mm内存管理信息,包括内存映射列表、页表指针等
  • state进程所处的状态,有PROC_UNINITPROC_SLEEPINGPROC_RUNNABLEPROC_ZOMBIE四种,定义在enum proc_state
  • parent父进程,在所有进程中只有内核创建的第一个内核线程idleproc没有父进程,内核根据父子关系建立树形结构维护一些特殊的操作
  • context进程的上下文,保存寄存器,用于进程切换
  • tf中断帧,总是指向内核栈的某个位置,当进程发生中断异常,从用户跳到内核时,中断帧记录了进程在中断前的状态,由于允许中断嵌套,因此ucore在内核栈上维护了tf链,保证tf总是能够指向当前的trapframe
  • kstack每个线程都有内核栈,并且位于内核地址空间的不同位置,对于内核线程,该栈就是运行时程序使用的栈,对于普通进程,该栈就是发生特权级改变时需保存被打断硬件信息的栈

另外为了管理系统中所有的进程控制块,ucore维护了一些重要的全局变量

  • static struct proc *current当前占用处理机并处于运行状态的进程控制块指针
  • static list_entry_t hash_list[HASH_LIST_SIZE]所有进程控制块的哈希表,hash_link将基于pid链接入这个哈希表

根据实验手册及代码的提示,练习1的代码如下

把proc进行初步初始化(即把proc_struct中的各个成员变量清零)。但有些成员变量设置了特殊的值

// alloc_proc - alloc a proc_struct and init all fields of proc_structstatic 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;                            // 设置进程pid的未初始化值        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 + 1);    }    return proc;}

之后proc_init会进一步初始化idleproc内核线程


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

do_fork是创建线程的主要函数,kernel_thread通过调用do_fork函数来完成内核线程的创建工作,练习2中要求完成的do_fork函数主要完成了6项工作

  • 分配并初始化进程控制块(alloc_proc
  • 分配并初始化内核栈(setup_stack
  • 根据clone_flag标志复制或共享内存管理结构(copy_mm
  • 设置进程在内核正常运行和调度所需要的中断帧和上下文(copy_thread
  • 把设置好的进程控制块放入hash_listproc_list两个全局进程链表中
  • 将进程状态设置为“就绪”态
/* do_fork -     parent process for a new child process * @clone_flags: used to guide how to clone the child process * @stack:       the parent's user stack pointer. if stack==0, It means to fork a kernel thread. * @tf:          the trapframe info, which will be copied to child process's proc->tf */intdo_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     *   wakeup_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 wakeup_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;                              // 分配失败,直接返回    }    if(setup_kstack(proc) != 0){        goto bad_fork_cleanup_kstack;               // 堆栈初始化失败,释放已占用的空间并返回    }    if(copy_mm(clone_flags, proc) != 0){        goto bad_fork_cleanup_proc;                 // 复制或共享内存管理结构失败,释放已占用的空间并返回    }    copy_thread(proc, stack, tf);                   // 复制中断帧和上下文    proc->pid = get_pid();                          // 分配pid    hash_proc(proc);                                // 将新进程加入哈希表    list_add(&proc_list, &(proc->list_link));    nr_process ++;    wakeup_proc(proc);                              // 唤醒进程    ret = proc->pid;                                // 返回进程pidfork_out:    return ret;bad_fork_cleanup_kstack:    put_kstack(proc);bad_fork_cleanup_proc:    kfree(proc);    goto fork_out;}

参考答案中有更多的考虑,不同部分及说明如下

    proc->parent = current;                         // 设置父进程为当前的进程    bool intr_flag;                                 // 由于要操作全局数据结构,而ucore允许中断嵌套,为了避免出现线程安全问题,这里需要利用到kern/sync/sync.h中的锁,至于线程安全问题可以参考相关的博文    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);

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

参考piazza上同学的单步调式

https://piazza.com/class/i5j09fnsl7k5x0?cid=331

1、kern_init调用了proc_init函数,后者启动了创建内核线程的步骤

  • idleproc = alloc_proc()通过kmalloc函数获得了proc_struct作为第0个进程的控制块,并初始化
  • proc_init函数对idleproc内核线程进行进一步初始化
idleproc->pid = 0;                                // 设置pid=0,即第0个内核线程idleproc->state = PROC_RUNNABLE;                  // 设置状态为可运行,等待处理机调度执行该进程idleproc->kstack = (uintptr_t)bootstack;          // 直接将ucore启动时的内核栈作为改线程的内核栈(以后其他线程的内核栈需要分配获得)idleproc->need_resched = 1;                       // 设置为可以被调度,根据cpu_idle函数,只要idleproc在执行且可以被调度,就立刻执行schedule函数切换到其他有用进程执行set_proc_name(idleproc, "idle");                  // 线程命名为idlenr_process ++;                                    // 线程数+1current = idleproc;                               // 设置当前线程为idleproc
  • 调用pid = kernel_thread(init_main, "Hello world!!", 0)创建一个内核线程init_main

2、kernel_thread函数创建了内核线程的临时中断帧,并调用do_fork函数来进行进一步产生新的内核线程

// kernel_thread - create a kernel thread using "fn" function// NOTE: the contents of temp trapframe tf will be copied to //       proc->tf in do_fork-->copy_thread functionintkernel_thread(int (*fn)(void *), void *arg, uint32_t clone_flags) {    struct trapframe tf;                            // 临时变量tf来保存中断帧,并传递给do_fork    memset(&tf, 0, sizeof(struct trapframe));       // tf清零初始化    tf.tf_cs = KERNEL_CS;                           // 设置代码段为内核代码段KERNEL_CS    tf.tf_ds = tf.tf_es = tf.tf_ss = KERNEL_DS;     // 设置数据段为内核数据段KERNEL_DS    tf.tf_regs.reg_ebx = (uint32_t)fn;    tf.tf_regs.reg_edx = (uint32_t)arg;    tf.tf_eip = (uint32_t)kernel_thread_entry;      // 设置入口为函数kernel_thread_entry(定义在kern/process/entry.S中),该函数主要为函数fn做准备    return do_fork(clone_flags | CLONE_VM, 0, &tf); // 调用do_fork进一步完成创建工作,第二个参数为0代表创建的是内核线程}
kernel_thread_entry:        # void kernel_thread(void)    pushl %edx              # push arg 将fn的参数压栈    call *%ebx              # call fn  调用fn    pushl %eax              # save the return value of fn(arg) 将fn的返回值压栈    call do_exit            # call do_exit to terminate current thread 退出线程

3、do_fork完成的工作在练习2中已充分说明,这里详细说明最重要的copy_thread函数

// copy_thread - setup the trapframe on the  process's kernel stack top and//             - setup the kernel entry point and stack of processstatic voidcopy_thread(struct proc_struct *proc, uintptr_t esp, struct trapframe *tf) {    proc->tf = (struct trapframe *)(proc->kstack + KSTACKSIZE) - 1;  // proc->kstack指向给进程分配的KSTACKPAGE大小的空间,加上大小之后就指向最高地址,然后强制类型转换后-1即在内存顶部空出一块trap frame大小的空间,用于复制传进来的临时帧    *(proc->tf) = *tf;                                               // 复制临时帧    proc->tf->tf_regs.reg_eax = 0;    proc->tf->tf_esp = esp;    proc->tf->tf_eflags |= FL_IF;    proc->context.eip = (uintptr_t)forkret;                          // 设置指令寄存器为forkret函数,则进入子进程后就会调用forkret    proc->context.esp = (uintptr_t)(proc->tf);                       // 设置栈顶为内核栈存放了tf后的最大地址,即子进程栈顶从tf下面开始}

随后到do_fork完成所有工作,返回到kernel_thread再返回到proc_init再返回到kern_init

4、此时在kern_init后续有cpu_idleschedule进行进程调度

// cpu_idle - at the end of kern_init, the first kernel thread idleproc will do below worksvoidcpu_idle(void) {    while (1) {        if (current->need_resched) {                                 // 显然此时current = idleproc且已被设置为需要被调度,故进入schedule函数调度            schedule();        }    }}

5、schedule将返回之前创建的进程,并调用proc_run进行运行

// proc_run - make process "proc" running on cpu// NOTE: before call switch_to, should load  base addr of "proc"'s new PDTvoidproc_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);                    // 修改TSS任务状态栈,将TSS的ts_esp0指向下一个进程的堆栈空间,参数值参考3中的注释            lcr3(next->cr3);                                         // 修改页表基址            switch_to(&(prev->context), &(next->context));           // 切换,函数定义在kern\process\switch.S中        }        local_intr_restore(intr_flag);    }}

6、proc_run完成一些准备工作后,调用switch_to最终完成切换

高地址--------------------|arg1:next->context|--------------------|arg0:prev->context|--------------------|return address    |    --->esp--------------------switch_to:                      # switch_to(from, to)    # save from's registers    movl 4(%esp), %eax          # eax points to from [esp+4]即prev->context,将指向prev->context的指针放入eax中    popl 0(%eax)                # save eip !popl 按照context的顺序保存原进程的各寄存器    movl %esp, 4(%eax)          # save esp::context of from    movl %ebx, 8(%eax)          # save ebx::context of from    movl %ecx, 12(%eax)         # save ecx::context of from    movl %edx, 16(%eax)         # save edx::context of from    movl %esi, 20(%eax)         # save esi::context of from    movl %edi, 24(%eax)         # save edi::context of from    movl %ebp, 28(%eax)         # save ebp::context of from高地址--------------------|arg1:next->context|--------------------|arg0:prev->context|   --->esp--------------------    # restore to's registers    movl 4(%esp), %eax          # not 8(%esp): popped return address already  由于前面已经popl,故这里不是8而是[esp+4]即next->context放入eax中                                # eax now points to to 按照context的顺序获取新进程的各寄存器    movl 28(%eax), %ebp         # restore ebp::context of to    movl 24(%eax), %edi         # restore edi::context of to    movl 20(%eax), %esi         # restore esi::context of to    movl 16(%eax), %edx         # restore edx::context of to    movl 12(%eax), %ecx         # restore ecx::context of to    movl 8(%eax), %ebx          # restore ebx::context of to    movl 4(%eax), %esp          # restore esp::context of to    pushl 0(%eax)               # push eip 前面将原进程的返回地址弹出,现在将next->context的eip压入栈    ret

注意,新进程就是前面创建的init_main,参考3可以知道当时proc->context.eip = (uintptr_t)forkret,当switch_to返回时,把栈顶的内容赋值给EIP寄存器,此时跳转到了forkret进行执行

7、forkret调用forkrets完成准备并最终进入init_main

// forkret -- the first kernel entry point of a new thread/process// NOTE: the addr of forkret is setted in copy_thread function//       after switch_to, the current proc will execute here.static voidforkret(void) {    forkrets(current->tf);}    # return falls through to trapret....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 forkretsforkrets:    # set stack to this new process's trapframe    movl 4(%esp), %esp                            #esp指向当前进程的中断帧即esp指向current->tf    jmp __trapret                                

注意参考3可以知道proc->context.esp = (uintptr_t)(proc->tf),而在6中switch_to最后压入了proc->context.eip,故在forkrets中[esp+4]即指向context.esp,这里就是中断帧proc->tf,参考栈的内容、struct trapframe__trapret就会理解跳转情况

switch_to之后的栈在内存中的存放:高地址-------------|context.ebp||...........| -------------|context.esp|   --->[esp+4] = proc->tf-------------|context.eip|   --->espstruct trapframe {    struct pushregs tf_regs;   // 通用寄存器,对应__trapret: popal    uint16_t tf_gs;            // 对应popl %gs    uint16_t tf_padding0;    uint16_t tf_fs;            // 对应popl %fs    uint16_t tf_padding1;    uint16_t tf_es;            // 对应popl %es    uint16_t tf_padding2;    uint16_t tf_ds;            // 对应popl %ds    uint16_t tf_padding3;    uint32_t tf_trapno;        // [esp]此时的esp指向这里    /* below here defined by x86 hardware */    uint32_t tf_err;           // [esp+4]    uintptr_t tf_eip;          // [esp+8]对应addl $0x8, %esp    uint16_t tf_cs;    ...};

iret返回时就会进入此时的esp所指向的proc->tf.tf_eip,在2中这个值被初始化为kernel_thread_entry

.text.globl kernel_thread_entrykernel_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

由此进入init_main,当返回时调用do_exit完成所有过程

完成代码后使用make qemu查看结果如下,程序正确

(THU.CST) os is loading ......省略部分输出check_alloc_page() succeeded!check_pgdir() succeeded!check_boot_pgdir() succeeded!...省略部分输出kmalloc_init() succeeded!check_vma_struct() succeeded!...省略部分输出check_pgfault() succeeded!check_vmm() succeeded....省略部分输出check_swap() succeeded!++ setup timer interruptsthis initproc, pid = 1, name = "init"To U: "Hello world!!".To U: "en.., Bye, Bye. :)"kernel panic at kern/process/proc.c:344:    process exit!!.Welcome to the kernel debug monitor!!Type 'help' for a list of commands.