資源
練習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的含義及做用是什麼
- 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; };
- 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));
-
trap_frame與context的區別是什麼?編程
- 從內容上看,trap_frame包含了context的信息,除此以外,trap_frame還保存有段寄存器、中斷號、錯誤碼err和狀態寄存器eflags等信息。
- 從做用時機來看,context主要用於進程切換時保存進程上下文,trap_frame主要用於發生中斷或異常時保存進程狀態。
- 當進程進行系統調用或發生中斷時,會發生特權級轉換,這時也會切換棧,所以須要保存棧信息(包括ss和esp)到trap_frame,但不須要更新context。
-
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的含義不理解?
代碼修改
對照答案時,發現本身的代碼有幾個優化的地方:
-
沒有設置proc->parent,應將其設置爲current
-
因爲do_fork已經設置了標籤,setup_kstack執行失敗後直接跳轉到bad_fork_cleanup_proc便可,copy_mm失敗後直接跳轉到bad_fork_cleanup_kstack便可。
-
copy_thread的第二個輸入參數esp應該使用do_fork的第二個輸入參數stack。
-
將當前進程插入到proc_list和hash_list時須要去使能中斷。(爲何?)
-
我是將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函數
-
首先判斷要切換到的進程是否是當前進程,如果則不需進行任何處理。
-
調用local_intr_save和local_intr_restore函數去使能中斷,避免在進程切換過程當中出現中斷。(疑問:進程切換過程當中處理中斷會有什麼問題?)
-
更新current進程爲proc
-
更新任務狀態段的esp0的值(疑問:爲何更新esp0?)
-
從新加載cr3寄存器,使頁目錄表更新爲新進程的頁目錄表
-
上下文切換,把當前進程的當前各寄存器的值保存在其proc_struct結構體的context變量中,再把要切換到的進程的proc_struct結構體的context變量加載到各寄存器。
-
完成上下文切換後,CPU會根據eip寄存器的值找到下一條指令的地址並執行。根據copy_thread函數可知eip寄存器指向forkret函數,forkret函數的實現爲
forkrets(current->tf);
-
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
- 根據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
- 根據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