《ucore lab4》實驗報告

資源

  1. ucore在線實驗指導書
  2. 個人ucore實驗代碼

練習1:分配並初始化一個進程控制塊

題目

alloc_proc函數(位於kern/process/proc.c中) 負責分配並返回一個新的struct proc_struct結構,用於存儲新創建的內核線程的管理信息。ucore須要對這個結構進行最基本的初始化,你須要完成這個初始化過程。linux

【提示】 在alloc_proc函數的實現中,須要初始化的proc_struct結構中的成員變量至少包括:state/pid/runs/kstack/need_resched/parent/mm/context/tf/cr3/flags/name。git

請在實驗報告中簡要說明你的設計實現過程。請回答以下問題: 請說明proc_struct中 struct context context 和 struct trapframe *tf 成員變量含義和在本實驗中的做用是啥?(提示經過看代碼和編程調試能夠判斷出來)github

解答

個人設計實現過程

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?請說明你的分析和理由。ui

解答

個人設計實現過程

根據註釋提供的步驟,很容易完成do_fork函數的實現。這裏須要注意的是:若是前面的步驟失敗,好比alloc_proc分配進程控制塊失敗或創建內核棧失敗,那麼須要釋放已申請的資源。this

回答問題: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\"\n", current->pid, get_proc_name(current));
    cprintf("To U: \"%s\".\n", (const char *)arg);
    cprintf("To U: \"en.., Bye, Bye. :)\"\n");
    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

相關文章
相關標籤/搜索