全部的實驗報告將會在 Github 同步更新,更多內容請移步至Github:https://github.com/AngelKitty/review_the_national_post-graduate_entrance_examination/blob/master/books_and_notes/professional_courses/operating_system/sources/ucore_os_lab/docs/lab_report/git
lab5
會依賴 lab1~lab4
,咱們須要把作的 lab1~lab4
的代碼填到 lab5
中缺失的位置上面。練習 0 就是一個工具的利用。這裏我使用的是 Linux
下的系統已預裝好的 Meld Diff Viewer
工具。和 lab4
操做流程同樣,咱們只須要將已經完成的 lab1~lab4
與待完成的 lab5
(因爲 lab5
是基於 lab1~lab4
基礎上完成的,因此這裏只須要導入 lab4
)分別導入進來,而後點擊 compare
就好了。github
而後軟件就會自動分析兩份代碼的不一樣,而後就一個個比較比較複製過去就好了,在軟件裏面是能夠支持打開對比複製了,點擊 Copy Right
便可。固然 bin
目錄和 obj
目錄下都是 make
生成的,就不用複製了,其餘須要修改的地方主要有如下七個文件,經過對比複製完成便可:數據結構
kdebug.c trap.c default_pmm.c pmm.c swap_fifo.c vmm.c proc.c
根據試驗要求,咱們須要對部分代碼進行改進,這裏講須要改進的地方的代碼和說明羅列以下:併發
alloc_proc
函數中,額外對進程控制塊中新增長的 wait_state, cptr, yptr, optr
成員變量進行初始化;咱們在原來的實驗基礎上,新增了 2 行代碼:app
proc->wait_state = 0;//PCB 進程控制塊中新增的條目,初始化進程等待狀態 proc->cptr = proc->optr = proc->yptr = NULL;//進程相關指針初始化
這兩行代碼主要是初始化進程等待狀態、和進程的相關指針,例如父進程、子進程、同胞等等。新增的幾個 proc 指針給出相關的解釋以下:框架
parent: proc->parent (proc is children) children: proc->cptr (proc is parent) older sibling: proc->optr (proc is younger sibling) younger sibling: proc->yptr (proc is older sibling)
由於這裏涉及到了用戶進程,天然須要涉及到調度的問題,因此進程等待狀態和各類指針須要被初始化。編輯器
因此改進後的 alloc_proc
函數以下:ide
static struct proc_struct *alloc_proc(void) { struct proc_struct *proc = kmalloc(sizeof(struct proc_struct)); if (proc != NULL) { proc->state = PROC_UNINIT; //設置進程爲未初始化狀態 proc->pid = -1; //未初始化的的進程id爲-1 proc->runs = 0; //初始化時間片 proc->kstack = 0; //內存棧的地址 proc->need_resched = 0; //是否須要調度設爲不須要 proc->parent = NULL; //父節點設爲空 proc->mm = NULL; //虛擬內存設爲空 memset(&(proc->context), 0, sizeof(struct context));//上下文的初始化 proc->tf = NULL; //中斷幀指針置爲空 proc->cr3 = boot_cr3; //頁目錄設爲內核頁目錄表的基址 proc->flags = 0; //標誌位 memset(proc->name, 0, PROC_NAME_LEN);//進程名 proc->wait_state = 0;//PCB 進程控制塊中新增的條目,初始化進程等待狀態 proc->cptr = proc->optr = proc->yptr = NULL;//進程相關指針初始化 } return proc; }
咱們在原來的實驗基礎上,新增了 2 行代碼:函數
assert(current->wait_state == 0); //確保當前進程正在等待 set_links(proc); //將原來簡單的計數改爲來執行 set_links 函數,從而實現設置進程的相關連接
第一行是爲了肯定當前的進程正在等待,咱們在 alloc_proc 中初始化 wait_state 爲0。第二行是將原來的計數換成了執行一個 set_links
函數,由於要涉及到進程的調度,因此簡單的計數確定是不行的。工具
咱們能夠看看 set_links 函數:
static void set_links(struct proc_struct *proc) { list_add(&proc_list,&(proc->list_link));//進程加入進程鏈表 proc->yptr = NULL; //當前進程的 younger sibling 爲空 if ((proc->optr = proc->parent->cptr) != NULL) { proc->optr->yptr = proc; //當前進程的 older sibling 爲當前進程 } proc->parent->cptr = proc; //父進程的子進程爲當前進程 nr_process ++; //進程數加一 }
能夠看出,set_links 函數的做用是設置當前進程的 process relations。
因此改進後的 do_fork
函數以下:
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) { //分配進程數大於 4096,返回 goto fork_out; //返回 } ret = -E_NO_MEM; //因內存不足而分配失敗 if ((proc = alloc_proc()) == NULL) { //調用 alloc_proc() 函數申請內存塊,若是失敗,直接返回處理 goto fork_out;//返回 } proc->parent = current; //將子進程的父節點設置爲當前進程 assert(current->wait_state == 0); //確保當前進程正在等待 if (setup_kstack(proc) != 0) { //調用 setup_stack() 函數爲進程分配一個內核棧 goto bad_fork_cleanup_proc; //返回 } if (copy_mm(clone_flags, proc) != 0) { //調用 copy_mm() 函數複製父進程的內存信息到子進程 goto bad_fork_cleanup_kstack; //返回 } copy_thread(proc, stack, tf); //調用 copy_thread() 函數複製父進程的中斷幀和上下文信息 //將新進程添加到進程的 hash 列表中 bool intr_flag; local_intr_save(intr_flag); //屏蔽中斷,intr_flag 置爲 1 { proc->pid = get_pid(); //獲取當前進程 PID hash_proc(proc); //創建 hash 映射 set_links(proc);//將原來簡單的計數改爲來執行set_links函數,從而實現設置進程的相關連接 } local_intr_restore(intr_flag); //恢復中斷 wakeup_proc(proc); //一切就緒,喚醒子進程 ret = proc->pid; //返回子進程的 pid fork_out: //已分配進程數大於 4096 return ret; bad_fork_cleanup_kstack: //分配內核棧失敗 put_kstack(proc); bad_fork_cleanup_proc: kfree(proc); goto fork_out; }
咱們在原來的實驗基礎上,新增了 1 行代碼:
SETGATE(idt[T_SYSCALL], 1, GD_KTEXT, __vectors[T_SYSCALL], DPL_USER);//這裏主要是設置相應的中斷門
因此改進後的 idt_init
函數以下:
void idt_init(void) { extern uintptr_t __vectors[]; int i; for (i = 0; i < sizeof(idt) / sizeof(struct gatedesc); i ++) { SETGATE(idt[i], 0, GD_KTEXT, __vectors[i], DPL_KERNEL); } SETGATE(idt[T_SYSCALL], 1, GD_KTEXT, __vectors[T_SYSCALL], DPL_USER); //設置相應的中斷門 lidt(&idt_pd); }
設置一個特定中斷號的中斷門,專門用於用戶進程訪問系統調用。在上述代碼中,能夠看到在執行加載中斷描述符表 lidt 指令前,專門設置了一個特殊的中斷描述符 idt[T_SYSCALL],它的特權級設置爲 DPL_USER,中斷向量處理地址在 __vectors[T_SYSCALL]
處。這樣創建好這個中斷描述符後,一旦用戶進程執行 INT T_SYSCALL 後,因爲此中斷容許用戶態進程產生(它的特權級設置爲 DPL_USER),因此 CPU 就會從用戶態切換到內核態,保存相關寄存器,並跳轉到 __vectors[T_SYSCALL]
處開始執行,造成以下執行路徑:
vector128(vectors.S)--\> \_\_alltraps(trapentry.S)--\>trap(trap.c)--\>trap\_dispatch(trap.c)----\>syscall(syscall.c)-
在 syscall 中,根據系統調用號來完成不一樣的系統調用服務。
咱們在原來的實驗基礎上,新增了 1 行代碼:
current->need_resched = 1;//時間片用完設置爲須要調度
這裏主要是將時間片設置爲須要調度,說明當前進程的時間片已經用完了。
因此改進後的 trap_dispatch
函數以下:
ticks ++; if (ticks % TICK_NUM == 0) { assert(current != NULL); current->need_resched = 1;//時間片用完設置爲須要調度 }
根據實驗說明書,咱們須要完善的函數是 load_icode 函數。
這裏介紹下這個函數的功能:load_icode 函數主要用來被 do_execve 調用,將執行程序加載到進程空間(執行程序自己已從磁盤讀取到內存中),給用戶進程創建一個可以讓用戶進程正常運行的用戶環境。這涉及到修改頁表、分配用戶棧等工做。
該函數主要完成的工做以下:
簡單的說,該 load_icode 函數的主要工做就是給用戶進程創建一個可以讓用戶進程正常運行的用戶環境。
咱們能夠看看 do_execve 函數:
// do_execve - call exit_mmap(mm)&put_pgdir(mm) to reclaim memory space of current process // - call load_icode to setup new memory space accroding binary prog. 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) { //若是沒有進程再須要此進程所佔用的內存空間 exit_mmap(mm); //釋放進程所佔用戶空間內存和進程頁表自己所佔空間 put_pgdir(mm); mm_destroy(mm); } current->mm = NULL; //把當前進程的 mm 內存管理指針爲空 } int ret; // 加載應用程序執行碼到當前進程的新建立的用戶態虛擬空間中。這裏涉及到讀 ELF 格式的文件,申請內存空間,創建用戶態虛存空間,加載應用程序執行碼等。load_icode 函數完成了整個複雜的工做。 if ((ret = load_icode(binary, size)) != 0) { goto execve_exit; } set_proc_name(current, local_name); return 0; execve_exit: do_exit(ret); panic("already exit: %e.\n", ret); }
而這裏這個 do_execve 函數主要作的工做就是先回收自身所佔用戶空間,而後調用 load_icode,用新的程序覆蓋內存空間,造成一個執行新程序的新進程。
至此,用戶進程的用戶環境已經搭建完畢。此時 initproc 將按產生系統調用的函數調用路徑原路返回,執行中斷返回指令 iret 後,將切換到用戶進程程序的第一條語句位置 _start
處開始執行。
實現過程以下:
load_icode 函數分析: 該函數的功能主要分爲 6 個部分,而咱們須要填寫的是第 6 個部分,就是僞造中斷返回現場,使得系統調用返回以後能夠正確跳轉到須要運行的程序入口,並正常運行;而 1-5 部分則是一系列對用戶內存空間的初始化,這部分將在 LAB8 的編碼實現中具體體現,所以在本 LAB 中暫時不加具體說明;與 LAB1 的 challenge 相似的,第 6 個部分是在進行中斷處理的棧(此時應當是內核棧)上僞造一箇中斷返回現場,使得中斷返回的時候能夠正確地切換到須要的執行程序入口處;在這個部分中須要對 tf 進行設置,不妨經過代碼分析來肯定這個 tf 變量究竟指到什麼位置,該 tf 變量與 current->tf 的數值一致,而 current->tf 是在進行中斷服務里程的 trap 函數中被設置爲當前中斷的中斷幀,也就是說這個 tf 最終指向了當前系統調用 exec 產生的中斷幀處; /* load_icode - load the content of binary program(ELF format) as the new content of current process * @binary: the memory addr of the content of binary program * @size: the size of the content of binary program */ static int load_icode(unsigned char *binary, size_t size) { if (current->mm != NULL) { //當前進程的內存爲空 panic("load_icode: current->mm must be empty.\n"); } int ret = -E_NO_MEM; //記錄錯誤信息:未分配內存 struct mm_struct *mm; //(1) create a new mm for current process if ((mm = mm_create()) == NULL) { //分配內存 goto bad_mm; //分配失敗,返回 } //(2) create a new PDT, and mm->pgdir= kernel virtual addr of PDT if (setup_pgdir(mm) != 0) { //申請一個頁目錄表所需的空間 goto bad_pgdir_cleanup_mm; //申請失敗 } //(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) { //讀取的 ELF 文件不合法 ret = -E_INVAL_ELF; //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) { //當前段大小爲 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; } } //(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); //(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)); //(6) setup trapframe for user environment struct trapframe *tf = current->tf; memset(tf, 0, sizeof(struct trapframe)); /* LAB5:EXERCISE1 YOUR CODE * 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 */ -------------------------------------------------------------------------------------------- 進程切換老是在內核態中發生,當內核選擇一個進程執行的時候,首先切換內核態的上下文(EBX、ECX、EDX、ESI、EDI、ESP、EBP、EIP 八個寄存器)以及內核棧。完成內核態切換以後,內核須要使用 IRET 指令將 trapframe 中的用戶態上下文恢復出來,返回到進程態,在用戶態中執行進程。 * 實現思路: 1. 因爲最終是在用戶態下運行的,因此須要將段寄存器初始化爲用戶態的代碼段、數據段、堆棧段; 2. esp 應當指向先前的步驟中建立的用戶棧的棧頂; 3. eip 應當指向 ELF 可執行文件加載到內存以後的入口處; 4. eflags 中應當初始化爲中斷使能,注意 eflags 的第 1 位是恆爲 1 的; 5. 設置 ret 爲 0,表示正常返回; load_icode 函數須要填寫的部分爲: * 將 trapframe 的代碼段設爲 USER_CS; * 將 trapframe 的數據段、附加段、堆棧段設爲 USER_DS; * 將 trapframe 的棧頂指針設爲 USTACKTOP; * 將 trapframe 的代碼段指針設爲 ELF 的入口地址 elf->e_entry; * 將 trapframe 中 EFLAGS 的 IF 置爲 1。 -------------------------------------------------------------------------------------------- /*code*/ tf->tf_cs = USER_CS; tf->tf_ds = tf->tf_es = tf->tf_ss = USER_DS; tf->tf_esp = USTACKTOP; //0xB0000000 tf->tf_eip = elf->e_entry; tf->tf_eflags = FL_IF; //FL_IF爲中斷打開狀態 ret = 0; -------------------------------------------------------------------------------------------- out: return ret; bad_cleanup_mmap: exit_mmap(mm); bad_elf_cleanup_pgdir: put_pgdir(mm); bad_pgdir_cleanup_mm: mm_destroy(mm); bad_mm: goto out; }
調用流程以下圖所示:
關於 tf_esp
和 tf_eip
的設置,咱們能夠經過閱讀下圖能夠得知。這是一個完整的虛擬內存空間的分佈圖:
/* file_path:kern/mm/memlayout.h */ /* * * Virtual memory map: Permissions * kernel/user * * 4G ------------------> +---------------------------------+ * | | * | Empty Memory (*) | * | | * +---------------------------------+ 0xFB000000 * | Cur. Page Table (Kern, RW) | RW/-- PTSIZE * VPT -----------------> +---------------------------------+ 0xFAC00000 * | Invalid Memory (*) | --/-- * KERNTOP -------------> +---------------------------------+ 0xF8000000 * | | * | Remapped Physical Memory | RW/-- KMEMSIZE * | | * KERNBASE ------------> +---------------------------------+ 0xC0000000 * | Invalid Memory (*) | --/-- * USERTOP -------------> +---------------------------------+ 0xB0000000 * | User stack | * +---------------------------------+ * | | * : : * | ~~~~~~~~~~~~~~~~ | * : : * | | * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ * | User Program & Heap | * UTEXT ---------------> +---------------------------------+ 0x00800000 * | Invalid Memory (*) | --/-- * | - - - - - - - - - - - - - - - | * | User STAB Data (optional) | * USERBASE, USTAB------> +---------------------------------+ 0x00200000 * | Invalid Memory (*) | --/-- * 0 -------------------> +---------------------------------+ 0x00000000 * (*) Note: The kernel ensures that "Invalid Memory" is *never* mapped. * "Empty Memory" is normally unmapped, but user programs may map pages * there if desired. * * */
請在實驗報告中描述當建立一個用戶態進程並加載了應用程序後,CPU 是如何讓這個應用程序最終在用戶態執行起來的。即這個用戶態進程被 ucore 選擇佔用 CPU 執行(RUNNING 態)到具體執行應用程序第一條指令的整個通過。
分析在建立了用戶態進程而且加載了應用程序以後,其佔用 CPU 執行到具體執行應用程序的整個通過:
這個工做的執行是由 do_fork 函數完成,具體是調用 copy_range 函數,而這裏咱們的任務就是補全這個函數。
這個具體的調用過程是由 do_fork 函數調用 copy_mm 函數,而後 copy_mm 函數調用 dup_mmap 函數,最後由這個 dup_mmap 函數調用 copy_range 函數。即 do_fork()---->copy_mm()---->dup_mmap()---->copy_range()
。
咱們回顧一下 do_fork 的執行過程,它完成的工做主要以下:
/* file_path:kern/process/proc.c */ /* 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) { //分配進程數大於 4096,返回 goto fork_out; //返回 } ret = -E_NO_MEM; //因內存不足而分配失敗 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; //將新進程添加到進程的 hash 列表中 local_intr_save(intr_flag); //屏蔽中斷,intr_flag 置爲 1 { proc->pid = get_pid(); //獲取當前進程 PID hash_proc(proc); //創建 hash 映射 list_add(&proc_list,&(proc->list_link));//將進程加入到進程鏈表中 nr_process ++; //進程數加一 } local_intr_restore(intr_flag); //恢復中斷 wakeup_proc(proc); //一切就緒,喚醒新進程 ret = proc->pid; //返回當前進程的 PID fork_out: //已分配進程數大於4096 return ret; bad_fork_cleanup_kstack: //分配內核棧失敗 put_kstack(proc); bad_fork_cleanup_proc: kfree(proc); goto fork_out; }
因爲 do_fork 函數中調用了 copy_mm 函數,這部分是咱們在 lab4 中未實現的部分,咱們能夠看看這部分函數是如何實現的:
/* file_path:kern/process/proc.c */ // copy_mm - process "proc" duplicate OR share process "current"'s mm according clone_flags // - if clone_flags & CLONE_VM, then "share" ; else "duplicate" static int copy_mm(uint32_t clone_flags, struct proc_struct *proc) { struct mm_struct *mm, *oldmm = current->mm; /* current is a kernel thread */ if (oldmm == NULL) { //當前進程地址空間爲 NULL return 0; } if (clone_flags & CLONE_VM) { //能夠共享地址空間 mm = oldmm; //共享地址空間 goto good_mm; } int ret = -E_NO_MEM; if ((mm = mm_create()) == NULL) { //建立地址空間未成功 goto bad_mm; } if (setup_pgdir(mm) != 0) { goto bad_pgdir_cleanup_mm; } lock_mm(oldmm); //打開互斥鎖,避免多個進程同時訪問內存 { ret = dup_mmap(mm, oldmm); //調用 dup_mmap 函數 } unlock_mm(oldmm); //釋放互斥鎖 if (ret != 0) { goto bad_dup_cleanup_mmap; } good_mm: mm_count_inc(mm); //共享地址空間的進程數加一 proc->mm = mm; //複製空間地址 proc->cr3 = PADDR(mm->pgdir); //複製頁表地址 return 0; bad_dup_cleanup_mmap: exit_mmap(mm); put_pgdir(mm); bad_pgdir_cleanup_mm: mm_destroy(mm); bad_mm: return ret; }
因爲 copy_mm 函數調用 dup_mmap 函數,咱們能夠看看這部分函數是如何實現的:
/* file_path:kern/mm/vmm.c */ int dup_mmap(struct mm_struct *to, struct mm_struct *from) { assert(to != NULL && from != NULL); //必須非空 // mmap_list 爲虛擬地址空間的首地址 list_entry_t *list = &(from->mmap_list), *le = list; while ((le = list_prev(le)) != list) { //遍歷全部段 struct vma_struct *vma, *nvma; vma = le2vma(le, list_link); //獲取某一段 nvma = vma_create(vma->vm_start, vma->vm_end, vma->vm_flags); if (nvma == NULL) { return -E_NO_MEM; } insert_vma_struct(to, nvma); //向新進程插入新建立的段 bool share = 0; //調用 copy_range 函數 if (copy_range(to->pgdir, from->pgdir, vma->vm_start, vma->vm_end, share) != 0) { return -E_NO_MEM; } } return 0; }
因爲 dup_mmap 函數調用 copy_range 函數,這部分函數實現以下:
/* file_path:kern/mm/pmm.c */ /* copy_range - copy content of memory (start, end) of one process A to another process B * @to: the addr of process B's Page Directory * @from: the addr of process A's Page Directory * @share: flags to indicate to dup OR share. We just use dup method, so it didn't be used. * * CALL GRAPH: copy_mm-->dup_mmap-->copy_range */ int copy_range(pde_t *to, pde_t *from, uintptr_t start, uintptr_t end, bool share) { assert(start % PGSIZE == 0 && end % PGSIZE == 0); assert(USER_ACCESS(start, end)); // copy content by page unit. do { //call get_pte to find process A's pte according to the addr start pte_t *ptep = get_pte(from, start, 0), *nptep; if (ptep == NULL) { start = ROUNDDOWN(start + PTSIZE, PTSIZE); continue ; } //call get_pte to find process B's pte according to the addr start. If pte is NULL, just alloc a PT if (*ptep & PTE_P) { if ((nptep = get_pte(to, start, 1)) == NULL) { return -E_NO_MEM; } uint32_t perm = (*ptep & PTE_USER); //get page from ptep struct Page *page = pte2page(*ptep); // alloc a page for process B struct Page *npage=alloc_page(); assert(page!=NULL); assert(npage!=NULL); int ret=0; /* LAB5:EXERCISE2 YOUR CODE * 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 */ -------------------------------------------------------------------------------------------- * 實現思路: copy_range 函數的具體執行流程是遍歷父進程指定的某一段內存空間中的每個虛擬頁,若是這個虛擬頁是存在的話,爲子進程對應的同一個地址(可是頁目錄表是不同的,所以不是一個內存空間)也申請分配一個物理頁,而後將前者中的全部內容複製到後者中去,而後爲子進程的這個物理頁和對應的虛擬地址(事實上是線性地址)創建映射關係;而在本練習中須要完成的內容就是內存的複製和映射的創建,具體流程以下: 1. 找到父進程指定的某一物理頁對應的內核虛擬地址; 2. 找到須要拷貝過去的子進程的對應物理頁對應的內核虛擬地址; 3. 將前者的內容拷貝到後者中去; 4. 爲子進程當前分配這一物理頁映射上對應的在子進程虛擬地址空間裏的一個虛擬頁; -------------------------------------------------------------------------------------------- /*code*/ void * kva_src = page2kva(page); // 找到父進程須要複製的物理頁在內核地址空間中的虛擬地址,這是因爲這個函數執行的時候使用的時內核的地址空間 void * kva_dst = page2kva(npage); // 找到子進程須要被填充的物理頁的內核虛擬地址 memcpy(kva_dst, kva_src, PGSIZE); // 將父進程的物理頁的內容複製到子進程中去 ret = page_insert(to, npage, start, perm); // 創建子進程的物理頁與虛擬頁的映射關係 assert(ret == 0); } start += PGSIZE; } while (start != 0 && start < end); return 0; }
請在實驗報告中簡要說明如何設計實現 」Copy on Write 機制「,給出概要設計,鼓勵給出詳細設計。
接下來將說明如何實現 「Copy on Write」 機制,該機制的主要思想爲使得進程執行 fork 系統調用進行復制的時候,父進程不會簡單地將整個內存中的內容複製給子進程,而是暫時共享相同的物理內存頁;而當其中一個進程須要對內存進行修改的時候,再額外建立一個本身私有的物理內存頁,將共享的內容複製過去,而後在本身的內存頁中進行修改;根據上述分析,主要對實驗框架的修改應當主要有兩個部分,一個部分在於進行 fork 操做的時候不直接複製內存,另一個處理在於出現了內存頁訪問異常的時候,會將共享的內存頁複製一份,而後在新的內存頁進行修改,具體的修改部分以下:
上述實現有一個較小的缺陷,在於在 do fork 的時候須要修改全部的 PTE,會有必定的時間效率上的損失;能夠考慮將共享的標記加在 PDE 上,而後一旦訪問了這個 PDE 以後再將標記下傳給對應的 PTE,這樣的話就起到了標記延遲和潛在的標記合併的左右,有利於提高時間效率;
首先咱們能夠羅列下目前 ucore 全部的系統調用,以下表所示:
系統調用名 | 含義 | 具體完成服務的函數 |
---|---|---|
SYS_exit | process exit | do_exit |
SYS_fork | create child process, dup mm | do_fork->wakeup_proc |
SYS_wait | wait process | do_wait |
SYS_exec | after fork, process execute a program | load a program and refresh the mm |
SYS_clone | create child thread | do_fork->wakeup_proc |
SYS_yield | process flag itself need resecheduling | proc->need_sched=1, then scheduler will rescheule this process |
SYS_sleep | process sleep | do_sleep |
SYS_kill | kill process | do_kill->proc->flags |= PF_EXITING->wakeup_proc->do_wait->do_exit |
SYS_getpid | get the process's pid |
通常來講,用戶進程只能執行通常的指令,沒法執行特權指令。採用系統調用機制爲用戶進程提供一個得到操做系統服務的統一接口層,簡化用戶進程的實現。
根據以前的分析,應用程序調用的 exit/fork/wait/getpid 等庫函數最終都會調用 syscall 函數,只是調用的參數不一樣而已(分別是 SYS_exit / SYS_fork / SYS_wait / SYS_getid )
當應用程序調用系統函數時,通常執行 INT T_SYSCALL 指令後,CPU 根據操做系統創建的系統調用中斷描述符,轉入內核態,而後開始了操做系統系統調用的執行過程,在內核函數執行以前,會保留軟件執行系統調用前的執行現場,而後保存當前進程的 tf 結構體中,以後操做系統就能夠開始完成具體的系統調用服務,完成服務後,調用 IRET 返回用戶態,並恢復現場。這樣整個系統調用就執行完畢了。
接下來對 fork/exec/wait/exit 四個系統調用進行分析:
調用過程爲:fork->SYS_fork->do_fork+wakeup_proc
首先當程序執行 fork 時,fork 使用了系統調用 SYS_fork,而系統調用 SYS_fork 則主要是由 do_fork 和 wakeup_proc 來完成的。do_fork() 完成的工做在練習 2 及 lab4 中已經作過詳細介紹,這裏再簡單說一下,主要是完成了如下工做:
而 wakeup_proc 函數主要是將進程的狀態設置爲等待,即 proc->wait_state = 0。
調用過程爲:SYS_exec->do_execve
當應用程序執行的時候,會調用 SYS_exec 系統調用,而當 ucore 收到此係統調用的時候,則會使用 do_execve() 函數來實現,所以這裏咱們主要介紹 do_execve() 函數的功能,函數主要時完成用戶進程的建立工做,同時使用戶進程進入執行。
主要工做以下:
調用過程爲:SYS_wait->do_wait
咱們能夠看看 do_wait 函數的實現過程:
/* file_path:kern/process/proc.c */ // do_wait - wait one OR any children with PROC_ZOMBIE state, and free memory space of kernel stack // - proc struct of this child. // NOTE: only after do_wait function, all resources of the child proces are free. 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; repeat: haskid = 0; //若是pid!=0,則找到進程id爲pid的處於退出狀態的子進程 if (pid != 0) { proc = find_proc(pid); if (proc != NULL && proc->parent == current) { haskid = 1; if (proc->state == PROC_ZOMBIE) { goto found; //找到進程 } } } else { //若是pid==0,則隨意找一個處於退出狀態的子進程 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; //釋放子進程的全部資源 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);//將子進程從hash_list中刪除 remove_links(proc);//將子進程從proc_list中刪除 } local_intr_restore(intr_flag); put_kstack(proc); //釋放子進程的內核堆棧 kfree(proc); //釋放子進程的進程控制塊 return 0; }
當執行 wait 功能的時候,會調用系統調用 SYS_wait,而該系統調用的功能則主要由 do_wait 函數實現,主要工做就是父進程如何完成對子進程的最後回收工做,具體的功能實現以下:
調用過程爲:SYS_exit->exit
咱們能夠看看 do_exit 函數的實現過程:
/* file_path:kern/process/proc.c */ // do_exit - called by sys_exit // 1. call exit_mmap & put_pgdir & mm_destroy to free the almost all memory space of process // 2. set process' state as PROC_ZOMBIE, then call wakeup_proc(parent) to ask parent reclaim itself. // 3. call scheduler to switch to other process 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){ exit_mmap(mm); /*若是沒有其餘進程共享這個內存釋放current->mm->vma鏈表中每一個vma描述的進程合法空間中實際分配的內存,而後把對應的頁表項內容清空,最後還把頁表所佔用的空間釋放並把對應的頁目錄表項清空*/ put_pgdir(mm); //釋放頁目錄佔用的內存 mm_destroy(mm); //釋放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); { proc = current->parent; if (proc->wait_state == WT_CHILD) { wakeup_proc(proc); //若是父進程在等待子進程,則喚醒 } while (current->cptr != NULL) { /*若是當前進程還有子進程,則須要把這些子進程的父進程指針設置爲內核線程initproc,且各個子進程指針須要插入到initproc的子進程鏈表中。若是某個子進程的執行狀態是PROC_ZOMBIE,則須要喚醒initproc來完成對此子進程的最後回收工做。*/ 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); schedule(); //選擇新的進程執行 panic("do_exit will not return!! %d.\n", current->pid); }
當執行 exit 功能的時候,會調用系統調用 SYS_exit,而該系統調用的功能主要是由 do_exit 函數實現。具體過程以下:
因此說該函數的功能簡單的說就是,回收當前進程所佔的大部份內存資源,並通知父進程完成最後的回收工做。
請分析 fork/exec/wait/exit 在實現中是如何影響進程的執行狀態的?
請給出 ucore 中一個用戶態進程的執行狀態生命週期圖(包執行狀態,執行狀態之間的變換關係,以及產生變換的事件或函數調用)。(字符方式畫便可)
首先,咱們梳理一下流程:
最終,咱們能夠畫出執行狀態圖以下所示:
最終的實驗結果以下圖所示:
若是 make grade 沒法滿分,嘗試註釋掉 tools/grade.sh 的 221 行到 233 行(在前面加上「#」)。
這裏咱們選用古老的編輯器 Vim,具體操做過程以下:
:221
跳轉至 221 行;在 vmm.c 中將 dup_mmap 中的 share 變量的值改成 1,啓用共享:
int dup_mmap(struct mm_struct *to, struct mm_struct *from) { ... bool share = 1; ... }
在 pmm.c 中爲 copy_range 添加對共享的處理,若是 share 爲 1,那麼將子進程的頁面映射到父進程的頁面。因爲兩個進程共享一個頁面以後,不管任何一個進程修改頁面,都會影響另一個頁面,因此須要子進程和父進程對於這個共享頁面都保持只讀。
int copy_range(pde_t *to, pde_t *from, uintptr_t start, uintptr_t end, bool share) { ... if (*ptep & PTE_P) { if ((nptep = get_pte(to, start, 1)) == NULL) { return -E_NO_MEM; } uint32_t perm = (*ptep & PTE_USER); //get page from ptep struct Page *page = pte2page(*ptep); assert(page!=NULL); int ret=0; if (share) { // share page page_insert(from, page, start, perm & (~PTE_W)); ret = page_insert(to, page, start, perm & (~PTE_W)); } else { // alloc a page for process B struct Page *npage=alloc_page(); assert(npage!=NULL); uintptr_t src_kvaddr = page2kva(page); uintptr_t dst_kvaddr = page2kva(npage); memcpy(dst_kvaddr, src_kvaddr, PGSIZE); ret = page_insert(to, npage, start, perm); } assert(ret == 0); } ... return 0; }
當程序嘗試修改只讀的內存頁面的時候,將觸發Page Fault中斷,在錯誤代碼中 P=1,W/R=1[OSDev]。所以,當錯誤代碼最低兩位都爲 1 的時候,說明進程訪問了共享的頁面,內核須要從新分配頁面、拷貝頁面內容、創建映射關係:
int do_pgfault(struct mm_struct *mm, uint32_t error_code, uintptr_t addr) { ... if (*ptep == 0) { ... } else if (error_code & 3 == 3) { // copy on write struct Page *page = pte2page(*ptep); struct Page *npage = pgdir_alloc_page(mm->pgdir, addr, perm); uintptr_t src_kvaddr = page2kva(page); uintptr_t dst_kvaddr = page2kva(npage); memcpy(dst_kvaddr, src_kvaddr, PGSIZE); } else { ... } ... }