全部的實驗報告將會在 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/linux
lab4
會依賴 lab一、lab2
和 lab3
,咱們須要把作的 lab一、lab2
和 lab3
的代碼填到 lab4
中缺失的位置上面。練習 0 就是一個工具的利用。這裏我使用的是 Linux
下的系統已預裝好的 Meld Diff Viewer
工具。和 lab3
操做流程同樣,咱們只須要將已經完成的 lab一、lab2
和 lab3
與待完成的 lab4
(因爲 lab4
是基於 lab一、lab二、lab3
基礎上完成的,因此這裏只須要導入 lab3
)分別導入進來,而後點擊 compare
就好了。git
而後軟件就會自動分析兩份代碼的不一樣,而後就一個個比較比較複製過去就好了,在軟件裏面是能夠支持打開對比複製了,點擊 Copy Right
便可。固然 bin
目錄和 obj
目錄下都是 make
生成的,就不用複製了,其餘須要修改的地方主要有如下五個文件,經過對比複製完成便可:github
default_pmm.c pmm.c swap_fifo.c vmm.c trap.c
內核線程是一種特殊的進程,內核線程與用戶進程的區別有兩個:算法
ucore
內核內存空間,不需爲每一個內核線程維護單獨的內存空間,而用戶進程須要維護各自的用戶內存空間。這裏主要是從 kern_init
函數的物理內存管理初始化開始的,按照函數的次序作了一個簡單的總結:編程
操做系統是以進程爲中心設計的,因此其首要任務是爲進程創建檔案,進程檔案用於表示、標識或描述進程,即進程控制塊。這裏須要完成的就是一個進程控制塊的初始化。數據結構
而這裏咱們分配的是一個內核線程的 PCB,它一般只是內核中的一小段代碼或者函數,沒有用戶空間。而因爲在操做系統啓動後,已經對整個核心內存空間進行了管理,經過設置頁表創建了核心虛擬空間(即 boot_cr3 指向的二級頁表描述的空間)。因此內核中的全部線程都不須要再創建各自的頁表,只需共享這個核心虛擬空間就能夠訪問整個物理內存了。框架
首先在 kern/process/proc.h
中定義了 PCB
,即進程控制塊的結構體 proc_struct
,以下:less
struct proc_struct { //進程控制塊 enum proc_state state; //進程狀態 int pid; //進程ID int runs; //運行時間 uintptr_t kstack; //內核棧位置 volatile bool need_resched; //是否須要調度 struct proc_struct *parent; //父進程 struct mm_struct *mm; //進程的虛擬內存 struct context context; //進程上下文 struct trapframe *tf; //當前中斷幀的指針 uintptr_t cr3; //當前頁表地址 uint32_t flags; //進程 char name[PROC_NAME_LEN + 1];//進程名字 list_entry_t list_link; //進程鏈表 list_entry_t hash_link; //進程哈希表 };
這裏簡單介紹下各個參數:ide
而這裏要求咱們完成一個 alloc_proc 函數來負責分配一個新的 struct proc_struct 結構,根據提示咱們須要初始化一些變量,具體的代碼以下:函數
* 實現思路: 該函數的具體含義爲建立一個新的進程控制塊,而且對控制塊中的全部成員變量進行初始化,根據實驗指導書中的要求,除了指定的若干個成員變量以外,其餘成員變量均初始化爲0,取特殊值的成員變量以下所示: proc->state = PROC_UNINIT; proc->pid = -1; proc->cr3 = boot_cr3; // 因爲是內核線程,共用一個虛擬內存空間 對於其餘成員變量中佔用內存空間較大的,能夠考慮使用 memset 函數進行初始化。 -------------------------------------------------------------------------------------------- /*code*/ 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);//進程名 } return proc; }
請說明 proc_struct 中
struct context context
和struct trapframe *tf
成員變量含義和在本實驗中的做用是啥?(提示經過看代碼和編程調試能夠判斷出來)
首先不妨查看 struct context 結構體的定義,能夠發如今結構體中存儲這除了 eax 以外的全部通用寄存器以及 eip 的數值,這就提示咱們這個線程控制塊中的 context 頗有多是保存的線程運行的上下文信息; 接下來使用 find grep 命令查找在 ucore 中對 context 成員變量進行了設置的代碼,總共能夠發現兩處,分別爲 Swtich.S 和 proc.c 中的 copy_thread 函數中,在其餘部分均沒有發現對 context 的引用和定義(除了初始化);那麼根據 Swtich 中代碼的語義,能夠肯定 context 變量的意義就在於內核線程之間進行切換的時候,將原先的線程運行的上下文保存下來這一做用。 那麼爲何沒有對 eax 進行保存呢?注意到在進行切換的時候調用了 switch_to 這一個函數,也就是說這個函數的裏面纔是線程之間切換的切換點,而在這個函數裏面,因爲 eax 是一個 caller-save 寄存器,而且在函數裏 eax 的數值一直均可以在棧上找到對應,所以沒有比較對其進行保存。 在 context 中保存着各類寄存器的內容,主要保存了前一個進程的現場(各個寄存器的狀態),是進程切換的上下文內容,這是爲了保存進程上下文,用於進程切換,爲進程調度作準備。 在 ucore 中,全部的進程在內核中也是相對獨立的。使用 context 保存寄存器的目的就在於在內核態中可以進行上下文之間的切換。實際利用 context 進行上下文切換的函數是在 kern/process/switch.S 中定義 switch_to 函數。 -------------------------------------------------------------------------------------------- /*code*/ struct context { uint32_t eip; uint32_t esp; uint32_t ebx; uint32_t ecx; uint32_t edx; uint32_t esi; uint32_t edi; uint32_t ebp; }; -------------------------------------------------------------------------------------------- 接下來一樣在代碼中尋找對 tf 變量進行了定義的地方,最後能夠發如今 copy_thread 函數中對 tf 進行了設置,可是值得注意的是,在這個函數中,同時對 context 變量的 esp 和 eip 進行了設置。前者設置爲 tf 變量的地址,後者設置爲 forkret 這個函數的指針。接下來觀察 forkret 函數,發現這個函數最終調用了 __trapret 進行中斷返回,這樣的話,tf 變量的做用就變得清晰起來了。 tf 變量的做用在於在構造出了新的線程的時候,若是要將控制權交給這個線程,是使用中斷返回的方式進行的(跟lab1中切換特權級相似的技巧),所以須要構造出一個僞造的中斷返回現場,也就是 trapframe,使得能夠正確地將控制權轉交給新的線程;具體切換到新的線程的作法爲: * 調用switch_to函數。 * 而後在該函數中進行函數返回,直接跳轉到 forkret 函數。 * 最終進行中斷返回函數 __trapret,以後即可以根據 tf 中構造的中斷返回地址,切換到新的線程了。 trapframe 保存着用於特權級轉換的棧 esp 寄存器,當進程發生特權級轉換的時候,中斷幀記錄了進入中斷時任務的上下文。當退出中斷時恢復環境。 tf 是一箇中斷幀的指針,老是指向內核棧的某個位置: * 當進程從用戶空間跳到內核空間時,中斷幀記錄了進程在被中斷前的狀態。 * 當內核須要跳回用戶空間時,須要調整中斷幀以恢復讓進程繼續執行的各寄存器值。 * 除此以外,ucore 內核容許嵌套中斷,所以爲了保證嵌套中斷髮生時 tf 老是可以指向當前的 trapframe,ucore 在內核棧上維護了 tf 的鏈。 -------------------------------------------------------------------------------------------- /*code*/ struct trapframe { struct pushregs { uint32_t reg_edi; uint32_t reg_esi; uint32_t reg_ebp; uint32_t reg_oesp; /* Useless */ uint32_t reg_ebx; uint32_t reg_edx; uint32_t reg_ecx; uint32_t reg_eax; }; uint16_t tf_gs; uint16_t tf_padding0; uint16_t tf_fs; uint16_t tf_padding1; uint16_t tf_es; uint16_t tf_padding2; uint16_t tf_ds; uint16_t tf_padding3; uint32_t tf_trapno; /* below here defined by x86 hardware */ uint32_t tf_err; uintptr_t tf_eip; uint16_t tf_cs; uint16_t tf_padding4; uint32_t tf_eflags; /* below here only when crossing rings, such as from user to kernel */ uintptr_t tf_esp; uint16_t tf_ss; uint16_t tf_padding5; } __attribute__((packed));
根據這張圖能夠看出,內核態和用戶態的轉換首先是留下 SS 和 ESP 的位置,而後調用中斷,改中斷棧裏面的內容, 而後退出中斷的時候跳到內核態中,最後將 ebp 賦給 esp 修復 esp 的位置。
alloc_proc 實質只是找到了一小塊內存用以記錄進程的必要信息,並無實際分配這些資源,而練習 2 完成的 do_fork 纔是真正完成了資源分配的工做,固然,do_fork 也只是建立當前內核線程的一個副本,它們的執行上下文、代碼、數據都同樣,可是存儲位置不一樣。
根據提示及閱讀源碼可知,它完成的工做主要以下:
實現過程以下:
* 實現思路: 該函數的語義爲爲內核線程建立新的線程控制塊,而且對控制塊中的每一個成員變量進行正確的設置,使得以後能夠正確切換到對應的線程中執行。 proc = alloc_proc(); // 爲要建立的新的線程分配線程控制塊的空間 if (proc == NULL) goto fork_out; // 判斷是否分配到內存空間 assert(setup_kstack(proc) == 0); // 爲新的線程設置棧,在本實驗中,每一個線程的棧的大小初始均爲 2 個 Page,即 8KB assert(copy_mm(clone_flags, proc) == 0); // 對虛擬內存空間進行拷貝,因爲在本實驗中,內核線程之間共享一個虛擬內存空間,所以實際上該函數不須要進行任何操做 copy_thread(proc, stack, tf); // 在新建立的內核線程的棧上面設置僞造好的中端幀,便於後文中利用 iret 命令將控制權轉移給新的線程 proc->pid = get_pid(); // 爲新的線程建立 pid hash_proc(proc); // 將線程放入使用 hash 組織的鏈表中,便於加速之後對某個指定的線程的查找 nr_process ++; // 將全局線程的數目加 1 list_add(&proc_list, &proc->list_link); // 將線程加入到全部線程的鏈表中,便於進行調度 wakeup_proc(proc); // 喚醒該線程,即將該線程的狀態設置爲能夠運行 ret = proc->pid; // 返回新線程的pid -------------------------------------------------------------------------------------------- /*code*/ /* 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; //因內存不足而分配失敗 //LAB4:EXERCISE2 YOUR CODE /* * Some Useful MACROs, Functions and DEFINEs, you can use them in below implementation. * MACROs or Functions: * alloc_proc: create a proc struct and init fields (lab4:exercise1) * setup_kstack: alloc pages with size KSTACKPAGE as process kernel stack * copy_mm: process "proc" duplicate OR share process "current"'s mm according clone_flags * if clone_flags & CLONE_VM, then "share" ; else "duplicate" * copy_thread: setup the trapframe on the process's kernel stack top and * setup the kernel entry point and stack of process * hash_proc: add proc into proc hash_list * get_pid: alloc a unique pid for process * wakeup_proc: set proc->state = PROC_RUNNABLE * VARIABLES: * proc_list: the process set's list * nr_process: the number of process set */ // 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 if ((proc = alloc_proc()) == NULL) { //調用 alloc_proc() 函數申請內存塊,若是失敗,直接返回處理 goto fork_out;//返回 } proc->parent = current; //將子進程的父節點設置爲當前進程 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 映射 list_add(&proc_list, &(proc->list_link)); //將進程加入到進程的鏈表中 nr_process ++; //進程數加 1 } 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; }
請說明 ucore 是否作到給每一個新 fork 的線程一個惟一的 id?請說明你的分析和理由。
能夠。保證每一個 fork 的線程給的 ID 惟一,調用的 get_pid() 函數,每次都從進程控制塊鏈表中找到合適的 ID。線程的 PID 由 get_pid
函數產生,該函數中包含了兩個靜態變量 last_pid
以及 next_safe
。last_pid
變量保存上一次分配的 PID,而 next_safe 和 last_pid 一塊兒表示一段可使用的 PID 取值範圍
接下來不妨分析該函數的內容: * 在該函數中使用到了兩個靜態的局部變量 next_safe 和 last_pid,根據命名推測,在每次進入 get_pid 函數的時候,這兩個變量的數值之間的取值均是合法的 pid(也就是說沒有被使用過),這樣的話,若是有嚴格的 next_safe > last_pid + 1,那麼久能夠直接取 last_pid + 1 做爲新的 pid(須要 last_pid 沒有超出 MAX_PID 從而變成 1); * 若是在進入函數的時候,這兩個變量以後沒有合法的取值,也就是說 next_safe > last_pid + 1 不成立,那麼進入循環,在循環之中首先經過 if (proc->pid == last_pid) 這一分支確保了不存在任何進程的 pid 與 last_pid 重合,而後再經過 if (proc->pid > last_pid && next_safe > proc->pid) 這一判斷語句保證了不存在任何已經存在的 pid 知足:last_pid < pid < next_safe,這樣就確保了最後可以找到這麼一個知足條件的區間,得到合法的 pid; * 之因此在該函數中使用瞭如此曲折的方法,維護一個合法的 pid 的區間,是爲了優化時間效率,若是簡單的暴力的話,每次須要枚舉全部的 pid,而且遍歷全部的線程,這就使得時間代價過大,而且不一樣的調用 get_pid 函數的時候不能利用到先前調用這個函數的中間結果; -------------------------------------------------------------------------------------------- /*code*/ // 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; }
這裏我從 proc_init() 函數開始提及的。因爲以前的 proc_init() 函數已經完成了 idleproc 內核線程和 initproc 內核線程的初始化。因此在 kern_init() 最後,它經過 cpu_idle() 喚醒了 0 號 idle 進程,在分析 proc_run 函數以前,咱們先分析調度函數 schedule() 。
schedule() 代碼以下:
/* 宏定義: #define le2proc(le, member) \ to_struct((le), struct proc_struct, member) */ void schedule(void) { bool intr_flag; //定義中斷變量 list_entry_t *le, *last; //當前list,下一list struct proc_struct *next = NULL; //下一進程 local_intr_save(intr_flag); //中斷禁止函數 { current->need_resched = 0; //設置當前進程不須要調度 //last是不是idle進程(第一個建立的進程),若是是,則從表頭開始搜索 //不然獲取下一鏈表 last = (current == idleproc) ? &proc_list : &(current->list_link); le = last; do { //一直循環,直到找到能夠調度的進程 if ((le = list_next(le)) != &proc_list) { next = le2proc(le, list_link);//獲取下一進程 if (next->state == PROC_RUNNABLE) { break; //找到一個能夠調度的進程,break } } } while (le != last); //循環查找整個鏈表 if (next == NULL || next->state != PROC_RUNNABLE) { next = idleproc; //未找到能夠調度的進程 } next->runs ++; //運行次數加一 if (next != current) { proc_run(next); //運行新進程,調用proc_run函數 } } local_intr_restore(intr_flag); //容許中斷 }
能夠看到 ucore 實現的是 FIFO 調度算法:
即 schedule
函數經過查找 proc_list 進程隊列,在這裏只能找到一個處於就緒態的 initproc 內核線程。因而經過 proc_run
和進一步的 switch_to 函數完成兩個執行現場的切換。
再分析 switch_to 函數
* 實現思路: switch_to 函數主要完成的是進程的上下文切換,先保存當前寄存器的值,而後再將下一進程的上下文信息保存到對於寄存器中。 1. 首先,保存前一個進程的執行現場,即 movl 4(%esp), %eax 和 popl 0(%eax) 兩行代碼。 2. 而後接下來的七條指令以下: movl %esp, 4(%eax) movl %ebx, 8(%eax) movl %ecx, 12(%eax) movl %edx, 16(%eax) movl %esi, 20(%eax) movl %edi, 24(%eax) movl %ebp, 28(%eax) 這些指令完成了保存前一個進程的其餘 7 個寄存器到 context 中的相應域中。至此前一個進程的執行現場保存完畢。 3. 再日後是恢復向一個進程的執行現場,這其實就是上述保存過程的逆執行過程,即從 context 的高地址的域 ebp 開始,逐一把相關域的值賦值給對應的寄存器。 4. 最後的 pushl 0(%eax) 實際上是把 context 中保存的下一個進程要執行的指令地址 context.eip 放到了堆棧頂,這樣接下來執行最後一條指令 「ret」 時,會把棧頂的內容賦值給 EIP 寄存器,這樣就切換到下一個進程執行了,即當前進程已是下一個進程了,從而完成了進程的切換。 -------------------------------------------------------------------------------------------- /*code*/ switch_to: # switch_to(from, to) # save from's registers movl 4(%esp), %eax #保存from的首地址 popl 0(%eax) #將返回值保存到context的eip movl %esp, 4(%eax) #保存esp的值到context的esp movl %ebx, 8(%eax) #保存ebx的值到context的ebx movl %ecx, 12(%eax) #保存ecx的值到context的ecx movl %edx, 16(%eax) #保存edx的值到context的edx movl %esi, 20(%eax) #保存esi的值到context的esi movl %edi, 24(%eax) #保存edi的值到context的edi movl %ebp, 28(%eax) #保存ebp的值到context的ebp # restore to's registers movl 4(%esp), %eax #保存to的首地址到eax movl 28(%eax), %ebp #保存context的ebp到ebp寄存器 movl 24(%eax), %edi #保存context的ebp到ebp寄存器 movl 20(%eax), %esi #保存context的esi到esi寄存器 movl 16(%eax), %edx #保存context的edx到edx寄存器 movl 12(%eax), %ecx #保存context的ecx到ecx寄存器 movl 8(%eax), %ebx #保存context的ebx到ebx寄存器 movl 4(%eax), %esp #保存context的esp到esp寄存器 pushl 0(%eax) #將context的eip壓入棧中 ret
最後分析一下 proc_run 函數
四、由 switch_to函數完成具體的兩個線程的執行現場切換,即切換各個寄存器,當 switch_to 函數執行完「ret」指令後,就切換到 initproc 執行了。
proc_run
的執行過程爲:
如下是對 proc_run 函數的具體分析過程:
* 實現思路: 1. 讓 current 指向 next 內核線程 initproc; 2. 設置任務狀態段 ts 中特權態 0 下的棧頂指針 esp0 爲 next 內核線程 initproc 的內核棧的棧頂,即 next->kstack + KSTACKSIZE ; 3. 設置 CR3 寄存器的值爲 next 內核線程 initproc 的頁目錄表起始地址 next->cr3,這其實是完成進程間的頁表切換; 4. 由 switch_to 函數完成具體的兩個線程的執行現場切換,即切換各個寄存器,當 switch_to 函數執行完 「ret」 指令後,就切換到 initproc 執行了。 * 當前進程/線程 切換到 proc 這個進程/線程 * 注意到在本實驗框架中,惟一調用到這個函數是在線程調度器的 schedule 函數中,也就是能夠推測 proc_run 的語義就是將當前的 CPU 的控制權交給指定的線程; * 能夠看到 proc_run 中首先進行了 TSS 以及 cr3 寄存器的設置,而後調用到了 swtich_to 函數來切換線程,根據上文中對 switch_to 函數的分析能夠知道,在調用該函數以後,首先會恢復要運行的線程的上下文,而後因爲恢復的上下文中已經將返回地址( copy_thread 函數中完成)修改爲了 forkret 函數的地址(若是這個線程是第一運行的話,不然就是切換到這個線程被切換出來的地址),也就是會跳轉到這個函數,最後進一步跳轉到了 __trapsret 函數,調用 iret ,最終將控制權切換到新的線程; -------------------------------------------------------------------------------------------- /*code*/ void proc_run(struct proc_struct *proc) { if (proc != current) { // 判斷須要運行的線程是否已經運行着了 bool intr_flag; struct proc_struct *prev = current, *next = proc; local_intr_save(intr_flag); // 關閉中斷 { current = proc; // 將當前進程換爲 要切換到的進程 // 設置任務狀態段 tss 中的特權級 0 下的 esp0 指針爲 next 內核線程 的內核棧的棧頂 load_esp0(next->kstack + KSTACKSIZE); // 設置 TSS lcr3(next->cr3); // 從新加載 cr3 寄存器(頁目錄表基址) 進行進程間的頁表切換,修改當前的 cr3 寄存器成須要運行線程(進程)的頁目錄表 switch_to(&(prev->context), &(next->context)); // 調用 switch_to 進行上下文的保存與切換,切換到新的線程 } local_intr_restore(intr_flag); } }
在本實驗的執行過程當中,建立且運行了幾個內核線程?
總共建立了兩個內核線程,分別爲:
語句
local_intr_save(intr_flag);....local_intr_restore(intr_flag);
在這裏有何做用?請說明理由。
在進行進程切換的時候,須要避免出現中斷干擾這個過程,因此須要在上下文切換期間清除 IF 位屏蔽中斷,而且在進程恢復執行後恢復 IF 位。
運行結果以下:
經過少許的修改,便可使用實驗2擴展練習實現的 Slub 算法。
void pmm_init(void) { ... kmem_int(); }
爲了使用Slub算法,須要聲明倉庫的指針。
struct kmem_cache_t *vma_cache = NULL; struct kmem_cache_t *mm_cache = NULL;
在虛擬內存初始化時建立倉庫。
void vmm_init(void) { mm_cache = kmem_cache_create("mm", sizeof(struct mm_struct), NULL, NULL); vma_cache = kmem_cache_create("vma", sizeof(struct vma_struct), NULL, NULL); ... }
在 mm_create 和 vma_create 中使用 Slub 算法。
struct mm_struct *mm_create(void) { struct mm_struct *mm = kmem_cache_alloc(mm_cache); ... } struct vma_struct *vma_create(uintptr_t vm_start, uintptr_t vm_end, uint32_t vm_flags) { struct vma_struct *vma = kmem_cache_alloc(vma_cache); ... }
在 mm_destroy 中釋放內存。
void mm_destroy(struct mm_struct *mm) { ... while ((le = list_next(list)) != list) { ... kmem_cache_free(mm_cache, le2vma(le, list_link)); //kfree vma } kmem_cache_free(mm_cache, mm); //kfree mm ... }
聲明倉庫指針。
struct kmem_cache_t *proc_cache = NULL;
在初始化函數中建立倉庫。
void proc_init(void) { ... proc_cache = kmem_cache_create("proc", sizeof(struct proc_struct), NULL, NULL); ... }
在 alloc_proc 中使用 Slub 算法。
static struct proc_struct *alloc_proc(void) { struct proc_struct *proc = kmem_cache_alloc(proc_cache); ... }
本實驗沒有涉及進程結束後 PCB 回收,不須要回收內存。