1. 程式人生 > >作業系統ucore lab5實驗報告

作業系統ucore lab5實驗報告

作業系統lab5實驗報告

到實驗四為止,ucore還一直在核心態“打轉”,沒有到使用者態執行。建立使用者程序,讓使用者程序在使用者態執行,且在需要ucore支援時,可通過系統呼叫來讓ucore提供服務。而本實驗將程序的執行空間擴充套件到了使用者態空間,出現了建立子程序執行應用程式等。即實驗五主要是分析使用者程序的整個生命週期來闡述使用者程序管理的設計與實現。

練習0 填寫已有實驗

這裡和前幾個實驗一樣,照樣運用meld軟體進行對比,大致的截圖如下:


這裡簡單將我們需要修改的地方羅列如下:

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

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

alloc_proc函式

改進後的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;
        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; proc->cptr = proc->optr = proc->yptr = NULL; } return proc; }

比起改進之前多了這兩行程式碼:

        proc->wait_state = 0;//初始化程序等待狀態  
        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)

就像註釋所寫的,這兩行程式碼主要是初始化程序等待狀態、和程序的相關指標,例如父程序、子程序、同胞等等。

因為這裡涉及到了使用者程序,自然需要涉及到排程的問題,所以程序等待狀態和各種指標需要被初始化。

do_fork函式

改進後的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) {
        goto fork_out;
    }
    ret = -E_NO_MEM;
    if ((proc = alloc_proc()) == NULL) {
        goto fork_out;
    }

    proc->parent = current;
    assert(current->wait_state == 0);//確保當前程序正在等待

    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);
        set_links(proc);//將原來簡單的計數改成來執行set_links函式,從而實現設定程序的相關連結 

    }
    local_intr_restore(intr_flag);

    wakeup_proc(proc);

    ret = proc->pid;
fork_out:
    return ret;

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

改動主要是上述程式碼中含註釋的兩行,第一行是為了確定當前的程序正在等待,第二行是將原來的計數換成了執行一個set_links函式,因為要涉及到程序的排程,所以簡單的計數肯定是不行的。

idt_init函式

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

相比於之前,多了這一行程式碼:

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

trap_dispatch函式

改進後的部分函式如下:

 ticks ++;
        if (ticks % TICK_NUM == 0) {
            assert(current != NULL);
            current->need_resched = 1;
        }
        break;

相比與原來主要是多了這一行程式碼

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函式主要做的工作就是先回收自身所佔使用者空間,然後呼叫load_icode,用新的程式覆蓋記憶體空間,形成一個執行新程式的新程序。

由於該完整函式太長,所以這裡我只將我們補充的部分羅如下:

static int load_icode(unsigned char *binary, size_t size) {
    ......
    ......
    /* 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
     */
    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;
    ......
    ......
}

根據註釋這裡我們主要完成的是proc_struct結構中tf結構體變數的設定,因為這裡我們要設定好tf以便於從核心態切換到使用者態然後執行程式,所以這裡tf_cs即程式碼段設定為USER_CS、將tf->tf_dstf->tf_estf->tf_ss均設定為USER_DS。
至於之後的tf_esptf_eip的設定需要看這個圖,這是一個完整的虛擬記憶體空間的分佈圖:

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

這樣子就知道為啥要這樣賦值了。
至於最後的tf->tf_eflags = FL_IF主要是開啟中斷。

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

這裡我們需要填寫以下部分:

int copy_range(pde_t *to, pde_t *from, uintptr_t start, uintptr_t end, bool share) {
    ......
    ......    
    void * kva_src = page2kva(page);//返回父程序的核心虛擬頁地址  
    void * kva_dst = page2kva(npage);//返回子程序的核心虛擬頁地址  
    memcpy(kva_dst, kva_src, PGSIZE);//複製父程序到子程序  
    ret = page_insert(to, npage, start, perm);//建立子程序頁地址起始位置與實體地址的對映關係(prem是許可權)  
    ......
    ......
}

這裡就是呼叫一個memcpy將父程序的記憶體直接複製給子程序即可。

練習3 閱讀分析原始碼,理解程序執行fork/exec/wait/exit的實現,以及系統呼叫的實現

我們逐個進行分析

fork

首先當程式執行fork時,fork使用了系統呼叫SYS_fork,而系統呼叫SYS_fork則主要是由do_forkwakeup_proc來完成的。do_fork()完成的工作在lab4的時候已經做過詳細介紹,這裡再簡單說一下,主要是完成了以下工作:

  • 1、分配並初始化程序控制塊(alloc_proc 函式);
  • 2、分配並初始化核心棧(setup_stack 函式);
  • 3、根據 clone_flag標誌複製或共享程序記憶體管理結構(copy_mm 函式);
  • 4、設定程序在核心(將來也包括使用者態)正常執行和排程所需的中斷幀和執行上下文(copy_thread 函式);
  • 5、把設定好的程序控制塊放入hash_listproc_list 兩個全域性程序連結串列中;
  • 6、自此,程序已經準備好執行了,把程序狀態設定為“就緒”態;
  • 7、設定返回碼為子程序的 id 號。

wakeup_proc函式主要是將程序的狀態設定為等待,即proc->wait_state = 0,此處不贅述。

exec

當應用程式執行的時候,會呼叫SYS_exec系統呼叫,而當ucore收到此係統呼叫的時候,則會使用do_execve()函式來實現,因此這裡我們主要介紹do_execve()函式的功能,函式主要時完成使用者程序的建立工作,同時使使用者程序進入執行。
主要工作如下:

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

wait

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

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

exit

當執行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()排程函式,選擇新的程序執行。

所以說該函式的功能簡單的說就是,回收當前程序所佔的大部分記憶體資源,並通知父程序完成最後的回收工作。

關於系統呼叫

首先羅列下目前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返回使用者態,並恢復現場。這樣整個系統呼叫就執行完畢了。

實驗結果

在lab5目錄下執行make qemu,得到如下結果:

說明實驗成功。