《ucore lab4》實驗報告


資源

  1. ucore在線實驗指導書
  2. 我的ucore實驗代碼

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

題目

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

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

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

解答

我的設計實現過程

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

解答

我的設計實現過程

根據注釋提供的步驟,很容易完成do_fork函數的實現。這里需要注意的是:如果前面的步驟失敗,比如alloc_proc分配進程控制塊失敗或建立內核棧失敗,那么需要釋放已申請的資源。

回答問題: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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM