《ucore lab5》實驗報告


資源

  1. ucore在線實驗指導書
  2. 我的ucore實驗代碼

練習1: 加載應用程序並執行(需要編碼)

題目

do_execv函數調用load_icode(位於kern/process/proc.c中) 來加載並解析一個處於內存中的ELF執行文件格式的應用程序,建立相應的用戶內存空間來放置應用程序的代碼段、數據段等,且要設置好proc_struct結構中的成員變量trapframe中的內容,確保在執行此進程后,能夠從應用程序設定的起始執行地址開始執行。需設置正確的trapframe內容。

請在實驗報告中簡要說明你的設計實現過程。

請在實驗報告中描述當創建一個用戶態進程並加載了應用程序后,CPU是如何讓這個應用程序最終在用戶態執行起來的。即這個用戶態進程被ucore選擇占用CPU執行(RUNNING態)到具體執行應用程序第一條指令的整個經過。

解答

我的設計實現過程

根據注釋的提示設置trapframe的內容即可。

  1. tf_cs設置為用戶態代碼段的段選擇子
  2. tf_ds、tf_es、tf_ss均設置為用戶態數據段的段選擇子
  3. tf_esp設置為用戶棧的棧頂
  4. tf_eip設置為ELF文件的入口e_entry
  5. tf_eflags使能中斷位

用戶態進程從被ucore選擇到執行第一條指令的過程

  1. 內核線程initproc在創建完成用戶態進程userproc后,調用do_wait函數,do_wait函數在確認存在RUNNABLE的子進程后,調用schedule函數。

  2. schedule函數通過調用proc_run來運行新線程,proc_run做了三件事情:

    • 設置userproc的棧指針esp為userproc->kstack + 2 * 4096,即指向userproc申請到的2頁棧空間的棧頂
    • 加載userproc的頁目錄表。用戶態的頁目錄表跟內核態的頁目錄表不同,因此要重新加載頁目錄表
    • 切換進程上下文,然后跳轉到userproc->context.eip指向的函數,即forkret
  3. forkret函數直接調用forkrets函數,forkrets先把棧指針指向userproc->tf的地址,然后跳到__trapret

  4. __trapret先將userproc->tf的內容pop給相應寄存器,然后通過iret指令,跳轉到userproc->tf.tf_eip指向的函數,即kernel_thread_entry

  5. kernel_thread_entry先將edx保存的輸入參數(NULL)壓棧,然后通過call指令,跳轉到ebx指向的函數,即user_main

  6. user_main先打印userproc的pid和name信息,然后調用kernel_execve

  7. kernel_execve執行exec系統調用,CPU檢測到系統調用后,會保存eflags/ss/eip等現場信息,然后根據中斷號查找中斷向量表,進入中斷處理例程。這里要經過一系列的函數跳轉,才真正進入到exec的系統處理函數do_execve中:vector128 -> __alltraps -> trap -> trap_dispatch -> syscall -> sys_exec -> do_execve

  8. do_execve首先檢查用戶態虛擬內存空間是否合法,如果合法且目前只有當前進程占用,則釋放虛擬內存空間,包括取消虛擬內存到物理內存的映射,釋放vma,mm及頁目錄表占用的物理頁等。

  9. 調用load_icode函數來加載應用程序

    • 為用戶進程創建新的mm結構
    • 創建頁目錄表
    • 校驗ELF文件的魔鬼數字是否正確
    • 創建虛擬內存空間,即往mm結構體添加vma結構
    • 分配內存,並拷貝ELF文件的各個program section到新申請的內存上
    • 為BSS section分配內存,並初始化為全0
    • 分配用戶棧內存空間
    • 設置當前用戶進程的mm結構、頁目錄表的地址及加載頁目錄表地址到cr3寄存器
    • 設置當前用戶進程的tf結構
  10. load_icode返回到do_exevce,do_execve設置完當前用戶進程的名字為“exit”后也返回了。這樣一直原路返回到__alltraps函數時,接下來進入__trapret函數

  11. __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執行失敗

問題描述

  1. 在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

調試過程(尚未解決)

  1. 單獨執行sudo make run-forktestsudo make run-forktree,程序跑到后面時均有以下報錯信息:
kernel panic at kern/process/proc.c:851:
    assertion failed: nr_free_pages_store == nr_free_pages()
  1. 調試forktest或forktree,發現在initmain開頭時空閑頁數目nr_free_pages_store = 31827,但在initmain結尾處調用nr_free_pages求到的空閑頁數目為31825,少了2頁。從現象來看發生了內存泄漏。

  2. 初步推測:forktest函數只是多次調用了fork和wait函數,因此內存泄漏應該是執行這兩個函數過程中發生的。接下來分析下整個過程的內存使用。

  3. 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,復制父進程的內存到子進程
  4. wait的內存使用情況:

    • 調用schedule,切換到forktest進程上下文,執行完main函數后,調用do_exit。do_exit分別調用exit_mmap釋放各vma結構的內存及對應的物理頁、put_pgdir釋放頁目錄表占用的內存,mm_destroy釋放mm_struct占用的內存。
    • 調用do_wait -> kfree,釋放proc_struct
  5. 從fork和wait函數看不出問題。后來嘗試修改進程數目,發現內存泄漏大小與進程數目成正比:12個進程,則泄漏1頁;26個進程,則泄漏2頁;64個進程,則泄漏4頁。

  6. 在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
  1. 后來在答案代碼目錄下執行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的實現
  1. 分配一個進程控制塊,設置其state為UNINIT

  2. 為內核棧分配2頁的內存空間,並將其地址記錄在進程控制塊的kstack字段中

  3. 復制父進程的內存空間到新進程

  4. 為新進程分配pid

  5. 設置新進程的父進程、子進程等關系信息

  6. 將新進程添加到進程鏈表proc_list和哈希表hash_list中

  7. 設置新進程的state為RUNNABLE,從而將其喚醒。

exec的實現

exec的功能是在已經存在的進程的上下文中運行新的可執行文件,替換先前的可執行文件。在ucore中exec對應的函數是do_execve。

  1. do_execve首先檢查用戶態虛擬內存空間是否合法,如果合法且目前只有當前進程占用,則釋放虛擬內存空間,包括取消虛擬內存到物理內存的映射,釋放vma,mm及頁目錄表占用的物理頁等。

  2. 調用load_icode函數來加載應用程序

  3. 重新設置當前進程的名字,然后返回

wait的實現

wait的功能是等待子進程結束,從而釋放子進程占用的資源。在ucore中wait對應的函數是do_wait。

  1. 遍歷進程鏈表proc_list,根據輸入參數尋找指定pid或任意pid的子進程,如果沒找到,直接返回錯誤信息。

  2. 如果找到子進程,而且其狀態為ZOMBIE,則釋放子進程占用的資源,然后返回。

  3. 如果找到子進程,但狀態不為ZOMBIE,則將當前進程的state設置為SLEEPING、wait_state設置為WT_CHILD,然后調用schedule函數,從而進入等待狀態。等再次被喚醒后,重復尋找狀態為ZOMBIE的子進程。

exit的實現

exit的功能是釋放進程占用的資源並結束運行進程。在ucore中exit對應的函數是do_exit。

  1. 釋放頁表項記錄的物理內存,以及mm結構、vma結構、頁目錄表占用的內存。

  2. 將自己的state設置為ZOMBIE,然后喚醒父進程,並調用schedule函數,等待父進程回收剩下的資源,最終徹底結束子進程。

系統調用的實現

ucore_os_docs在lab5中已經詳細介紹了系統調用的實現,另外在我的ucore源碼分析 lab1中也有分析。簡單來說,ucore實現系統調用分為以下幾步:

  1. 在idt_init函數中初始化系統調用對應的中斷描述符。
  2. 在user/libs/syscall.c中封裝了syscall接口,簡化應用程序訪問系統調用的復雜性。
  3. 在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.


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM