資源
練習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的含義及作用是什么
- 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?請說明你的分析和理由。
解答
我的設計實現過程
根據注釋提供的步驟,很容易完成do_fork函數的實現。這里需要注意的是:如果前面的步驟失敗,比如alloc_proc分配進程控制塊失敗或建立內核棧失敗,那么需要釋放已申請的資源。
回答問題: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