清華大學操作系統(tǒng)Lab5實驗報告
課程主頁:http://os.cs.tsinghua.edu.cn/oscourse/OS2018spring
實驗指導書:https://chyyuu.gitbooks.io/ucore_os_docs/content/
github:https://github.com/chyyuu/ucore_os_lab
實驗目的
- 了解第一個用戶進程創(chuàng)建過程
- 了解系統(tǒng)調用框架的實現(xiàn)機制
- 了解ucore如何實現(xiàn)系統(tǒng)調用sys_fork/sys_exec/sys_exit/sys_wait來進行進程管理
實驗內容
實驗4完成了內核線程翩迈,但到目前為止辟灰,所有的運行都在內核態(tài)執(zhí)行袍镀。實驗5將創(chuàng)建用戶進程,讓用戶進程在用戶態(tài)執(zhí)行,且在需要ucore支持時,可通過系統(tǒng)調用來讓ucore提供服務锹漱。為此需要構造出第一個用戶進程,并通過系統(tǒng)調用sys_fork/sys_exec/sys_exit/sys_wait來支持運行不同的應用程序慕嚷,完成對用戶進程的執(zhí)行過程的基本管理哥牍。相關原理介紹可看附錄B毕泌。
練習0:填寫已有實驗
除了將原有Lab中的代碼轉移到Lab5之外,還需要做一些修改嗅辣。
在alloc_proc
中撼泛,初始化proc_struct中新加入的幾個成員變量。
//LAB5 2015011346 : (update LAB4 steps)
/*
* below fields(add in LAB5) in proc_struct need to be initialized
* uint32_t wait_state; // waiting state
* struct proc_struct *cptr, *yptr, *optr; // relations between processes
*/
proc -> wait_state = 0x0;
proc -> cptr = proc -> yptr = proc -> optr = NULL;
在do_fork
中澡谭,加入與進程控制有關的信息愿题。
//LAB5 2015011346 : (update LAB4 steps)
/* Some Functions
* set_links: set the relation links of process. ALSO SEE: remove_links: lean the relation links of process
* -------------------
* update step 1: set child proc's parent to current process, make sure current process's wait_state is 0
* update step 5: insert proc_struct into hash_list && proc_list, set the relation links of process
*/
proc = alloc_proc();
if (proc == NULL) {
goto fork_out;
}
assert(proc -> wait_state == 0x0);
proc -> parent = current;
int kstack_success = setup_kstack(proc);
if (kstack_success != 0) {
goto bad_fork_cleanup_proc;
}
int copy_success = copy_mm(clone_flags, proc);
if (copy_success != 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);
set_links(proc);
local_intr_restore(intr_flag);
wakeup_proc(proc);
ret = proc -> pid;
在trap_dispatch
中,修改timer interrupt蛙奖,在中斷時將當前正在運行的進程設置為可調度的潘酗,以便在下一個時間片重新選擇進程。
case IRQ_OFFSET + IRQ_TIMER:
/* LAB5 2015011346 */
/* you should upate you lab1 code (just add ONE or TWO lines of code):
* Every TICK_NUM cycle, you should set current process's current->need_resched = 1
*/
ticks++;
if (ticks == TICK_NUM) {
ticks = 0;
current -> need_resched = 1;
}
練習1:加載應用程序并執(zhí)行
load_icode
函數(shù)中雁仲,在特權級為0的內核棧中創(chuàng)建新的中斷幀仔夺,通過彈出該中斷幀可以賺到特權級為3的用戶程序處執(zhí)行。
/* LAB5:EXERCISE1 2015011346
* should set tf_cs,tf_ds,tf_es,tf_ss,tf_esp,tf_eip,tf_eflags
* NOTICE: If we set trapframe correctly, then the user level process can return to USER MODE from kernel. So
* tf_cs should be USER_CS segment (see memlayout.h)
* tf_ds=tf_es=tf_ss should be USER_DS segment
* tf_esp should be the top addr of user stack (USTACKTOP)
* tf_eip should be the entry point of this binary program (elf->e_entry)
* tf_eflags should be set to enable computer to produce Interrupt
*/
tf -> tf_cs = USER_CS;
tf -> tf_ds = tf -> tf_es = tf -> tf_ss = USER_DS;
tf -> tf_esp = USTACKTOP;
tf -> tf_eip = elf -> e_entry;
tf -> tf_eflags = FL_IF;
請在實驗報告中描述當創(chuàng)建一個用戶態(tài)進程并加載了應用程序后攒砖,CPU是如何讓這個應用程序最終在用戶態(tài)執(zhí)行起來的缸兔。即這個用戶態(tài)進程被ucore選擇占用CPU執(zhí)行( RUNNING態(tài))到具體執(zhí)行應用程序第一條指令的整個經過。
這部分主要在load_icode
函數(shù)中實現(xiàn)祭衩。
- 為內存管理的數(shù)據(jù)結構
mm
分配空間并初始化灶体,代碼如下:
//(1) create a new mm for current process
if ((mm = mm_create()) == NULL) {
goto bad_mm;
}
- 通過
setup_pgdir
為用戶空間創(chuàng)建頁目錄,并將內存管理數(shù)據(jù)結構mm
的pgdir
設置為頁目錄的虛地址掐暮。
//(2) create a new PDT, and mm->pgdir= kernel virtual addr of PDT
if (setup_pgdir(mm) != 0) {
goto bad_pgdir_cleanup_mm;
}
- 接下來將解析已經被載入內存的ELF格式的用戶代碼。解析ELF header政钟,找到用戶程序中program section headers路克。隨后通過調用
mm_map
將不同段的起始地址和長度記錄到虛擬內存空間管理的數(shù)據(jù)結構vma
中去。接下來根據(jù)program section的header中的信息养交,找到每個program section精算,并將其中的內容拷貝到用戶進程的內存中(包括BSS section和TEXT/DATA section)。
//(3) copy TEXT/DATA section, build BSS parts in binary to memory space of process
struct Page *page;
//(3.1) get the file header of the bianry program (ELF format)
struct elfhdr *elf = (struct elfhdr *)binary;
//(3.2) get the entry of the program section headers of the bianry program (ELF format)
struct proghdr *ph = (struct proghdr *)(binary + elf->e_phoff);
//(3.3) This program is valid?
if (elf->e_magic != ELF_MAGIC) {
ret = -E_INVAL_ELF;
goto bad_elf_cleanup_pgdir;
}
uint32_t vm_flags, perm;
struct proghdr *ph_end = ph + elf->e_phnum;
for (; ph < ph_end; ph ++) {
//(3.4) find every program section headers
if (ph->p_type != ELF_PT_LOAD) {
continue ;
}
if (ph->p_filesz > ph->p_memsz) {
ret = -E_INVAL_ELF;
goto bad_cleanup_mmap;
}
if (ph->p_filesz == 0) {
continue ;
}
//(3.5) call mm_map fun to setup the new vma ( ph->p_va, ph->p_memsz)
vm_flags = 0, perm = PTE_U;
if (ph->p_flags & ELF_PF_X) vm_flags |= VM_EXEC;
if (ph->p_flags & ELF_PF_W) vm_flags |= VM_WRITE;
if (ph->p_flags & ELF_PF_R) vm_flags |= VM_READ;
if (vm_flags & VM_WRITE) perm |= PTE_W;
if ((ret = mm_map(mm, ph->p_va, ph->p_memsz, vm_flags, NULL)) != 0) {
goto bad_cleanup_mmap;
}
unsigned char *from = binary + ph->p_offset;
size_t off, size;
uintptr_t start = ph->p_va, end, la = ROUNDDOWN(start, PGSIZE);
ret = -E_NO_MEM;
//(3.6) alloc memory, and copy the contents of every program section (from, from+end) to process's memory (la, la+end)
end = ph->p_va + ph->p_filesz;
//(3.6.1) copy TEXT/DATA section of bianry program
while (start < end) {
if ((page = pgdir_alloc_page(mm->pgdir, la, perm)) == NULL) {
goto bad_cleanup_mmap;
}
off = start - la, size = PGSIZE - off, la += PGSIZE;
if (end < la) {
size -= la - end;
}
memcpy(page2kva(page) + off, from, size);
start += size, from += size;
}
//(3.6.2) build BSS section of binary program
end = ph->p_va + ph->p_memsz;
if (start < la) {
/* ph->p_memsz == ph->p_filesz */
if (start == end) {
continue ;
}
off = start + PGSIZE - la, size = PGSIZE - off;
if (end < la) {
size -= la - end;
}
memset(page2kva(page) + off, 0, size);
start += size;
assert((end < la && start == end) || (end >= la && start == la));
}
while (start < end) {
if ((page = pgdir_alloc_page(mm->pgdir, la, perm)) == NULL) {
goto bad_cleanup_mmap;
}
off = start - la, size = PGSIZE - off, la += PGSIZE;
if (end < la) {
size -= la - end;
}
memset(page2kva(page) + off, 0, size);
start += size;
}
}
- 接下來通過調用mm_map函數(shù)為用戶進程的user stack分配空間:
//(4) build user stack memory
vm_flags = VM_READ | VM_WRITE | VM_STACK;
if ((ret = mm_map(mm, USTACKTOP - USTACKSIZE, USTACKSIZE, vm_flags, NULL)) != 0) {
goto bad_cleanup_mmap;
}
assert(pgdir_alloc_page(mm->pgdir, USTACKTOP-PGSIZE , PTE_USER) != NULL);
assert(pgdir_alloc_page(mm->pgdir, USTACKTOP-2*PGSIZE , PTE_USER) != NULL);
assert(pgdir_alloc_page(mm->pgdir, USTACKTOP-3*PGSIZE , PTE_USER) != NULL);
assert(pgdir_alloc_page(mm->pgdir, USTACKTOP-4*PGSIZE , PTE_USER) != NULL);
- 建立用戶進程的內存管理數(shù)據(jù)結構
mm
中的內容碎连,并在進程控制塊中記錄下用戶進程的頁目錄地址灰羽,將用戶進程的頁目錄地址賦給CR3寄存器。
//(5) set current process's mm, sr3, and set CR3 reg = physical addr of Page Directory
mm_count_inc(mm);
current->mm = mm;
current->cr3 = PADDR(mm->pgdir);
lcr3(PADDR(mm->pgdir));
- 最后清空原來的中斷幀鱼辙,建立新的中斷幀廉嚼,代碼就是填入的那段代碼。通過iret指令從內核棧中彈出中斷幀恢復各種段寄存器的值倒戏。這時段寄存器已經指向特權級為3的段怠噪,也就說完成了到用戶進程的切換。
練習2:父進程復制自己的內存空間給子進程
在copy_range
函數(shù)中完成內存資源的復制杜跷。
/* LAB5:EXERCISE2 2015011346
* replicate content of page to npage, build the map of phy addr of nage with the linear addr start
*
* Some Useful MACROs and DEFINEs, you can use them in below implementation.
* MACROs or Functions:
* page2kva(struct Page *page): return the kernel vritual addr of memory which page managed (SEE pmm.h)
* page_insert: build the map of phy addr of an Page with the linear addr la
* memcpy: typical memory copy function
*
* (1) find src_kvaddr: the kernel virtual address of page
* (2) find dst_kvaddr: the kernel virtual address of npage
* (3) memory copy from src_kvaddr to dst_kvaddr, size is PGSIZE
* (4) build the map of phy addr of nage with the linear addr start
*/
// (1) find src_kvaddr: the kernel virtual address of page
uint32_t src_kvaddr = page2kva(page);
// (2) find dst_kvaddr: the kernel virtual address of npage
uint32_t dst_kvaddr = page2kva(npage);
// (3) memory copy from src_kvaddr to dst_kvaddr, size is PGSIZE
memcpy(dst_kvaddr, src_kvaddr, PGSIZE);
// (4) build the map of phy addr of nage with the linear addr start
ret = page_insert(to, npage, start, perm);
assert(ret == 0);
請在實驗報告中簡要說明如何設計實現(xiàn)”Copy on Write 機制“傍念,給出概要設計矫夷,鼓勵給出詳細設計。
“Copy on Write”是指在fork一個進程時不立刻將父進程的數(shù)據(jù)段/代碼段等復制到子進程的內存空間憋槐,而是當父進程或子進程中對相關內存做出修改時双藕,才進行復制操作。
實現(xiàn)時阳仔,在fork一個進程時忧陪,可以省去load_icode
中創(chuàng)建新頁目錄的操作,而是直接將父進程頁目錄的地址賦給子進程驳概,為了防止誤操作以及辨別是否需要復制赤嚼,應該將尚未完成復制的部分的訪問權限設為只讀。
當執(zhí)行讀操作顺又,父進程和子進程均不受影響更卒。但當執(zhí)行寫操作時,會發(fā)生權限錯誤(因為此時的訪問權限為只讀)稚照。這時候會進入到page fault的處理中去蹂空,在page fault的處理中,如果發(fā)現(xiàn)錯誤原因讀/寫權限問題果录,而訪問的段的段描述符權限為可寫上枕,便可以知道是由于使用COW機制而導致的,這時再將父進程的數(shù)據(jù)段弱恒、代碼段等復制到子進程內存空間上即可辨萍。
練習3:閱讀分析源代碼,理解進程執(zhí)行 fork/exec/wait/exit 的實現(xiàn)返弹,以及系統(tǒng)調用的實現(xiàn)锈玉。
系統(tǒng)調用共用一個中斷號(即代碼中的T_SYSCALL
)。當發(fā)生沖段或異常后义起,會進入到中斷服務例程中去拉背,最終在trap_dispatch
函數(shù)中調用syscall
函數(shù),并通過系統(tǒng)調用號選擇應該執(zhí)行函數(shù)sys_fork/exec/wait/exit
中的一個默终,這些函數(shù)會解析系統(tǒng)調用時傳入的參數(shù)椅棺,并將參數(shù)傳遞給do_fork/execv/wait/exit
執(zhí)行具體操作。
請分析fork/exec/wait/exit在實現(xiàn)中是如何影響進程的執(zhí)行狀態(tài)的齐蔽?
根據(jù)上面的系統(tǒng)調用處理過程的分析两疚,我們只需了解do_fork/execve/wait/exit
中的實現(xiàn)。
-
do_fork
:sys_fork的相關函數(shù)肴熏。在該函數(shù)中鬼雀,首先要為子進程創(chuàng)建進程控制塊,設置好進程控制塊中的上下文的中斷幀等信息蛙吏,為子進程創(chuàng)建用戶棧源哩、內核棧等鞋吉。隨后通過wakeup_proc
函數(shù)將子進程設置為RUNNABLE。之后該函數(shù)給父進程返回子進程的pid励烦,給子進程返回0谓着。隨后在ucore循環(huán)執(zhí)行進程調度schedule
時,就會將子進程考慮進去坛掠。詳細說明見代碼注釋赊锚。
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
// 2. call setup_kstack to allocate a kernel stack for child process
// 3. call copy_mm to dup OR share mm according clone_flag
// 4. call copy_thread to setup tf & context in proc_struct
// 5. insert proc_struct into hash_list && proc_list
// 6. call wakeup_proc to make the new child process RUNNABLE
// 7. set ret vaule using child proc's pid
proc = alloc_proc();
if (proc == NULL) {
goto fork_out;
}
assert(proc -> wait_state == 0x0);
proc -> parent = current;
int kstack_success = setup_kstack(proc);
if (kstack_success != 0) {
goto bad_fork_cleanup_proc;
}
int copy_success = copy_mm(clone_flags, proc);
if (copy_success != 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);
set_links(proc);
local_intr_restore(intr_flag);
wakeup_proc(proc);
ret = proc -> pid;
fork_out:
return ret;
bad_fork_cleanup_kstack:
put_kstack(proc);
bad_fork_cleanup_proc:
kfree(proc);
goto fork_out;
}
-
do_execve
:sys_exec的相關函數(shù)。sys_exec不創(chuàng)建新進程屉栓,而是用新的內容覆蓋原來的進程內存空間舷蒲。在do_execve
中,首先使用exit_mmap
友多、put_pgdir
牲平、mm_destroy
來刪除并釋放掉當前進程內存空間的頁表信息、內存管理信息域滥。隨后通過load_icode
將新的用戶程序從ELF文件中加載進來執(zhí)行纵柿。如果加載失敗,則調用do_exit
退出當前進程启绰。執(zhí)行sys_exec后昂儒,當前進程的狀態(tài)保持不變。詳細說明見代碼注釋委可。
int
do_execve(const char *name, size_t len, unsigned char *binary, size_t size) {
struct mm_struct *mm = current->mm;
if (!user_mem_check(mm, (uintptr_t)name, len, 0)) {
return -E_INVAL;
}
if (len > PROC_NAME_LEN) {
len = PROC_NAME_LEN;
}
char local_name[PROC_NAME_LEN + 1];
memset(local_name, 0, sizeof(local_name));
memcpy(local_name, name, len);
// 刪除當前進程的內存空間里的內容
if (mm != NULL) {
lcr3(boot_cr3);
if (mm_count_dec(mm) == 0) {
// 取消vma中記錄的合法內存塊
exit_mmap(mm);
// 刪除頁表
put_pgdir(mm);
// 刪除mm記錄的信息和占用的空間
mm_destroy(mm);
}
current->mm = NULL;
}
int ret;
// 調用load_icode加載新的進程內容
if ((ret = load_icode(binary, size)) != 0) {
goto execve_exit;
}
set_proc_name(current, local_name);
return 0;
// 如果exec執(zhí)行不成功渊跋,則退出進程
execve_exit:
do_exit(ret);
panic("already exit: %e.\n", ret);
}
-
do_wait
:sys_wait的相關函數(shù)。在該函數(shù)中着倾,循環(huán)查看子進程的狀態(tài)刹枉,直到一個正在等待的子進程的狀態(tài)變成Zombie狀態(tài),這時完成這個子進程的剩余資源回收工作屈呕,釋放子進程的空間。詳細說明見代碼注釋棺亭。
int
do_wait(int pid, int *code_store) {
struct mm_struct *mm = current->mm;
if (code_store != NULL) {
if (!user_mem_check(mm, (uintptr_t)code_store, sizeof(int), 1)) {
return -E_INVAL;
}
}
struct proc_struct *proc;
bool intr_flag, haskid;
// 循環(huán)詢問正在等待的子進程的狀態(tài)虎眨,直到有子進程狀態(tài)變?yōu)閆OMBIE。
repeat:
haskid = 0;
if (pid != 0) {
proc = find_proc(pid);
if (proc != NULL && proc->parent == current) {
haskid = 1;
if (proc->state == PROC_ZOMBIE) {
goto found;
}
}
}
else {
proc = current->cptr;
for (; proc != NULL; proc = proc->optr) {
haskid = 1;
if (proc->state == PROC_ZOMBIE) {
goto found;
}
}
}
if (haskid) {
current->state = PROC_SLEEPING;
current->wait_state = WT_CHILD;
schedule();
if (current->flags & PF_EXITING) {
do_exit(-E_KILLED);
}
goto repeat;
}
return -E_BAD_PROC;
// 如果發(fā)現(xiàn)一個子進程變成了ZOMBIE镶摘,則釋放該子進程剩余的資源嗽桩。
found:
if (proc == idleproc || proc == initproc) {
panic("wait idleproc or initproc.\n");
}
if (code_store != NULL) {
*code_store = proc->exit_code;
}
local_intr_save(intr_flag);
{
unhash_proc(proc);
remove_links(proc);
}
local_intr_restore(intr_flag);
put_kstack(proc);
kfree(proc);
return 0;
}
-
do_exit
:sys_exit的相關函數(shù)。退出時凄敢,首先釋放掉該進程占用的一部分內存(還有一部分可能由父進程釋放)碌冶。然后將該進程標記為僵尸進程。如果它的父進程處于等待子進程退出的狀態(tài)涝缝,則喚醒父進程扑庞,將自己的子進程交給initproc處理譬重,并進行的進程調度。詳細說明見代碼注釋罐氨。
int
do_exit(int error_code) {
if (current == idleproc) {
panic("idleproc exit.\n");
}
if (current == initproc) {
panic("initproc exit.\n");
}
// 釋放該進程的空間
struct mm_struct *mm = current->mm;
if (mm != NULL) {
// 加載當前進程的頁目錄地址
lcr3(boot_cr3);
if (mm_count_dec(mm) == 0) {
// 釋放由vma記錄的內存地址塊
exit_mmap(mm);
// 刪除頁表
put_pgdir(mm);
// 刪除內存管理結構mm占用的內存
mm_destroy(mm);
}
current->mm = NULL;
}
// 記錄當前進程的退出編碼臀规,并標記為僵尸進程
current->state = PROC_ZOMBIE;
current->exit_code = error_code;
bool intr_flag;
struct proc_struct *proc;
local_intr_save(intr_flag);
{
// 如果當前進程的父進程處于等待子進程退出狀態(tài),則將父進程設置為RUNNABLE
proc = current->parent;
if (proc->wait_state == WT_CHILD) {
wakeup_proc(proc);
}
// 如果當前進程有子進程栅隐,則將子進程設置為initproc的子進程塔嬉,并完成子進程中處于僵尸狀態(tài)的進程的最后的回收工作
while (current->cptr != NULL) {
proc = current->cptr;
current->cptr = proc->optr;
proc->yptr = NULL;
if ((proc->optr = initproc->cptr) != NULL) {
initproc->cptr->yptr = proc;
}
proc->parent = initproc;
initproc->cptr = proc;
if (proc->state == PROC_ZOMBIE) {
if (initproc->wait_state == WT_CHILD) {
wakeup_proc(initproc);
}
}
}
}
local_intr_restore(intr_flag);
// 執(zhí)行進程調度
schedule();
panic("do_exit will not return!! %d.\n", current->pid);
}
請給出ucore中一個用戶態(tài)進程的執(zhí)行狀態(tài)生命周期圖(包執(zhí)行狀態(tài),執(zhí)行狀態(tài)之間的變換關系租悄,以及產生變換的事件或函數(shù)調用) 谨究。
覆蓋的知識點
- 進程切換的全過程
- 在父進程執(zhí)行fork時的行為
- 子進程執(zhí)行exit后的行為
與參考答案的區(qū)別
- 練習1:自己完成。
- 練習2:自己完成泣棋。
總結
感覺這次實驗比之前的容易一點胶哲,官方說法應該是由于認真看了mooc和實驗指導書,但實際也可能是因為壓著DDL寫的比較有動力外傅。
同時很懷疑思考題的表述是否準確纪吮,如果不準確希望助教指正。(當然不扣分最好了萎胰,我還是認真寫了的)碾盟。