清華大學(xué)操作系統(tǒng)Lab4實(shí)驗(yàn)報(bào)告
課程主頁:http://os.cs.tsinghua.edu.cn/oscourse/OS2018spring
實(shí)驗(yàn)指導(dǎo)書:https://chyyuu.gitbooks.io/ucore_os_docs/content/
github:https://github.com/chyyuu/ucore_os_lab
實(shí)驗(yàn)?zāi)康?/h3>
- 了解內(nèi)核線程創(chuàng)建/執(zhí)行的管理過程
- 了解內(nèi)核線程的切換和基本調(diào)度過程
實(shí)驗(yàn)內(nèi)容
實(shí)驗(yàn)二、三完成了無力和虛擬內(nèi)存管理,這給創(chuàng)建內(nèi)核線程(內(nèi)核線程是一種特殊的進(jìn)程)打下了提供內(nèi)存管理的基礎(chǔ)。當(dāng)一個(gè)程序加載到內(nèi)存中運(yùn)行時(shí),首先通過ucore OS的內(nèi)存管理子系統(tǒng)分配合適的空間,然后就需要考慮如何分時(shí)使用CPU來“并發(fā)”執(zhí)行多個(gè)程序,讓每個(gè)運(yùn)行的程序(這里用線程或進(jìn)程表示)“感到”它們各自擁有“自己”的CPU。
本次實(shí)驗(yàn)將首先接觸的是內(nèi)核線程的管理。內(nèi)核線程是一種特殊的進(jìn)程,內(nèi)核線程與用戶進(jìn)程的區(qū)別有兩個(gè):
- 內(nèi)核線程只運(yùn)行在內(nèi)核態(tài)
- 用戶進(jìn)程會(huì)在用戶態(tài)和內(nèi)核態(tài)交替運(yùn)行
- 所有內(nèi)核線程共用ucore內(nèi)核內(nèi)存空間,不需為每個(gè)內(nèi)核線程維護(hù)單獨(dú)的內(nèi)存空間
- 而用戶線程需要維護(hù)各自的用戶內(nèi)存空間
練習(xí)1:分配并初始化一個(gè)進(jìn)程控制塊
在alloc_proc函數(shù)中,初始化進(jìn)程控制塊。
// alloc_proc - alloc a proc_struct and init all fields of proc_struct
static struct proc_struct *
alloc_proc(void) {
struct proc_struct *proc = kmalloc(sizeof(struct proc_struct));
if (proc != NULL) {
//LAB4:EXERCISE1 2015011345
proc -> state = PROC_UNINIT;
proc -> pid = -1;
proc -> cr3 = boot_cr3; // 內(nèi)核線程,頁表使用boot_cr3
proc -> mm = NULL;
proc -> runs = 0;
proc -> kstack = 0;
proc -> need_resched = 0;
proc -> parent = NULL;
memset(&(proc -> context), 0, sizeof(struct context));
proc -> tf = NULL;
proc -> flags = 0;
memset(proc -> name, 0, PROC_NAME_LEN);
}
return proc;
}
請(qǐng)說明proc_struct中struct context context和struct trapframe *tf成員變量含義和在本實(shí)驗(yàn)中的作用。
proc_struct中的context:進(jìn)程的上下文,用于進(jìn)程切換(switch.S中)。在uCore中,所有的進(jìn)程在內(nèi)核中也是相對(duì)獨(dú)立的(例如獨(dú)立的內(nèi)核堆棧和上下文等等)。使用context保存寄存器的目的就在于在內(nèi)核態(tài)中能夠進(jìn)行上下文之間的切換。實(shí)際利用context進(jìn)行上下文切換的函數(shù)是在kern/process/switch.S中的switch_to。proc_struct中的tf:中斷幀的指針??偸侵赶騼?nèi)核棧的某個(gè)位置:當(dāng)進(jìn)程從用戶空間跳到內(nèi)核空間時(shí),中斷幀記錄了進(jìn)程在被中斷前的狀態(tài)。當(dāng)內(nèi)核需要跳回用戶空間時(shí),需要調(diào)整中斷幀以恢復(fù)讓進(jìn)程繼續(xù)執(zhí)行的各寄存器值。除此之外,uCore內(nèi)核允許嵌套中斷。因此為了保證嵌套中斷發(fā)生時(shí)tf總是能夠指向當(dāng)前的trapframe,uCore在內(nèi)核棧上維護(hù)了tf的鏈,在trap.c::trap中。
練習(xí)2:為新創(chuàng)建的內(nèi)核線程分配資源
/* do_fork - parent process for a new child process
* @clone_flags: used to guide how to clone the child process
* @stack: the parent's user stack pointer. if stack==0, It means to fork a kernel thread.
* @tf: the trapframe info, which will be copied to child process's proc->tf
*/
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;
// 1. call alloc_proc to allocate a proc_struct
proc = alloc_proc();
if (proc == NULL) {
goto fork_out;
}
proc -> parent = current;
// 2. call setup_kstack to allocate a kernel stack for child process
int kstack_success = setup_kstack(proc);
if (kstack_success != 0) {
goto bad_fork_cleanup_proc;
}
// 3. call copy_mm to dup OR share mm according clone_flag
int copy_success = copy_mm(clone_flags, proc);
if (copy_success != 0) {
goto bad_fork_cleanup_kstack;
}
// 4. call copy_thread to setup tf & context in proc_struct
copy_thread(proc, stack, tf);
bool intr_flag;
local_intr_save(intr_flag);
proc -> pid = get_pid();
// 5. insert proc_struct into hash_list && proc_list
hash_proc(proc);
list_add(&proc_list, &(proc -> list_link));
nr_process++;
local_intr_restore(intr_flag);
// 6. call wakeup_proc to make the new child process RUNNABLE
wakeup_proc(proc);
// 7. set ret vaule using child proc's pid
ret = proc -> pid;
fork_out:
return ret;
bad_fork_cleanup_kstack:
put_kstack(proc);
bad_fork_cleanup_proc:
kfree(proc);
goto fork_out;
}
請(qǐng)說明ucore是否做到給每個(gè)新fork的線程一個(gè)唯一的id?請(qǐng)說明你的分析和理由。
可以保證每個(gè)線程的id唯一。具體可見proc.c::get_pid函數(shù):
// get_pid - alloc a unique pid for process
static int
get_pid(void) {
static_assert(MAX_PID > MAX_PROCESS);
struct proc_struct *proc;
list_entry_t *list = &proc_list, *le;
static int next_safe = MAX_PID, last_pid = MAX_PID;
if (++ last_pid >= MAX_PID) {
last_pid = 1;
goto inside;
}
if (last_pid >= next_safe) {
inside:
next_safe = MAX_PID;
repeat:
le = list;
while ((le = list_next(le)) != list) {
proc = le2proc(le, list_link);
if (proc->pid == last_pid) {
if (++ last_pid >= next_safe) {
if (last_pid >= MAX_PID) {
last_pid = 1;
}
next_safe = MAX_PID;
goto repeat;
}
}
else if (proc->pid > last_pid && next_safe > proc->pid) {
next_safe = proc->pid;
}
}
}
return last_pid;
}
首先,第一句assert可以保證進(jìn)程數(shù)一定不會(huì)多于可以分配的進(jìn)程標(biāo)識(shí)號(hào)的數(shù)目。
接下來,函數(shù)將掃描所有的進(jìn)程,找到一個(gè)當(dāng)前沒被使用的進(jìn)程號(hào),存儲(chǔ)在last_pid中,作為新進(jìn)程的進(jìn)程號(hào)。具體來說,循環(huán)掃描每一個(gè)當(dāng)前進(jìn)程:當(dāng)一個(gè)現(xiàn)有的進(jìn)程號(hào)和last_pid相等時(shí),則將last_pid+1;當(dāng)現(xiàn)有的進(jìn)程號(hào)大于last_pid時(shí),這意味著在已經(jīng)掃描的進(jìn)程中[last_pid, min(next_safe, proc->pid)]這段進(jìn)程號(hào)尚未被占用,繼續(xù)掃描。
這樣可以保證返回的新進(jìn)程號(hào)一定沒有被占用,即具有唯一的id。
練習(xí)3:閱讀代碼,理解proc_run函數(shù)和它調(diào)用的函數(shù)如何完成進(jìn)程切換的。
// proc_run - make process "proc" running on cpu
// NOTE: before call switch_to, should load base addr of "proc"'s new PDT
void
proc_run(struct proc_struct *proc) {
// 如果要調(diào)度的進(jìn)程不是當(dāng)前進(jìn)程的話進(jìn)行如下操作
if (proc != current) {
bool intr_flag;
struct proc_struct *prev = current, *next = proc;
// 關(guān)中斷,防止進(jìn)程調(diào)度過程中再發(fā)生其他中斷導(dǎo)致嵌套的進(jìn)程調(diào)度
local_intr_save(intr_flag);
{
// 當(dāng)前進(jìn)程設(shè)為待調(diào)度的進(jìn)程
current = proc;
// 加載待調(diào)度進(jìn)程的內(nèi)核?;刂泛晚摫砘刂? load_esp0(next->kstack + KSTACKSIZE);
lcr3(next->cr3);
// 保存原線程的寄存器并恢復(fù)待調(diào)度線程的寄存器
switch_to(&(prev->context), &(next->context));
}
// 恢復(fù)中斷
local_intr_restore(intr_flag);
}
}
保存寄存器和恢復(fù)待調(diào)度進(jìn)程的寄存器部分代碼在switch_to中,如下:
switch_to: # switch_to(from, to)
# save from's registers
movl 4(%esp), %eax # eax points to from
popl 0(%eax) # save eip !popl
movl %esp, 4(%eax) # save esp::context of from
movl %ebx, 8(%eax) # save ebx::context of from
movl %ecx, 12(%eax) # save ecx::context of from
movl %edx, 16(%eax) # save edx::context of from
movl %esi, 20(%eax) # save esi::context of from
movl %edi, 24(%eax) # save edi::context of from
movl %ebp, 28(%eax) # save ebp::context of from
# restore to's registers
movl 4(%esp), %eax # not 8(%esp): popped return address already
# eax now points to to
movl 28(%eax), %ebp # restore ebp::context of to
movl 24(%eax), %edi # restore edi::context of to
movl 20(%eax), %esi # restore esi::context of to
movl 16(%eax), %edx # restore edx::context of to
movl 12(%eax), %ecx # restore ecx::context of to
movl 8(%eax), %ebx # restore ebx::context of to
movl 4(%eax), %esp # restore esp::context of to
pushl 0(%eax) # push eip
ret
本實(shí)驗(yàn)在執(zhí)行過程中,創(chuàng)建且運(yùn)行了幾個(gè)內(nèi)核線程
共兩個(gè)線程:
- idleproc:ucore的第一個(gè)內(nèi)核線程,完成內(nèi)核中各個(gè)子系統(tǒng)的初始化,之后立即調(diào)度,執(zhí)行其他進(jìn)程。
- initproc:“hello world”線程。
語句 local_intr_save(intr_flag);....local_intr_restore(intr_flag);在這里有何作用?請(qǐng)說明理由。
在進(jìn)程調(diào)度開始前關(guān)中斷,在結(jié)束進(jìn)程調(diào)度后開中斷。這是為了防止在進(jìn)程調(diào)度過程中產(chǎn)生中斷導(dǎo)致進(jìn)程調(diào)度的嵌套。
覆蓋的知識(shí)點(diǎn)
- 內(nèi)核線程的創(chuàng)建和切換。
與參考答案的區(qū)別
- 練習(xí)1:只寫了一部分初始化,參考答案后發(fā)現(xiàn)沒有寫完整。
- 練習(xí)2:沒有使用
local_intr_save(intr_flag);....local_intr_restore(intr_flag);,后參考答案后不上。
總結(jié)
感謝期中考試讓我有機(jī)會(huì)從頭開始搞清楚了各種原理和ucore的前幾個(gè)lab的很多實(shí)現(xiàn)細(xì)節(jié)。
盡管這次在實(shí)驗(yàn)開始前觀看了mooc并認(rèn)真閱讀了實(shí)驗(yàn)指導(dǎo)書,但是在實(shí)現(xiàn)過程中還是有很多問題,有待進(jìn)一步研究。