資源
練習1: 加載應用程序並執行(需要編碼)
題目
do_execv函數調用load_icode(位於kern/process/proc.c中) 來加載並解析一個處於內存中的ELF執行文件格式的應用程序,建立相應的用戶內存空間來放置應用程序的代碼段、數據段等,且要設置好proc_struct結構中的成員變量trapframe中的內容,確保在執行此進程后,能夠從應用程序設定的起始執行地址開始執行。需設置正確的trapframe內容。
請在實驗報告中簡要說明你的設計實現過程。
請在實驗報告中描述當創建一個用戶態進程並加載了應用程序后,CPU是如何讓這個應用程序最終在用戶態執行起來的。即這個用戶態進程被ucore選擇占用CPU執行(RUNNING態)到具體執行應用程序第一條指令的整個經過。
解答
我的設計實現過程
根據注釋的提示設置trapframe的內容即可。
- tf_cs設置為用戶態代碼段的段選擇子
- tf_ds、tf_es、tf_ss均設置為用戶態數據段的段選擇子
- tf_esp設置為用戶棧的棧頂
- tf_eip設置為ELF文件的入口e_entry
- tf_eflags使能中斷位
用戶態進程從被ucore選擇到執行第一條指令的過程
-
內核線程initproc在創建完成用戶態進程userproc后,調用do_wait函數,do_wait函數在確認存在RUNNABLE的子進程后,調用schedule函數。
-
schedule函數通過調用proc_run來運行新線程,proc_run做了三件事情:
- 設置userproc的棧指針esp為userproc->kstack + 2 * 4096,即指向userproc申請到的2頁棧空間的棧頂
- 加載userproc的頁目錄表。用戶態的頁目錄表跟內核態的頁目錄表不同,因此要重新加載頁目錄表
- 切換進程上下文,然后跳轉到userproc->context.eip指向的函數,即forkret
-
forkret函數直接調用forkrets函數,forkrets先把棧指針指向userproc->tf的地址,然后跳到__trapret
-
__trapret先將userproc->tf的內容pop給相應寄存器,然后通過iret指令,跳轉到userproc->tf.tf_eip指向的函數,即kernel_thread_entry
-
kernel_thread_entry先將edx保存的輸入參數(NULL)壓棧,然后通過call指令,跳轉到ebx指向的函數,即user_main
-
user_main先打印userproc的pid和name信息,然后調用kernel_execve
-
kernel_execve執行exec系統調用,CPU檢測到系統調用后,會保存eflags/ss/eip等現場信息,然后根據中斷號查找中斷向量表,進入中斷處理例程。這里要經過一系列的函數跳轉,才真正進入到exec的系統處理函數do_execve中:vector128 -> __alltraps -> trap -> trap_dispatch -> syscall -> sys_exec -> do_execve
-
do_execve首先檢查用戶態虛擬內存空間是否合法,如果合法且目前只有當前進程占用,則釋放虛擬內存空間,包括取消虛擬內存到物理內存的映射,釋放vma,mm及頁目錄表占用的物理頁等。
-
調用load_icode函數來加載應用程序
- 為用戶進程創建新的mm結構
- 創建頁目錄表
- 校驗ELF文件的魔鬼數字是否正確
- 創建虛擬內存空間,即往mm結構體添加vma結構
- 分配內存,並拷貝ELF文件的各個program section到新申請的內存上
- 為BSS section分配內存,並初始化為全0
- 分配用戶棧內存空間
- 設置當前用戶進程的mm結構、頁目錄表的地址及加載頁目錄表地址到cr3寄存器
- 設置當前用戶進程的tf結構
-
load_icode返回到do_exevce,do_execve設置完當前用戶進程的名字為“exit”后也返回了。這樣一直原路返回到__alltraps函數時,接下來進入__trapret函數
-
__trapret函數先將棧上保存的tf的內容pop給相應的寄存器,然后跳轉到userproc->tf.tf_eip指向的函數,也就是應用程序的入口(exit.c文件中的main函數)。注意,此處的設計十分巧妙:__alltraps函數先將各寄存器的值保存到userproc->tf中,接着將userproc->tf的地址壓入棧后,然后調用trap函數;trap返回后再將current->tf的地址出棧,最后恢復current->tf的內容到各寄存器。這樣看來中斷處理前后各寄存器的值應該保存不變。但事實上,load_icode函數清空了原來的current->tf的內容,並重新設置為應用進程的相關狀態。這樣,當__trapret執行iret指令時,實際上跳轉到應用程序的入口去了,而且特權級也由內核態跳轉到用戶態。接下來就開始執行用戶程序(exit.c文件的main函數)啦。
練習2: 父進程復制自己的內存空間給子進程(需要編碼)
題目
創建子進程的函數do_fork在執行中將拷貝當前進程(即父進程) 的用戶內存地址空間中的合法內容到新進程中(子進程) ,完成內存資源的復制。具體是通過copy_range函數(位於kern/mm/pmm.c中) 實現的,請補充copy_range的實現,確保能夠正確執行。
請在實驗報告中簡要說明如何設計實現”Copy on Write 機制“,給出概要設計,鼓勵給出詳細設計。
Copy-on-write(簡稱COW) 的基本概念是指如果有多個使用者對一個資源A(比如內存塊) 進行讀操作,則每個使用者只需獲得一個指向同一個資源A的指針,就可以該資源了。若某使用者需要對這個資源A進行寫操作,系統會對該資源進行拷貝操作,從而使得該“寫操作”使用者獲得一個該資源A的“私有”拷貝—資源B,可對資源B進行寫操作。該“寫操作”使用者對資源B的改變對於其他的使用者而言是不可見的,因為其他使用者看到的還是資源A。
解答
copy_range的實現
基本思路是遍歷對父進程的每一塊vma,逐頁拷貝其內容給子進程。由於子進程目前只是設置好了mm和vma結構,尚未為虛擬頁分配物理頁。因此在拷貝過程中,需要申請物理頁,拷貝好內容后,調用page_insert建立虛擬地址到物理地址的映射。
“Copy on Write機制”的設計實現(待完善)
如果要實現“Copy on Write機制”,可以在現有代碼的基礎上稍作修改。修改內容:
- 在執行do_fork時,子進程的頁目錄表直接拷貝父進程的頁目錄表,而不是拷貝內核頁目錄表;在dup_mmap,只需保留拷貝vma鏈表的部分,取消調用copy_range來為子進程分配物理內存。
- 將父進程的內存空間對應的所有Page結構的ref均加1,表示子進程也在使用這些內存
- 將父子進程的頁目錄表的寫權限取消,這樣一旦父子進程執行寫操作時,就會發生頁面訪問異常,進入頁面訪問異常處理函數中,再進行內存拷貝操作,並恢復頁目錄表的寫權限。
Bug 1:forktest和forktree執行失敗
問題描述
- 在lab5目錄下執行
sudo make grade
,跑到最后兩個用戶程序forktest和forktree時失敗,打印信息如下:
forktest: (1.3s)
-check result: WRONG
-e !! error: missing 'init check memory pass.'
-check output: OK
forktree: (1.3s)
-check result: WRONG
-e !! error: missing 'init check memory pass.'
-check output: OK
Total Score: 136/150
Makefile:314: recipe for target 'grade' failed
調試過程(尚未解決)
- 單獨執行
sudo make run-forktest
或sudo make run-forktree
,程序跑到后面時均有以下報錯信息:
kernel panic at kern/process/proc.c:851:
assertion failed: nr_free_pages_store == nr_free_pages()
-
調試forktest或forktree,發現在initmain開頭時空閑頁數目nr_free_pages_store = 31827,但在initmain結尾處調用nr_free_pages求到的空閑頁數目為31825,少了2頁。從現象來看發生了內存泄漏。
-
初步推測:forktest函數只是多次調用了fork和wait函數,因此內存泄漏應該是執行這兩個函數過程中發生的。接下來分析下整個過程的內存使用。
-
fork的內存使用情況:
- 調用alloc_proc -> kmalloc,為proc_struct申請分配內存
- 調用copy_mm -> mm_create -> kmalloc,為mm_struct申請分配內存
- 調用setup_pgdir -> alloc_pages,為頁目錄表申請分配物理頁
- 調用dup_mmap -> vma_create -> kmalloc,為每個vma_struct申請分配內存
- 調用dup_mmap -> copy_range -> alloc_page,復制父進程的內存到子進程
-
wait的內存使用情況:
- 調用schedule,切換到forktest進程上下文,執行完main函數后,調用do_exit。do_exit分別調用exit_mmap釋放各vma結構的內存及對應的物理頁、put_pgdir釋放頁目錄表占用的內存,mm_destroy釋放mm_struct占用的內存。
- 調用do_wait -> kfree,釋放proc_struct
-
從fork和wait函數看不出問題。后來嘗試修改進程數目,發現內存泄漏大小與進程數目成正比:12個進程,則泄漏1頁;26個進程,則泄漏2頁;64個進程,則泄漏4頁。
-
在alloc_pages和free_pages入口打印分配或釋放的內存地址,寫個小程序尋找只分配而不釋放的地址,發現是0xc01afba0;在原代碼中增加調試打印,如使用print_stackframe打印調用棧,發現是在fork第11個子進程時出的問題,調用鏈如下所示。
alloc_pages: n = 1, nmalloc = 207, nfree = 0, nr_free = 31619 page = 0xc01afba0
ebp:0xc038fd38 eip:0xc0100d1c args:0x00000000 0x00000000 0x00000001 0xc01afba0
kern/debug/kdebug.c:342: print_stackframe+48
ebp:0xc038fd58 eip:0xc0103e48 args:0x00000001 0x00000078 0xc038fdc8 0xc0107c2b
kern/mm/pmm.c:179: alloc_pages+154
ebp:0xc038fd88 eip:0xc0107c49 args:0x00000000 0x00000000 0xc038fdc8 0xc0107d0d
kern/mm/kmalloc.c:83: __slob_get_free_pages+41
ebp:0xc038fdc8 eip:0xc0107e4b args:0x00000020 0x00000000 0x00000000 0xc0108086
kern/mm/kmalloc.c:142: slob_alloc+407
ebp:0xc038fdf8 eip:0xc01080a9 args:0x00000018 0x00000000 0xc01afb8c 0xc0108184
kern/mm/kmalloc.c:225: __kmalloc+46
ebp:0xc038fe18 eip:0xc0108196 args:0x00000018 0xc0109ce7 0xc038fe34 0xc0105c08
kern/mm/kmalloc.c:251: kmalloc+28
ebp:0xc038fe48 eip:0xc0105c19 args:0xaff00000 0xb0000000 0x0000000b 0xc01060be
kern/mm/vmm.c:65: vma_create+28
ebp:0xc038fe88 eip:0xc0106126 args:0xc0386fe0 0xc03861a0 0xc038feb8 0xc010a81a
kern/mm/vmm.c:197: dup_mmap+116
ebp:0xc038feb8 eip:0xc010a859 args:0x00000000 0xc0386f58 0xc038fee8 0xc010a9f0
kern/process/proc.c:335: copy_mm+140
ebp:0xc038fee8 eip:0xc010aa59 args:0x00000000 0xafffff40 0xc038ffb4 0xc010bd71
kern/process/proc.c:395: do_fork+158
ebp:0xc038ff18 eip:0xc010bd9f args:0xc038ff34 0xc038ff54 0xc0102e88 0xc010bf00
kern/syscall/syscall.c:19: sys_fork+57
ebp:0xc038ff58 eip:0xc010bf78 args:0x00000000 0x00000000 0x00000000 0x00000000
kern/syscall/syscall.c:93: syscall+131
ebp:0xc038ff78 eip:0xc0102ce0 args:0xc038ffb4 0x00000000 0x00000023 0xc0102e2b
kern/trap/trap.c:220: trap_dispatch+300
ebp:0xc038ffa8 eip:0xc0102e88 args:0xc038ffb4 0x00802008 0xafffffa8 0xafffff6c
kern/trap/trap.c:291: trap+104
ebp:0xafffff6c eip:0xc0103960 args:0x00000002 0xafffff88 0x00800263 0x008014d8
kern/trap/trapentry.S:24: <unknown>+0
ebp:0xafffff78 eip:0x0080016c args:0x008014d8 0x00802008 0xafffffa8 0x00801190
user/libs/syscall.c:40: sys_fork+19
ebp:0xafffff88 eip:0x00800263 args:0x00000000 0x00000000 0x0000000d 0x0000000b
user/libs/ulib.c:15: fork+23
ebp:0xafffffa8 eip:0x00801190 args:0x00000000 0x00000000 0x00000000 0x00800458
user/forktest.c:11: main+63
ebp:0xafffffd8 eip:0x00800458 args:0x00000000 0x00000000 0x00000000 0x00000000
user/libs/umain.c:7: umain+22
alloc_pages: n = 1, nmalloc = 208, nfree = 0, nr_free = 31618 page = 0xc01afbc0
- 后來在答案代碼目錄下執行
sudo make run-forktest
,發現有同樣的問題。這說明原代碼確實有bug。由於目前時間緊缺,而這個bug看上去不容易定位(折騰一個早上沒結果),還是留到日后有時間再研究吧。
練習3: 閱讀分析源代碼,理解進程執行 fork/exec/wait/exit 的實現,以及系統調用的實現(不需要編碼)
題目
請在實驗報告中簡要說明你對 fork/exec/wait/exit函數的分析。並回答如下問題:
請分析fork/exec/wait/exit在實現中是如何影響進程的執行狀態的?
請給出ucore中一個用戶態進程的執行狀態生命周期圖(包括執行狀態,執行狀態之間的變換關系,以及產生變換的事件或函數調用) 。(字符方式畫即可)
執行:make grade。如果所顯示的應用程序檢測都輸出ok,則基本正確。(使用的是qemu-1.0.1)
解答
fork的實現
fork的功能是創建一個新進程,具體地說是創建一個新進程所需的控制信息。我們以用戶程序forktest為例,來分析fork的調用過程。
從用戶態的fork到內核態的do_fork
user/forktest.c的main調用fork來創建新進程,從fork到do_fork的調用過程如下:
fork -> sys_fork(位於user/lib/syscall.c) -> syscall(SYS_fork) -> sys_fork(kern/syscall/syscall.c) -> do_fork
do_fork的實現
-
分配一個進程控制塊,設置其state為UNINIT
-
為內核棧分配2頁的內存空間,並將其地址記錄在進程控制塊的kstack字段中
-
復制父進程的內存空間到新進程
-
為新進程分配pid
-
設置新進程的父進程、子進程等關系信息
-
將新進程添加到進程鏈表proc_list和哈希表hash_list中
-
設置新進程的state為RUNNABLE,從而將其喚醒。
exec的實現
exec的功能是在已經存在的進程的上下文中運行新的可執行文件,替換先前的可執行文件。在ucore中exec對應的函數是do_execve。
-
do_execve首先檢查用戶態虛擬內存空間是否合法,如果合法且目前只有當前進程占用,則釋放虛擬內存空間,包括取消虛擬內存到物理內存的映射,釋放vma,mm及頁目錄表占用的物理頁等。
-
調用load_icode函數來加載應用程序
-
重新設置當前進程的名字,然后返回
wait的實現
wait的功能是等待子進程結束,從而釋放子進程占用的資源。在ucore中wait對應的函數是do_wait。
-
遍歷進程鏈表proc_list,根據輸入參數尋找指定pid或任意pid的子進程,如果沒找到,直接返回錯誤信息。
-
如果找到子進程,而且其狀態為ZOMBIE,則釋放子進程占用的資源,然后返回。
-
如果找到子進程,但狀態不為ZOMBIE,則將當前進程的state設置為SLEEPING、wait_state設置為WT_CHILD,然后調用schedule函數,從而進入等待狀態。等再次被喚醒后,重復尋找狀態為ZOMBIE的子進程。
exit的實現
exit的功能是釋放進程占用的資源並結束運行進程。在ucore中exit對應的函數是do_exit。
-
釋放頁表項記錄的物理內存,以及mm結構、vma結構、頁目錄表占用的內存。
-
將自己的state設置為ZOMBIE,然后喚醒父進程,並調用schedule函數,等待父進程回收剩下的資源,最終徹底結束子進程。
系統調用的實現
ucore_os_docs在lab5中已經詳細介紹了系統調用的實現,另外在我的ucore源碼分析 lab1中也有分析。簡單來說,ucore實現系統調用分為以下幾步:
- 在idt_init函數中初始化系統調用對應的中斷描述符。
- 在user/libs/syscall.c中封裝了syscall接口,簡化應用程序訪問系統調用的復雜性。
- 在kernel/syscall/syscall.c中用函數數組來存儲各系統調用對應的處理函數的地址,並封裝了syscall接口,用於根據系統調用號索引函數數組,找到對應的處理函數來運行。
畫出userproc的執行狀態生命周期圖
(alloc_proc) (wakeup_proc)
---------------> NEW ----------------> READY
|
|
| (proc_run)
|
(do_wait) (do_exit) V
EXIT <---------- ZOMBIE <-------------- RUNNING
擴展練習 Challenge :實現 Copy on Write (COW) 機制(待完成)
題目
給出實現源碼,測試用例和設計報告(包括在cow情況下的各種狀態轉換(類似有限狀態自動機)的說明)。
這個擴展練習涉及到本實驗和上一個實驗“虛擬內存管理”。在ucore操作系統中,當一個用戶父進程創建自己的子進程時,父進程會把其申請的用戶空間設置為只讀,子進程可共享父進程占用的用戶內存空間中的頁面(這就是一個共享的資源)。當其中任何一個進程修改此用戶內存空間中的某頁面時,ucore會通過page fault異常獲知該操作,並完成拷貝內存頁面,使得兩個進程都有各自的內存頁面。這樣一個進程所做的修改不會被另外一個進程可見了。請在ucore中實現這樣的COW機制。
由於COW實現比較復雜,容易引入bug,請參考 https://dirtycow.ninja/ 看看能否在ucore的COW實現中模擬這個錯誤和解決方案。需要有解釋。
這是一個big challenge.