ucoreOS_lab5 實驗報告

所有的實驗報告將會在 Github 同步更新,更多內容請移步至Github:https://github.com/AngelKitty/review_the_national_post-graduate_entrance_examination/blob/master/books_and_notes/professional_courses/operating_system/sources/ucore_os_lab/docs/lab_report/

練習0:填寫已有實驗

lab5 會依賴 lab1~lab4 ,我們需要把做的 lab1~lab4 的代碼填到 lab5 中缺失的位置上面。練習 0 就是一個工具的利用。這裏我使用的是 Linux 下的系統已預裝好的 Meld Diff Viewer 工具。和 lab4 操作流程一樣,我們只需要將已經完成的 lab1~lab4 與待完成的 lab5 (由於 lab5 是基於 lab1~lab4 基礎上完成的,所以這裏只需要導入 lab4 )分別導入進來,然後點擊 compare 就行了。

compare

然後軟件就會自動分析兩份代碼的不同,然後就一個個比較比較複製過去就行了,在軟件裏面是可以支持打開對比複製了,點擊 Copy Right 即可。當然 bin 目錄和 obj 目錄下都是 make 生成的,就不用複製了,其他需要修改的地方主要有以下七個文件,通過對比複製完成即可:

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

根據試驗要求,我們需要對部分代碼進行改進,這裏講需要改進的地方的代碼和說明羅列如下:

  • 在初始化 IDT 的時候,設置系統調用對應的中斷描述符,使其能夠在用戶態下被調用,並且設置爲 trap 類型。(事實上這個部分已經在LAB1的實驗中順手被完成了)
  • 在時鐘中斷的處理部分,每過 TICK_NUM 箇中斷,就將當前的進程設置爲可以被重新調度的,這樣使得當前的線程可以被換出,從而實現多個線程的併發執行;
  • alloc_proc 函數中,額外對進程控制塊中新增加的 wait_state, cptr, yptr, optr 成員變量進行初始化;
  • 在 do_fork 函數中,使用 set_links 函數來完成將 fork 的線程添加到線程鏈表中的過程,值得注意的是,該函數中就包括了對進程總數加 1 這一操作,因此需要將原先的這個操作給刪除掉;

alloc_proc() 函數

我們在原來的實驗基礎上,新增了 2 行代碼:

proc->wait_state = 0;//PCB 進程控制塊中新增的條目,初始化進程等待狀態  
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)

因爲這裏涉及到了用戶進程,自然需要涉及到調度的問題,所以進程等待狀態和各種指針需要被初始化。

所以改進後的 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;             //未初始化的的進程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);//進程名
        proc->wait_state = 0;//PCB 進程控制塊中新增的條目,初始化進程等待狀態  
        proc->cptr = proc->optr = proc->yptr = NULL;//進程相關指針初始化
    }
    return proc;
}

do_fork() 函數

我們在原來的實驗基礎上,新增了 2 行代碼:

assert(current->wait_state == 0); //確保當前進程正在等待
set_links(proc); //將原來簡單的計數改成來執行 set_links 函數,從而實現設置進程的相關鏈接 

第一行是爲了確定當前的進程正在等待,我們在 alloc_proc 中初始化 wait_state 爲0。第二行是將原來的計數換成了執行一個 set_links 函數,因爲要涉及到進程的調度,所以簡單的計數肯定是不行的。

我們可以看看 set_links 函數:

static void set_links(struct proc_struct *proc) {
    list_add(&proc_list,&(proc->list_link));//進程加入進程鏈表
    proc->yptr = NULL; //當前進程的 younger sibling 爲空
    if ((proc->optr = proc->parent->cptr) != NULL) {
        proc->optr->yptr = proc; //當前進程的 older sibling 爲當前進程
    }
    proc->parent->cptr = proc; //父進程的子進程爲當前進程
    nr_process ++; //進程數加一
}

可以看出,set_links 函數的作用是設置當前進程的 process relations。

所以改進後的 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) { //分配進程數大於 4096,返回
        goto fork_out; //返回
    }
    ret = -E_NO_MEM; //因內存不足而分配失敗
    if ((proc = alloc_proc()) == NULL) { //調用 alloc_proc() 函數申請內存塊,如果失敗,直接返回處理
        goto fork_out;//返回
    }

    proc->parent = current; //將子進程的父節點設置爲當前進程
    assert(current->wait_state == 0); //確保當前進程正在等待
  
    if (setup_kstack(proc) != 0) { //調用 setup_stack() 函數爲進程分配一個內核棧
        goto bad_fork_cleanup_proc; //返回
    }
    if (copy_mm(clone_flags, proc) != 0) { //調用 copy_mm() 函數複製父進程的內存信息到子進程
        goto bad_fork_cleanup_kstack; //返回
    }
    copy_thread(proc, stack, tf); //調用 copy_thread() 函數複製父進程的中斷幀和上下文信息
    //將新進程添加到進程的 hash 列表中
    bool intr_flag;
    local_intr_save(intr_flag); //屏蔽中斷,intr_flag 置爲 1
    {
        proc->pid = get_pid(); //獲取當前進程 PID
        hash_proc(proc); //建立 hash 映射
        set_links(proc);//將原來簡單的計數改成來執行set_links函數,從而實現設置進程的相關鏈接
    }
    local_intr_restore(intr_flag); //恢復中斷

    wakeup_proc(proc); //一切就緒,喚醒子進程

    ret = proc->pid; //返回子進程的 pid
fork_out: //已分配進程數大於 4096
    return ret;

bad_fork_cleanup_kstack: //分配內核棧失敗
    put_kstack(proc);
bad_fork_cleanup_proc:
    kfree(proc);
    goto fork_out;
}

idt_init() 函數

我們在原來的實驗基礎上,新增了 1 行代碼:

SETGATE(idt[T_SYSCALL], 1, GD_KTEXT, __vectors[T_SYSCALL], DPL_USER);//這裏主要是設置相應的中斷門

所以改進後的 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);
}

設置一個特定中斷號的中斷門,專門用於用戶進程訪問系統調用。在上述代碼中,可以看到在執行加載中斷描述符表 lidt 指令前,專門設置了一個特殊的中斷描述符 idt[T_SYSCALL],它的特權級設置爲 DPL_USER,中斷向量處理地址在 __vectors[T_SYSCALL] 處。這樣建立好這個中斷描述符後,一旦用戶進程執行 INT T_SYSCALL 後,由於此中斷允許用戶態進程產生(它的特權級設置爲 DPL_USER),所以 CPU 就會從用戶態切換到內核態,保存相關寄存器,並跳轉到 __vectors[T_SYSCALL] 處開始執行,形成如下執行路徑:

vector128(vectors.S)--\>
\_\_alltraps(trapentry.S)--\>trap(trap.c)--\>trap\_dispatch(trap.c)----\>syscall(syscall.c)-

syscall 中,根據系統調用號來完成不同的系統調用服務。

trap_dispatch() 函數

我們在原來的實驗基礎上,新增了 1 行代碼:

current->need_resched = 1;//時間片用完設置爲需要調度

這裏主要是將時間片設置爲需要調度,說明當前進程的時間片已經用完了。

所以改進後的 trap_dispatch 函數如下:

ticks ++;
        if (ticks % TICK_NUM == 0) {
            assert(current != NULL);
            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 函數:

// do_execve - call exit_mmap(mm)&put_pgdir(mm) to reclaim memory space of current process
//           - call load_icode to setup new memory space accroding binary prog.
int
do_execve(const char *name, size_t len, unsigned char *binary, size_t size) {
    struct mm_struct *mm = current->mm; //獲取當前進程的內存地址
    if (!user_mem_check(mm, (uintptr_t)name, len, 0)) {
        return -E_INVAL;
    }
    if (len > PROC_NAME_LEN) {
        len = PROC_NAME_LEN;
    }

    char local_name[PROC_NAME_LEN + 1];
    memset(local_name, 0, sizeof(local_name));
    memcpy(local_name, name, len);
    //爲加載新的執行碼做好用戶態內存空間清空準備 
    if (mm != NULL) {
        lcr3(boot_cr3); //設置頁表爲內核空間頁表
        if (mm_count_dec(mm) == 0) { //如果沒有進程再需要此進程所佔用的內存空間
            exit_mmap(mm); //釋放進程所佔用戶空間內存和進程頁表本身所佔空間
            put_pgdir(mm);
            mm_destroy(mm);
        }
        current->mm = NULL; //把當前進程的 mm 內存管理指針爲空
    }
    int ret;
    // 加載應用程序執行碼到當前進程的新創建的用戶態虛擬空間中。這裏涉及到讀 ELF 格式的文件,申請內存空間,建立用戶態虛存空間,加載應用程序執行碼等。load_icode 函數完成了整個複雜的工作。  
    if ((ret = load_icode(binary, size)) != 0) {
        goto execve_exit;
    }
    set_proc_name(current, local_name);
    return 0;

execve_exit:
    do_exit(ret);
    panic("already exit: %e.\n", ret);
}

而這裏這個 do_execve 函數主要做的工作就是先回收自身所佔用戶空間,然後調用 load_icode,用新的程序覆蓋內存空間,形成一個執行新程序的新進程。

do_execve

至此,用戶進程的用戶環境已經搭建完畢。此時 initproc 將按產生系統調用的函數調用路徑原路返回,執行中斷返回指令 iret 後,將切換到用戶進程程序的第一條語句位置 _start 處開始執行。

實現過程如下:

load_icode 函數分析:
該函數的功能主要分爲 6 個部分,而我們需要填寫的是第 6 個部分,就是僞造中斷返回現場,使得系統調用返回之後可以正確跳轉到需要運行的程序入口,並正常運行;而 1-5 部分則是一系列對用戶內存空間的初始化,這部分將在 LAB8 的編碼實現中具體體現,因此在本 LAB 中暫時不加具體說明;與 LAB1 的 challenge 類似的,第 6 個部分是在進行中斷處理的棧(此時應當是內核棧)上僞造一箇中斷返回現場,使得中斷返回的時候可以正確地切換到需要的執行程序入口處;在這個部分中需要對 tf 進行設置,不妨通過代碼分析來確定這個 tf 變量究竟指到什麼位置,該 tf 變量與 current->tf 的數值一致,而 current->tf 是在進行中斷服務里程的 trap 函數中被設置爲當前中斷的中斷幀,也就是說這個 tf 最終指向了當前系統調用 exec 產生的中斷幀處;
/* load_icode - load the content of binary program(ELF format) as the new content of current process
 * @binary:  the memory addr of the content of binary program
 * @size:  the size of the content of binary program
 */
static int load_icode(unsigned char *binary, size_t size) {
    if (current->mm != NULL) { //當前進程的內存爲空
        panic("load_icode: current->mm must be empty.\n");
    }

    int ret = -E_NO_MEM; //記錄錯誤信息:未分配內存
    struct mm_struct *mm;
    //(1) create a new mm for current process
    if ((mm = mm_create()) == NULL) { //分配內存
        goto bad_mm; //分配失敗,返回
    }
    //(2) create a new PDT, and mm->pgdir= kernel virtual addr of PDT
    if (setup_pgdir(mm) != 0) { //申請一個頁目錄表所需的空間
        goto bad_pgdir_cleanup_mm; //申請失敗
    }
    //(3) copy TEXT/DATA section, build BSS parts in binary to memory space of process
    struct Page *page;
    //(3.1) get the file header of the bianry program (ELF format)
    struct elfhdr *elf = (struct elfhdr *)binary;
    //(3.2) get the entry of the program section headers of the bianry program (ELF format)
    struct proghdr *ph = (struct proghdr *)(binary + elf->e_phoff); //獲取段頭部表的地址
    //(3.3) This program is valid?
    if (elf->e_magic != ELF_MAGIC) { //讀取的 ELF 文件不合法
        ret = -E_INVAL_ELF; //ELF 文件不合法錯誤
        goto bad_elf_cleanup_pgdir; //返回
    }

    uint32_t vm_flags, perm;
    struct proghdr *ph_end = ph + elf->e_phnum; //段入口數目
    for (; ph < ph_end; ph ++) { //遍歷每一個程序段
    //(3.4) find every program section headers
        if (ph->p_type != ELF_PT_LOAD) { //當前段不能被加載
            continue ;
        }
        if (ph->p_filesz > ph->p_memsz) { //虛擬地址空間大小大於分配的物理地址空間
            ret = -E_INVAL_ELF;
            goto bad_cleanup_mmap;
        }
        if (ph->p_filesz == 0) { //當前段大小爲 0
            continue ;
        }
    //(3.5) call mm_map fun to setup the new vma ( ph->p_va, ph->p_memsz)
        vm_flags = 0, perm = PTE_U;
        if (ph->p_flags & ELF_PF_X) vm_flags |= VM_EXEC;
        if (ph->p_flags & ELF_PF_W) vm_flags |= VM_WRITE;
        if (ph->p_flags & ELF_PF_R) vm_flags |= VM_READ;
        if (vm_flags & VM_WRITE) perm |= PTE_W;
        if ((ret = mm_map(mm, ph->p_va, ph->p_memsz, vm_flags, NULL)) != 0) {
            goto bad_cleanup_mmap;
        }
        unsigned char *from = binary + ph->p_offset;
        size_t off, size;
        uintptr_t start = ph->p_va, end, la = ROUNDDOWN(start, PGSIZE);

        ret = -E_NO_MEM;

     //(3.6) alloc memory, and  copy the contents of every program section (from, from+end) to process's memory (la, la+end)
        end = ph->p_va + ph->p_filesz;
     //(3.6.1) copy TEXT/DATA section of bianry program
        while (start < end) {
            if ((page = pgdir_alloc_page(mm->pgdir, la, perm)) == NULL) {
                goto bad_cleanup_mmap;
            }
            off = start - la, size = PGSIZE - off, la += PGSIZE;
            if (end < la) {
                size -= la - end;
            }
            memcpy(page2kva(page) + off, from, size);
            start += size, from += size;
        }

      //(3.6.2) build BSS section of binary program
        end = ph->p_va + ph->p_memsz;
        if (start < la) {
            /* ph->p_memsz == ph->p_filesz */
            if (start == end) {
                continue ;
            }
            off = start + PGSIZE - la, size = PGSIZE - off;
            if (end < la) {
                size -= la - end;
            }
            memset(page2kva(page) + off, 0, size);
            start += size;
            assert((end < la && start == end) || (end >= la && start == la));
        }
        while (start < end) {
            if ((page = pgdir_alloc_page(mm->pgdir, la, perm)) == NULL) {
                goto bad_cleanup_mmap;
            }
            off = start - la, size = PGSIZE - off, la += PGSIZE;
            if (end < la) {
                size -= la - end;
            }
            memset(page2kva(page) + off, 0, size);
            start += size;
        }
    }
    //(4) build user stack memory
    vm_flags = VM_READ | VM_WRITE | VM_STACK;
    if ((ret = mm_map(mm, USTACKTOP - USTACKSIZE, USTACKSIZE, vm_flags, NULL)) != 0) {
        goto bad_cleanup_mmap;
    }
    assert(pgdir_alloc_page(mm->pgdir, USTACKTOP-PGSIZE , PTE_USER) != NULL);
    assert(pgdir_alloc_page(mm->pgdir, USTACKTOP-2*PGSIZE , PTE_USER) != NULL);
    assert(pgdir_alloc_page(mm->pgdir, USTACKTOP-3*PGSIZE , PTE_USER) != NULL);
    assert(pgdir_alloc_page(mm->pgdir, USTACKTOP-4*PGSIZE , PTE_USER) != NULL);
    
    //(5) set current process's mm, sr3, and set CR3 reg = physical addr of Page Directory
    mm_count_inc(mm);
    current->mm = mm;
    current->cr3 = PADDR(mm->pgdir);
    lcr3(PADDR(mm->pgdir));

    //(6) setup trapframe for user environment
    struct trapframe *tf = current->tf;
    memset(tf, 0, sizeof(struct trapframe));
    /* 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
     */
--------------------------------------------------------------------------------------------
進程切換總是在內核態中發生,當內核選擇一個進程執行的時候,首先切換內核態的上下文(EBX、ECX、EDX、ESI、EDI、ESP、EBP、EIP 八個寄存器)以及內核棧。完成內核態切換之後,內核需要使用 IRET 指令將 trapframe 中的用戶態上下文恢復出來,返回到進程態,在用戶態中執行進程。
* 實現思路:
  1. 由於最終是在用戶態下運行的,所以需要將段寄存器初始化爲用戶態的代碼段、數據段、堆棧段;
  2. esp 應當指向先前的步驟中創建的用戶棧的棧頂;
  3. eip 應當指向 ELF 可執行文件加載到內存之後的入口處;
  4. eflags 中應當初始化爲中斷使能,注意 eflags 的第 1 位是恆爲 1 的;
  5. 設置 ret 爲 0,表示正常返回;

load_icode 函數需要填寫的部分爲:
  * 將 trapframe 的代碼段設爲 USER_CS;
  * 將 trapframe 的數據段、附加段、堆棧段設爲 USER_DS;
  * 將 trapframe 的棧頂指針設爲 USTACKTOP;
  * 將 trapframe 的代碼段指針設爲 ELF 的入口地址 elf->e_entry;
  * 將 trapframe 中 EFLAGS 的 IF 置爲 1。
--------------------------------------------------------------------------------------------
/*code*/
    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; //FL_IF爲中斷打開狀態
    ret = 0;
--------------------------------------------------------------------------------------------
out:
    return ret;
bad_cleanup_mmap:
    exit_mmap(mm);
bad_elf_cleanup_pgdir:
    put_pgdir(mm);
bad_pgdir_cleanup_mm:
    mm_destroy(mm);
bad_mm:
    goto out;
}

調用流程如下圖所示:

load_icode

關於 tf_esptf_eip 的設置,我們可以通過閱讀下圖可以得知。這是一個完整的虛擬內存空間的分佈圖:

/*
   file_path:kern/mm/memlayout.h
*/
/* *
 * Virtual memory map:                                          Permissions
 *                                                              kernel/user
 *
 *     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
 * (*) Note: The kernel ensures that "Invalid Memory" is *never* mapped.
 *     "Empty Memory" is normally unmapped, but user programs may map pages
 *     there if desired.
 *
 * */

請在實驗報告中描述當創建一個用戶態進程並加載了應用程序後,CPU 是如何讓這個應用程序最終在用戶態執行起來的。即這個用戶態進程被 ucore 選擇佔用 CPU 執行(RUNNING 態)到具體執行應用程序第一條指令的整個經過。

分析在創建了用戶態進程並且加載了應用程序之後,其佔用 CPU 執行到具體執行應用程序的整個經過:

  1. 在經過調度器佔用了 CPU 的資源之後,用戶態進程調用了 exec 系統調用,從而轉入到了系統調用的處理例程;
  2. 在經過了正常的中斷處理例程之後,最終控制權轉移到了 syscall.c 中的 syscall 函數,然後根據系統調用號轉移給了 sys_exec 函數,在該函數中調用了上文中提及的 do_execve 函數來完成指定應用程序的加載;
  3. 在do_execve中進行了若干設置,包括推出當前進程的頁表,換用 kernel 的 PDT 之後,使用 load_icode 函數,完成了對整個用戶線程內存空間的初始化,包括堆棧的設置以及將 ELF 可執行文件的加載,之後通過 current->tf 指針修改了當前系統調用的 trapframe,使得最終中斷返回的時候能夠切換到用戶態,並且同時可以正確地將控制權轉移到應用程序的入口處;
  4. 在完成了 do_exec 函數之後,進行正常的中斷返回的流程,由於中斷處理例程的棧上面的 eip 已經被修改成了應用程序的入口處,而 CS 上的 CPL 是用戶態,因此 iret 進行中斷返回的時候會將堆棧切換到用戶的棧,並且完成特權級的切換,並且跳轉到要求的應用程序的入口處;
  5. 接下來開始具體執行應用程序的第一條指令;

練習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()

我們回顧一下 do_fork 的執行過程,它完成的工作主要如下:

  • 1、分配並初始化進程控制塊( alloc_proc 函數);
  • 2、分配並初始化內核棧,爲內核進程(線程)建立棧空間( setup_stack 函數);
  • 3、根據 clone_flag 標誌複製或共享進程內存管理結構( copy_mm 函數);
  • 4、設置進程在內核(將來也包括用戶態)正常運行和調度所需的中斷幀和執行上下文 ( copy_thread 函數);
  • 5、爲進程分配一個 PID( get_pid() 函數);
  • 6、把設置好的進程控制塊放入 hash_list 和 proc_list 兩個全局進程鏈表中;
  • 7、自此,進程已經準備好執行了,把進程狀態設置爲“就緒”態;
  • 8、設置返回碼爲子進程的 PID 號。
/*
   file_path:kern/process/proc.c
*/
/* 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
 */
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) { //分配進程數大於 4096,返回
        goto fork_out;  //返回
    }
    ret = -E_NO_MEM;  //因內存不足而分配失敗
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; 
    //將新進程添加到進程的 hash 列表中
    local_intr_save(intr_flag);  //屏蔽中斷,intr_flag 置爲 1
    {
        proc->pid = get_pid(); //獲取當前進程 PID
        hash_proc(proc);  //建立 hash 映射
        list_add(&proc_list,&(proc->list_link));//將進程加入到進程鏈表中
        nr_process ++;  //進程數加一
    }
    local_intr_restore(intr_flag); //恢復中斷
    wakeup_proc(proc); //一切就緒,喚醒新進程
    ret = proc->pid; //返回當前進程的 PID
fork_out:  //已分配進程數大於4096
    return ret;
bad_fork_cleanup_kstack: //分配內核棧失敗
    put_kstack(proc);
bad_fork_cleanup_proc: 
    kfree(proc);
    goto fork_out;
}

由於 do_fork 函數中調用了 copy_mm 函數,這部分是我們在 lab4 中未實現的部分,我們可以看看這部分函數是如何實現的:

/*
   file_path:kern/process/proc.c
*/
// copy_mm - process "proc" duplicate OR share process "current"'s mm according clone_flags
//         - if clone_flags & CLONE_VM, then "share" ; else "duplicate"
static int copy_mm(uint32_t clone_flags, struct proc_struct *proc) {
    struct mm_struct *mm, *oldmm = current->mm;
    /* current is a kernel thread */
    if (oldmm == NULL) { //當前進程地址空間爲 NULL
        return 0;
    }
    if (clone_flags & CLONE_VM) { //可以共享地址空間
        mm = oldmm;  //共享地址空間
        goto good_mm;
    }
    int ret = -E_NO_MEM; 
    if ((mm = mm_create()) == NULL) { //創建地址空間未成功
        goto bad_mm;
    }
    if (setup_pgdir(mm) != 0) { 
        goto bad_pgdir_cleanup_mm;
    }
    lock_mm(oldmm); //打開互斥鎖,避免多個進程同時訪問內存
    {
        ret = dup_mmap(mm, oldmm); //調用 dup_mmap 函數
    }
    unlock_mm(oldmm); //釋放互斥鎖
    if (ret != 0) {
        goto bad_dup_cleanup_mmap;
    }
good_mm:
    mm_count_inc(mm);  //共享地址空間的進程數加一
    proc->mm = mm;     //複製空間地址
    proc->cr3 = PADDR(mm->pgdir); //複製頁表地址
    return 0;
bad_dup_cleanup_mmap:
    exit_mmap(mm);
    put_pgdir(mm);
bad_pgdir_cleanup_mm:
    mm_destroy(mm);
bad_mm:
    return ret; 
}

由於 copy_mm 函數調用 dup_mmap 函數,我們可以看看這部分函數是如何實現的:

/*
   file_path:kern/mm/vmm.c
*/
int dup_mmap(struct mm_struct *to, struct mm_struct *from) {
    assert(to != NULL && from != NULL); //必須非空
    // mmap_list 爲虛擬地址空間的首地址
    list_entry_t *list = &(from->mmap_list), *le = list;
    while ((le = list_prev(le)) != list) { //遍歷所有段
        struct vma_struct *vma, *nvma; 
        vma = le2vma(le, list_link); //獲取某一段
        nvma = vma_create(vma->vm_start, vma->vm_end, vma->vm_flags);
        if (nvma == NULL) {
            return -E_NO_MEM;
        }
        insert_vma_struct(to, nvma); //向新進程插入新創建的段
        bool share = 0; 
        //調用 copy_range 函數
        if (copy_range(to->pgdir, from->pgdir, vma->vm_start, vma->vm_end, share) != 0) {
            return -E_NO_MEM;
        }
    }
    return 0;
}

由於 dup_mmap 函數調用 copy_range 函數,這部分函數實現如下:

/*
   file_path:kern/mm/pmm.c
*/
/* copy_range - copy content of memory (start, end) of one process A to another process B
 * @to:    the addr of process B's Page Directory
 * @from:  the addr of process A's Page Directory
 * @share: flags to indicate to dup OR share. We just use dup method, so it didn't be used.
 *
 * CALL GRAPH: copy_mm-->dup_mmap-->copy_range
 */
int copy_range(pde_t *to, pde_t *from, uintptr_t start, uintptr_t end, bool share) {
    assert(start % PGSIZE == 0 && end % PGSIZE == 0);
    assert(USER_ACCESS(start, end));
    // copy content by page unit.
    do {
        //call get_pte to find process A's pte according to the addr start
        pte_t *ptep = get_pte(from, start, 0), *nptep;
        if (ptep == NULL) {
            start = ROUNDDOWN(start + PTSIZE, PTSIZE);
            continue ;
        }
        //call get_pte to find process B's pte according to the addr start. If pte is NULL, just alloc a PT
        if (*ptep & PTE_P) {
            if ((nptep = get_pte(to, start, 1)) == NULL) {
                return -E_NO_MEM;
            }
        uint32_t perm = (*ptep & PTE_USER);
        //get page from ptep
        struct Page *page = pte2page(*ptep);
        // alloc a page for process B
        struct Page *npage=alloc_page();
        assert(page!=NULL);
        assert(npage!=NULL);
        int ret=0;
        /* LAB5:EXERCISE2 YOUR CODE
         * replicate content of page to npage, build the map of phy addr of nage with the linear addr start
         *
         * Some Useful MACROs and DEFINEs, you can use them in below implementation.
         * MACROs or Functions:
         *    page2kva(struct Page *page): return the kernel vritual addr of memory which page managed (SEE pmm.h)
         *    page_insert: build the map of phy addr of an Page with the linear addr la
         *    memcpy: typical memory copy function
         *
         * (1) find src_kvaddr: the kernel virtual address of page
         * (2) find dst_kvaddr: the kernel virtual address of npage
         * (3) memory copy from src_kvaddr to dst_kvaddr, size is PGSIZE
         * (4) build the map of phy addr of  nage with the linear addr start
         */
--------------------------------------------------------------------------------------------
* 實現思路:
copy_range 函數的具體執行流程是遍歷父進程指定的某一段內存空間中的每一個虛擬頁,如果這個虛擬頁是存在的話,爲子進程對應的同一個地址(但是頁目錄表是不一樣的,因此不是一個內存空間)也申請分配一個物理頁,然後將前者中的所有內容複製到後者中去,然後爲子進程的這個物理頁和對應的虛擬地址(事實上是線性地址)建立映射關係;而在本練習中需要完成的內容就是內存的複製和映射的建立,具體流程如下:
  1. 找到父進程指定的某一物理頁對應的內核虛擬地址;
  2. 找到需要拷貝過去的子進程的對應物理頁對應的內核虛擬地址;
  3. 將前者的內容拷貝到後者中去;
  4. 爲子進程當前分配這一物理頁映射上對應的在子進程虛擬地址空間裏的一個虛擬頁;
--------------------------------------------------------------------------------------------
/*code*/
        void * kva_src = page2kva(page); // 找到父進程需要複製的物理頁在內核地址空間中的虛擬地址,這是由於這個函數執行的時候使用的時內核的地址空間
        void * kva_dst = page2kva(npage); // 找到子進程需要被填充的物理頁的內核虛擬地址
    
        memcpy(kva_dst, kva_src, PGSIZE); // 將父進程的物理頁的內容複製到子進程中去

        ret = page_insert(to, npage, start, perm); // 建立子進程的物理頁與虛擬頁的映射關係
        assert(ret == 0);
        }
        start += PGSIZE;
    } while (start != 0 && start < end);
    return 0;
}

請在實驗報告中簡要說明如何設計實現 ”Copy on Write 機制“,給出概要設計,鼓勵給出詳細設計。

接下來將說明如何實現 “Copy on Write” 機制,該機制的主要思想爲使得進程執行 fork 系統調用進行復制的時候,父進程不會簡單地將整個內存中的內容複製給子進程,而是暫時共享相同的物理內存頁;而當其中一個進程需要對內存進行修改的時候,再額外創建一個自己私有的物理內存頁,將共享的內容複製過去,然後在自己的內存頁中進行修改;根據上述分析,主要對實驗框架的修改應當主要有兩個部分,一個部分在於進行 fork 操作的時候不直接複製內存,另外一個處理在於出現了內存頁訪問異常的時候,會將共享的內存頁複製一份,然後在新的內存頁進行修改,具體的修改部分如下:

  • do fork 部分:在進行內存複製的部分,比如 copy_range 函數內部,不實際進行內存的複製,而是將子進程和父進程的虛擬頁映射上同一個物理頁面,然後在分別在這兩個進程的虛擬頁對應的 PTE 部分將這個頁置成是不可寫的,同時利用 PTE 中的保留位將這個頁設置成共享的頁面,這樣的話如果應用程序試圖寫某一個共享頁就會產生頁訪問異常,從而可以將控制權交給操作系統進行處理;
  • page fault 部分:在 page fault 的 ISR 部分,新增加對當前的異常是否由於嘗試寫了某一個共享頁面引起的,如果是的話,額外申請分配一個物理頁面,然後將當前的共享頁的內容複製過去,建立出錯的線性地址與新創建的物理頁面的映射關係,將 PTE 設置設置成非共享的;然後查詢原先共享的物理頁面是否還是由多個其它進程共享使用的,如果不是的話,就將對應的虛地址的 PTE 進行修改,刪掉共享標記,恢復寫標記;這樣的話 page fault 返回之後就可以正常完成對虛擬內存(原想的共享內存)的寫操作了;

上述實現有一個較小的缺陷,在於在 do fork 的時候需要修改所有的 PTE,會有一定的時間效率上的損失;可以考慮將共享的標記加在 PDE 上,然後一旦訪問了這個 PDE 之後再將標記下傳給對應的 PTE,這樣的話就起到了標記延遲和潛在的標記合併的左右,有利於提升時間效率;

練習3: 閱讀分析源代碼,理解進程執行 fork/exec/wait/exit 的實現,以及系統調用的實現(不需要編碼)

首先我們可以羅列下目前 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 返回用戶態,並恢復現場。這樣整個系統調用就執行完畢了。

接下來對 fork/exec/wait/exit 四個系統調用進行分析:

fork

調用過程爲:fork->SYS_fork->do_fork+wakeup_proc

首先當程序執行 fork 時,fork 使用了系統調用 SYS_fork,而系統調用 SYS_fork 則主要是由 do_fork 和 wakeup_proc 來完成的。do_fork() 完成的工作在練習 2 及 lab4 中已經做過詳細介紹,這裏再簡單說一下,主要是完成了以下工作:

  • 1、分配並初始化進程控制塊( alloc_proc 函數);
  • 2、分配並初始化內核棧,爲內核進程(線程)建立棧空間( setup_stack 函數);
  • 3、根據 clone_flag 標誌複製或共享進程內存管理結構( copy_mm 函數);
  • 4、設置進程在內核(將來也包括用戶態)正常運行和調度所需的中斷幀和執行上下文 ( copy_thread 函數);
  • 5、爲進程分配一個 PID( get_pid() 函數);
  • 6、把設置好的進程控制塊放入 hash_list 和 proc_list 兩個全局進程鏈表中;
  • 7、自此,進程已經準備好執行了,把進程狀態設置爲“就緒”態;
  • 8、設置返回碼爲子進程的 PID 號。

而 wakeup_proc 函數主要是將進程的狀態設置爲等待,即 proc->wait_state = 0。

exec

調用過程爲:SYS_exec->do_execve

當應用程序執行的時候,會調用 SYS_exec 系統調用,而當 ucore 收到此係統調用的時候,則會使用 do_execve() 函數來實現,因此這裏我們主要介紹 do_execve() 函數的功能,函數主要時完成用戶進程的創建工作,同時使用戶進程進入執行。

主要工作如下:

  • 1、首先爲加載新的執行碼做好用戶態內存空間清空準備。如果 mm 不爲 NULL,則設置頁表爲內核空間頁表,且進一步判斷 mm 的引用計數減 1 後是否爲 0,如果爲 0,則表明沒有進程再需要此進程所佔用的內存空間,爲此將根據 mm 中的記錄,釋放進程所佔用戶空間內存和進程頁表本身所佔空間。最後把當前進程的 mm 內存管理指針爲空。
  • 2、接下來是加載應用程序執行碼到當前進程的新創建的用戶態虛擬空間中。之後就是調用 load_icode 從而使之準備好執行。(具體 load_icode 的功能在練習 1 已經介紹的很詳細了,這裏不贅述了)

wait

調用過程爲:SYS_wait->do_wait

我們可以看看 do_wait 函數的實現過程:

/*
   file_path:kern/process/proc.c
*/
// do_wait - wait one OR any children with PROC_ZOMBIE state, and free memory space of kernel stack
//         - proc struct of this child.
// NOTE: only after do_wait function, all resources of the child proces are free.
int do_wait(int pid, int *code_store) {
    struct mm_struct *mm = current->mm;
    if (code_store != NULL) {
        if (!user_mem_check(mm, (uintptr_t)code_store, sizeof(int), 1)) {
            return -E_INVAL;
        }
    }
    struct proc_struct *proc;
    bool intr_flag, haskid;
repeat:
    haskid = 0;
    //如果pid!=0,則找到進程id爲pid的處於退出狀態的子進程 
    if (pid != 0) {
        proc = find_proc(pid);
        if (proc != NULL && proc->parent == current) {
            haskid = 1;
            if (proc->state == PROC_ZOMBIE) {
                goto found; //找到進程
            }
        }
    }
    else {
      //如果pid==0,則隨意找一個處於退出狀態的子進程
        proc = current->cptr;
        for (; proc != NULL; proc = proc->optr) {
            haskid = 1;
            if (proc->state == PROC_ZOMBIE) {
                goto found;
            }
        }
    }
    if (haskid) {//如果沒找到,則父進程重新進入睡眠,並重復尋找的過程
        current->state = PROC_SLEEPING;
        current->wait_state = WT_CHILD;
        schedule();
        if (current->flags & PF_EXITING) {
            do_exit(-E_KILLED);
        }
        goto repeat;
    }
    return -E_BAD_PROC;
   //釋放子進程的所有資源 
found:
    if (proc == idleproc || proc == initproc) {
        panic("wait idleproc or initproc.\n");
    }
    if (code_store != NULL) {
        *code_store = proc->exit_code;
    }
    local_intr_save(intr_flag);
    {
        unhash_proc(proc);//將子進程從hash_list中刪除
        remove_links(proc);//將子進程從proc_list中刪除 
    }
    local_intr_restore(intr_flag);
    put_kstack(proc); //釋放子進程的內核堆棧
    kfree(proc);  //釋放子進程的進程控制塊
    return 0;
}

當執行 wait 功能的時候,會調用系統調用 SYS_wait,而該系統調用的功能則主要由 do_wait 函數實現,主要工作就是父進程如何完成對子進程的最後回收工作,具體的功能實現如下:

  • 1、 如果 pid!=0,表示只找一個進程 id 號爲 pid 的退出狀態的子進程,否則找任意一個處於退出狀態的子進程;
  • 2、 如果此子進程的執行狀態不爲 PROC_ZOMBIE,表明此子進程還沒有退出,則當前進程設置執行狀態爲 PROC_SLEEPING(睡眠),睡眠原因爲 WT_CHILD (即等待子進程退出),調用 schedule() 函數選擇新的進程執行,自己睡眠等待,如果被喚醒,則重複跳回步驟 1 處執行;
  • 3、 如果此子進程的執行狀態爲 PROC_ZOMBIE,表明此子進程處於退出狀態,需要當前進程(即子進程的父進程)完成對子進程的最終回收工作,即首先把子進程控制塊從兩個進程隊列 proc_list 和 hash_list 中刪除,並釋放子進程的內核堆棧和進程控制塊。自此,子進程才徹底地結束了它的執行過程,它所佔用的所有資源均已釋放。

exit

調用過程爲:SYS_exit->exit

我們可以看看 do_exit 函數的實現過程:

/*
   file_path:kern/process/proc.c
*/
// do_exit - called by sys_exit
//   1. call exit_mmap & put_pgdir & mm_destroy to free the almost all memory space of process
//   2. set process' state as PROC_ZOMBIE, then call wakeup_proc(parent) to ask parent reclaim itself.
//   3. call scheduler to switch to other process
int do_exit(int error_code) {
    if (current == idleproc) {
        panic("idleproc exit.\n");
    }
    if (current == initproc) {
        panic("initproc exit.\n");
    }
    struct mm_struct *mm = current->mm;
    if (mm != NULL) { //如果該進程是用戶進程
        lcr3(boot_cr3); //切換到內核態的頁表
        if (mm_count_dec(mm) == 0){
            exit_mmap(mm); 
/*如果沒有其他進程共享這個內存釋放current->mm->vma鏈表中每個vma描述的進程合法空間中實際分配的內存,然後把對應的頁表項內容清空,最後還把頁表所佔用的空間釋放並把對應的頁目錄表項清空*/
            put_pgdir(mm); //釋放頁目錄佔用的內存 
            mm_destroy(mm); //釋放mm佔用的內存
        }
        current->mm = NULL; //虛擬內存空間回收完畢
    }
    current->state = PROC_ZOMBIE; //僵死狀態
    current->exit_code = error_code;//等待父進程做最後的回收
    bool intr_flag;
    struct proc_struct *proc;
    local_intr_save(intr_flag);
    {
        proc = current->parent;
        if (proc->wait_state == WT_CHILD) {
            wakeup_proc(proc); //如果父進程在等待子進程,則喚醒
        }
        while (current->cptr != NULL) {
 /*如果當前進程還有子進程,則需要把這些子進程的父進程指針設置爲內核線程initproc,且各個子進程指針需要插入到initproc的子進程鏈表中。如果某個子進程的執行狀態是PROC_ZOMBIE,則需要喚醒initproc來完成對此子進程的最後回收工作。*/
            proc = current->cptr;
            current->cptr = proc->optr;

            proc->yptr = NULL;
            if ((proc->optr = initproc->cptr) != NULL) {
                initproc->cptr->yptr = proc;
            }
            proc->parent = initproc;
            initproc->cptr = proc;
            if (proc->state == PROC_ZOMBIE) {
                if (initproc->wait_state == WT_CHILD) {
                    wakeup_proc(initproc);
                }
            }
        }
    }
    local_intr_restore(intr_flag);
    schedule(); //選擇新的進程執行
    panic("do_exit will not return!! %d.\n", current->pid);
}

當執行 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() 調度函數,選擇新的進程執行。

所以說該函數的功能簡單的說就是,回收當前進程所佔的大部分內存資源,並通知父進程完成最後的回收工作。

請分析 fork/exec/wait/exit 在實現中是如何影響進程的執行狀態的?

  • fork 執行完畢後,如果創建新進程成功,則出現兩個進程,一個是子進程,一個是父進程。在子進程中,fork 函數返回 0,在父進程中,fork 返回新創建子進程的進程 ID。我們可以通過 fork 返回的值來判斷當前進程是子進程還是父進程。fork 不會影響當前進程的執行狀態,但是會將子進程的狀態標記爲 RUNNALB,使得可以在後續的調度中運行起來;
  • exec 完成用戶進程的創建工作。首先爲加載新的執行碼做好用戶態內存空間清空準備。接下來的一步是加載應用程序執行碼到當前進程的新創建的用戶態虛擬空間中。exec 不會影響當前進程的執行狀態,但是會修改當前進程中執行的程序;
  • wait 是等待任意子進程的結束通知。wait_pid 函數等待進程 id 號爲 pid 的子進程結束通知。這兩個函數最終訪問 sys_wait 系統調用接口讓 ucore 來完成對子進程的最後回收工作。wait 系統調用取決於是否存在可以釋放資源(ZOMBIE)的子進程,如果有的話不會發生狀態的改變,如果沒有的話會將當前進程置爲 SLEEPING 態,等待執行了 exit 的子進程將其喚醒;
  • exit 會把一個退出碼 error_code 傳遞給 ucore,ucore 通過執行內核函數 do_exit 來完成對當前進程的退出處理,主要工作簡單地說就是回收當前進程所佔的大部分內存資源,並通知父進程完成最後的回收工作。exit 會將當前進程的狀態修改爲 ZOMBIE 態,並且會將父進程喚醒(修改爲RUNNABLE),然後主動讓出 CPU 使用權;

請給出 ucore 中一個用戶態進程的執行狀態生命週期圖(包執行狀態,執行狀態之間的變換關係,以及產生變換的事件或函數調用)。(字符方式畫即可)

首先,我們梳理一下流程:

process_state

最終,我們可以畫出執行狀態圖如下所示:

ucore_run

最終的實驗結果如下圖所示:

make_grade

如果 make grade 無法滿分,嘗試註釋掉 tools/grade.sh 的 221 行到 233 行(在前面加上“#”)。

這裏我們選用古老的編輯器 Vim,具體操作過程如下:

  • 1、首先按 esc 進入命令行模式下,按下 :221 跳轉至 221 行;
  • 2、按下 Ctrl + v,進入列(也叫區塊)模式;
  • 3、在行首使用上下鍵選擇需要註釋的多行(221~233 行);
  • 4、按下鍵盤(大寫)“I”鍵,進入插入模式;
  • 5、然後輸入註釋符(“//”、“#”等);
  • 6、最後按下“Esc”鍵。

擴展練習 Challenge :實現 Copy on Write (COW)機制

設置共享標誌

在 vmm.c 中將 dup_mmap 中的 share 變量的值改爲 1,啓用共享:

int dup_mmap(struct mm_struct *to, struct mm_struct *from) {
        ...
        bool share = 1;
        ...
}

映射共享頁面

在 pmm.c 中爲 copy_range 添加對共享的處理,如果 share 爲 1,那麼將子進程的頁面映射到父進程的頁面。由於兩個進程共享一個頁面之後,無論任何一個進程修改頁面,都會影響另外一個頁面,所以需要子進程和父進程對於這個共享頁面都保持只讀。

int copy_range(pde_t *to, pde_t *from, uintptr_t start, uintptr_t end, bool share) {
    ...
        if (*ptep & PTE_P) {
            if ((nptep = get_pte(to, start, 1)) == NULL) {
                return -E_NO_MEM;
            }
            uint32_t perm = (*ptep & PTE_USER);
            //get page from ptep
            struct Page *page = pte2page(*ptep);
            assert(page!=NULL);
            int ret=0;
            if (share) {    
                // share page
                page_insert(from, page, start, perm & (~PTE_W));
                ret = page_insert(to, page, start, perm & (~PTE_W));
            } else {
                // alloc a page for process B
                struct Page *npage=alloc_page();
                assert(npage!=NULL);
                uintptr_t src_kvaddr = page2kva(page);
                uintptr_t dst_kvaddr = page2kva(npage);
                memcpy(dst_kvaddr, src_kvaddr, PGSIZE);
                ret = page_insert(to, npage, start, perm);
            }
            assert(ret == 0);
        }
        ...
    return 0;
}

修改時拷貝

當程序嘗試修改只讀的內存頁面的時候,將觸發Page Fault中斷,在錯誤代碼中 P=1,W/R=1[OSDev]。因此,當錯誤代碼最低兩位都爲 1 的時候,說明進程訪問了共享的頁面,內核需要重新分配頁面、拷貝頁面內容、建立映射關係:

int do_pgfault(struct mm_struct *mm, uint32_t error_code, uintptr_t addr) {
    ...
    if (*ptep == 0) {
        ...
    } else if (error_code & 3 == 3) {   // copy on write
        struct Page *page = pte2page(*ptep);
        struct Page *npage = pgdir_alloc_page(mm->pgdir, addr, perm);
        uintptr_t src_kvaddr = page2kva(page);
        uintptr_t dst_kvaddr = page2kva(npage);
        memcpy(dst_kvaddr, src_kvaddr, PGSIZE);
    } else {
        ...
    }
    ...
}

參考資料

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章