作業系統實驗四實驗報告_作業系統

來源:互聯網
上載者:User
實驗四:核心線程管理 練習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 context和struct trapframe中有很多寄存器是一樣的,前者用於進程環境切換,後者用於中斷環境切換。注意這兩者的含義是不一樣的,在本實驗中一個進程開始執行需要系統進行初始化,此時tf被用來儲存中斷幀,而進程執行時是通過context來完成切換的,詳細見練習3的說明。

結構中一些重要的成員變數說明如下 mm記憶體管理資訊,包括記憶體映射列表、頁表指標等 state進程所處的狀態,有PROC_UNINIT、PROC_SLEEPING、PROC_RUNNABLE、PROC_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_list和proc_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_idle和schedule進行進程調度

// 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.

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.