所有的實驗報告將會在 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/
練習0:填寫已有實驗
lab4
會依賴 lab1、lab2
和 lab3
,我們需要把做的 lab1、lab2
和 lab3
的代碼填到 lab4
中缺失的位置上面。練習 0 就是一個工具的利用。這裏我使用的是 Linux
下的系統已預裝好的 Meld Diff Viewer
工具。和 lab3
操作流程一樣,我們只需要將已經完成的 lab1、lab2
和 lab3
與待完成的 lab4
(由於 lab4
是基於 lab1、lab2、lab3
基礎上完成的,所以這裏只需要導入 lab3
)分別導入進來,然後點擊 compare
就行了。
然後軟件就會自動分析兩份代碼的不同,然後就一個個比較比較複製過去就行了,在軟件裏面是可以支持打開對比複製了,點擊 Copy Right
即可。當然 bin
目錄和 obj
目錄下都是 make
生成的,就不用複製了,其他需要修改的地方主要有以下五個文件,通過對比複製完成即可:
default_pmm.c
pmm.c
swap_fifo.c
vmm.c
trap.c
練習1:分配並初始化一個進程控制塊(需要編碼)
內核線程是一種特殊的進程,內核線程與用戶進程的區別有兩個:
- 內核線程只運行在內核態,而用戶進程會在在用戶態和內核態交替運行;
- 所有內核線程直接使用共同
ucore
內核內存空間,不需爲每個內核線程維護單獨的內存空間,而用戶進程需要維護各自的用戶內存空間。
這裏主要是從 kern_init
函數的物理內存管理初始化開始的,按照函數的次序做了一個簡單的總結:
- 1、pmm_init()
- (1) 初始化物理內存管理器。
- (2) 初始化空閒頁,主要是初始化物理頁的 Page 數據結構,以及建立頁目錄表和頁表。
- (3) 初始化 boot_cr3 使之指向了 ucore 內核虛擬空間的頁目錄表首地址,即一級頁表的起始物理地址。
- (4) 初始化第一個頁表 boot_pgdir。
- (5) 初始化 GDT,即全局描述符表。
- 2、pic_init()
- 初始化 8259A 中斷控制器
- 3、idt_init()
- 初始化 IDT,即中斷描述符表
- 4、vmm_init()
- 主要就是實驗了一個 do_pgfault() 函數達到頁錯誤異常處理功能,以及虛擬內存相關的 mm,vma 結構數據的創建/銷燬/查找/插入等函數
- 5、proc_init()
- 這個函數啓動了創建內核線程的步驟,完成了 idleproc 內核線程和 initproc 內核線程的創建或複製工作,這是本次實驗分析的重點,後面將詳細分析。
- 6、ide_init()
- 完成對用於頁換入換出的硬盤(簡稱 swap 硬盤)的初始化工作
- 7、swap_init()
- swap_init() 函數首先建立完成頁面替換過程的主要功能模塊,即 swap_manager,其中包含了頁面置換算法的實現
操作系統是以進程爲中心設計的,所以其首要任務是爲進程建立檔案,進程檔案用於表示、標識或描述進程,即進程控制塊。這裏需要完成的就是一個進程控制塊的初始化。
而這裏我們分配的是一個內核線程的 PCB,它通常只是內核中的一小段代碼或者函數,沒有用戶空間。而由於在操作系統啓動後,已經對整個核心內存空間進行了管理,通過設置頁表建立了核心虛擬空間(即 boot_cr3 指向的二級頁表描述的空間)。所以內核中的所有線程都不需要再建立各自的頁表,只需共享這個核心虛擬空間就可以訪問整個物理內存了。
首先在 kern/process/proc.h
中定義了 PCB
,即進程控制塊的結構體 proc_struct
,如下:
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; //進程哈希表
};
這裏簡單介紹下各個參數:
- state:進程所處的狀態。
- PROC_UNINIT // 未初始狀態
- PROC_SLEEPING // 睡眠(阻塞)狀態
- PROC_RUNNABLE // 運行與就緒態
- PROC_ZOMBIE // 僵死狀態
- pid:進程 id 號。
- kstack:記錄了分配給該進程/線程的內核桟的位置。
- need_resched:是否需要調度
- parent:用戶進程的父進程。
- mm:即實驗三中的描述進程虛擬內存的結構體
- context:進程的上下文,用於進程切換。
- tf:中斷幀的指針,總是指向內核棧的某個位置。中斷幀記錄了進程在被中斷前的狀態。
- cr3:記錄了當前使用的頁表的地址
而這裏要求我們完成一個 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 的位置。
練習2:爲新創建的內核線程分配資源(需要編碼)
alloc_proc 實質只是找到了一小塊內存用以記錄進程的必要信息,並沒有實際分配這些資源,而練習 2 完成的 do_fork 纔是真正完成了資源分配的工作,當然,do_fork 也只是創建當前內核線程的一個副本,它們的執行上下文、代碼、數據都一樣,但是存儲位置不同。
根據提示及閱讀源碼可知,它完成的工作主要如下:
- 1、分配並初始化進程控制塊( alloc_proc 函數);
- 2、分配並初始化內核棧,爲內核進程(線程)建立棧空間( setup_stack 函數);
- 3、根據 clone_flag 標誌複製或共享進程內存管理結構( copy_mm 函數);
- 4、設置進程在內核(將來也包括用戶態)正常運行和調度所需的中斷幀和執行上下文
( copy_thread 函數); - 5、爲進程分配一個 PID( get_pid() 函數);
- 6、把設置好的進程控制塊放入 hash_list 和 proc_list 兩個全局進程鏈表中;
- 7、自此,進程已經準備好執行了,把進程狀態設置爲“就緒”態;
- 8、設置返回碼爲子進程的 PID 號。
實現過程如下:
* 實現思路:
該函數的語義爲爲內核線程創建新的線程控制塊,並且對控制塊中的每個成員變量進行正確的設置,使得之後可以正確切換到對應的線程中執行。
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 取值範圍 ,同時要求 PID 的取值範圍爲 ,last_pid
和 next_safe
被初始化爲 MAX_PID
。每次調用 get_pid
時,除了確定一個可以分配的 PID 外,還需要確定 next_safe
來實現均攤以此優化時間複雜度,PID 的確定過程中會檢查所有進程的 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;
}
練習3:閱讀代碼,理解 proc_run 函數和它調用的函數如何完成進程切換的。(無編碼工作)
這裏我從 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 調度算法:
- 1、調度開始時,先屏蔽中斷,設置當前內核線程 current->need_resched 爲 0。
- 2、在進程鏈表中,查找第一個可以被調度的程序,即在 proc_list 隊列中查找下一個處於就緒態的線程或進程 next。
- 3、找到這樣的進程後,就調用 proc_run 函數,保存當前進程 current 的執行現場(進程上下文),恢復新進程的執行現場,運行新進程,允許中斷,完成進程切換。
即 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 函數
4、由 switch_to函數完成具體的兩個線程的執行現場切換,即切換各個寄存器,當 switch_to 函數執行完“ret”指令後,就切換到 initproc 執行了。
proc_run
的執行過程爲:
- 保存 IF 位並且禁止中斷;
- 將 current 指針指向將要執行的進程;
- 更新 TSS 中的棧頂指針;
- 加載新的頁表;
- 調用 switch_to 進行上下文切換;
- 當執行 proc_run 的進程恢復執行之後,需要恢復 IF 位。
以下是對 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);
}
}
在本實驗的執行過程中,創建且運行了幾個內核線程?
總共創建了兩個內核線程,分別爲:
- idle_proc,爲第 0 個內核線程,在完成新的內核線程的創建以及各種初始化工作之後,進入死循環,用於調度其他進程或線程;
- init_proc,被創建用於打印 "Hello World" 的線程。本次實驗的內核線程,只用來打印字符串。
語句
local_intr_save(intr_flag);....local_intr_restore(intr_flag);
在這裏有何作用?請說明理由。
在進行進程切換的時候,需要避免出現中斷干擾這個過程,所以需要在上下文切換期間清除 IF 位屏蔽中斷,並且在進程恢復執行後恢復 IF 位。
- 該語句的左右是關閉中斷,使得在這個語句塊內的內容不會被中斷打斷,是一個原子操作;
- 這就使得某些關鍵的代碼不會被打斷,從而不會一起不必要的錯誤;
- 比如說在 proc_run 函數中,將 current 指向了要切換到的線程,但是此時還沒有真正將控制權轉移過去,如果在這個時候出現中斷打斷這些操作,就會出現 current 中保存的並不是正在運行的線程的中斷控制塊,從而出現錯誤;
運行結果如下:
擴展練習Challenge:實現支持任意大小的內存分配算法
通過少量的修改,即可使用實驗2擴展練習實現的 Slub 算法。
- 初始化 Slub 算法:在初始化物理內存最後初始化 Slub ;
void pmm_init(void) {
...
kmem_int();
}
- 在 vmm.c 中使用 Slub 算法:
爲了使用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
...
}
- 在 proc.c 中使用 Slub 算法:
聲明倉庫指針。
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 回收,不需要回收內存。
參考文獻
- https://en.wikipedia.org/wiki/SLOB
- https://lwn.net/Articles/157944/
- https://www.ibm.com/developerworks/cn/linux/l-linux-slab-allocator/