作業系統lab4實驗報告
本次實驗将接觸的是核心線程的管理。核心線程是一種特殊的程序,核心線程與使用者程序的差別有兩個:核心線程隻運作在核心态而使用者程序會在在使用者态和核心态交替運作;所有核心線程直接使用共同的
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 ,其中包含了頁面置換算法的實作
練習0 填寫已有實驗
同樣我們運用
meld
軟體進行比較。大緻截圖如下:
經過比較和修改,我将我所需要修改的檔案羅列如下:
default_pmm.c
pmm.c
swap_fifo.c
vmm.c
trap.c
練習1 配置設定并初始化一個程序控制塊
作業系統是以程序為中心設計的,是以其首要任務是為程序建立檔案,程序檔案用于表示、辨別或描述程序,即程序控制塊。這裡需要完成的就是一個程序控制塊的初始化。
而這裡我們配置設定的是一個核心線程的
PCB
,它通常隻是核心中的一小段代碼或者函數,沒有使用者空間。而由于在作業系統啟動後,已經對整個核心記憶體空間進行了管理,通過設定頁表建立了核心虛拟空間(即
boot_cr3
指向的二級頁表描述的空間)。是以核心中的所有線程都不需要再建立各自的頁表,隻需共享這個核心虛拟空間就可以通路整個實體記憶體了。
首先在
kern/process/proc.h
中定義了
PCB
即程序控制塊的結構體
proc_struct
,如下:
struct proc_struct {
enum proc_state state; // Process state
int pid; // Process ID
int runs; // the running times of Proces
uintptr_t kstack; // Process kernel stack
volatile bool need_resched; // bool value: need to be rescheduled to release CPU?
struct proc_struct *parent; // the parent process
struct mm_struct *mm; // Process's memory management field
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for current interrupt
uintptr_t cr3; // CR3 register: the base addr of Page Directroy Table(PDT)
uint32_t flags; // Process flag
char name[PROC_NAME_LEN + ]; // Process name
list_entry_t list_link; // Process link list
list_entry_t hash_link; // Process hash list
};
這裡簡單介紹下各個參數:
- state:程序所處的狀态。
PROC_UNINIT // 未初始狀态
PROC_SLEEPING // 睡眠(阻塞)狀态
PROC_RUNNABLE // 運作與就緒态
PROC_ZOMBIE // 僵死狀态
- pid:程序id号。
- kstack:記錄了配置設定給該程序/線程的核心桟的位置。
- need_resched:是否需要排程
- parent:使用者程序的父程序。
- mm:即實驗三中的描述程序虛拟記憶體的結構體
- context:程序的上下文,用于程序切換。
- tf:中斷幀的指針,總是指向核心棧的某個位置。中斷幀記錄了程序在被中斷前的狀态。
- cr3:記錄了目前使用的頁表的位址
而這裡要求我們完成一個
alloc_proc
函數來負責配置設定一個新的
struct proc_struct
結構,根據提示我們需要初始化一些變量,具體的代碼如下:
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 = -; //未初始化的的程序id為-1
proc->runs = ; //初始化時間片
proc->kstack = ; //記憶體棧的位址
proc->need_resched = ; //是否需要排程設為不需要
proc->parent = NULL; //父節點設為空
proc->mm = NULL; //虛拟記憶體設為空
memset(&(proc->context), , sizeof(struct context));//上下文的初始化
proc->tf = NULL; //中斷幀指針置為空
proc->cr3 = boot_cr3; //頁目錄設為核心頁目錄表的基址
proc->flags = ; //标志位
memset(proc->name, , PROC_NAME_LEN);//程序名
}
return proc;
}
第一條設定了程序的狀态為“初始”态,這表示程序已經“出生”了;
第二條語句設定了程序的
pid
為-1,這表示程序的“身份證号”還沒有辦好;
第三條語句表明由于該核心線程在核心中運作,故采用為
ucore
核心已經建立的頁表,即設定為在
ucore
核心頁表的起始位址
boot_cr3
。
練習2 為新建立的核心線程配置設定資源
alloc_proc
實質隻是找到了一小塊記憶體用以記錄程序的必要資訊,并沒有實際配置設定這些資源,而練習2完成的
do_fork
才是真正完成了資源配置設定的工作,當然,
do_fork
也隻是建立目前核心線程的一個副本,它們的執行上下文、代碼、資料都一樣,但是存儲位置不同。
根據提示及閱讀源碼可知,它完成的工作主要如下:
- 1.配置設定并初始化程序控制塊(
函數);alloc_proc
- 2.配置設定并初始化核心棧(
函數);setup_stack
- 3.根據
标志複制或共享程序記憶體管理結構(clone_flag
函數);copy_mm
-
4.設定程序在核心(将來也包括使用者态)正常運作和排程所需的中斷幀和執行上下文
(
函數);copy_thread
- 5.把設定好的程序控制塊放入
和hash_list
兩個全局程序連結清單中;proc_list
- 6.自此,程序已經準備好執行了,把程序狀态設定為“就緒”态;
- 7.設定傳回碼為子程序的
号。id
補全後的代碼如下:詳細注釋見代碼中
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) {
goto fork_out;
}
ret = -E_NO_MEM;
//1:調用alloc_proc()函數申請記憶體塊,如果失敗,直接傳回處理
if ((proc = alloc_proc()) == NULL) {
goto fork_out;
}
//2.将子程序的父節點設定為目前程序
proc->parent = current;
//3.調用setup_stack()函數為程序配置設定一個核心棧
if (setup_kstack(proc) != ) {
goto bad_fork_cleanup_proc;
}
//4.調用copy_mm()函數複制父程序的記憶體資訊到子程序
if (copy_mm(clone_flags, proc) != ) {
goto bad_fork_cleanup_kstack;
}
//5.調用copy_thread()函數複制父程序的中斷幀和上下文資訊
copy_thread(proc, stack, tf);
//6.将新程序添加到程序的hash清單中
bool intr_flag;
local_intr_save(intr_flag);
{
proc->pid = get_pid();
hash_proc(proc); //建立映射
nr_process ++; //程序數加1
list_add(&proc_list, &(proc->list_link));//将程序加入到程序的連結清單中
}
local_intr_restore(intr_flag);
// 7.一切就緒,喚醒子程序
wakeup_proc(proc);
// 8.傳回子程序的pid
ret = proc->pid;
fork_out:
return ret;
bad_fork_cleanup_kstack:
put_kstack(proc);
bad_fork_cleanup_proc:
kfree(proc);
goto fork_out;
}
練習3 了解 proc_run
和它調用的函數如何完成程序切換的
proc_run
這裡我從 proc_init() 函數開始說起的。由于之前的 proc_init() 函數已經完成了 idleproc 核心線程和 initproc 核心線程的初始化。是以在 kern_init() 最後,它通過 cpu_idle() 喚醒了0号 idle 程序,在分析 proc_run 函數之前,我們先分析排程函數 schedule() 。
schedule()
代碼如下:
void
schedule(void) {
bool intr_flag;
list_entry_t *le, *last;
struct proc_struct *next = NULL;
local_intr_save(intr_flag);
{
current->need_resched = ;
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;
}
}
} while (le != last);
if (next == NULL || next->state != PROC_RUNNABLE) {
next = idleproc;
}
next->runs ++;
if (next != current) {
proc_run(next);
}
}
local_intr_restore(intr_flag);
}
很容易閱讀到它的代碼邏輯,它是一個 FIFO 排程器,執行過程如下:
- 1、設定目前核心線程 current->need_resched 為 0;
- 2、在 proc_list 隊列中查找下一個處于就緒态的線程或程序 next;
- 3、找到這樣的程序後,就調用 proc_run 函數,儲存目前程序 current 的執行現場(程序上下文),恢複新程序的執行現場,完成程序切換。
即
schedule
函數通過查找 proc_list 程序隊列,在這裡隻能找到一個處于就緒态的 initproc 核心線程。于是通過
proc_run
和進一步的 switch_to 函數完成兩個執行現場的切換。
好,現在進入到重點的
proc_run
函數,代碼如下:
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;
load_esp0(next->kstack + KSTACKSIZE);
lcr3(next->cr3);
switch_to(&(prev->context), &(next->context));
}
local_intr_restore(intr_flag);
}
}
那麼我們來分析分析這個代碼:
- 1、讓 current 指向 next 核心線程 initproc;
- 2、設定任務狀态段 ts 中特權态 0 下的棧頂指針 esp0 為 next 核心線程 initproc 的核心棧的棧頂,即 next->kstack + KSTACKSIZE ;
- 3、設定 CR3 寄存器的值為 next 核心線程 initproc 的頁目錄表起始位址 next->cr3,這實際上是完成程序間的頁表切換;
- 4、由
函數完成具體的兩個線程的執行現場切換,即切換各個寄存器,當 switch_to 函數執行完“ret”指令後,就切換到 initproc 執行了。switch_to
接下來我們再來進一步分析一下這個
switch_to
函數,主要代碼如下:
switch_to: # switch_to(from, to)
# save from's registers
movl (%esp), %eax # eax points to from
popl (%eax) # save eip !popl
movl %esp, (%eax)
movl %ebx, (%eax)
movl %ecx, (%eax)
movl %edx, (%eax)
movl %esi, (%eax)
movl %edi, (%eax)
movl %ebp, (%eax)
# restore to's registers
movl (%esp), %eax # not 8(%esp): popped return address already
# eax now points to to
movl (%eax), %ebp
movl (%eax), %edi
movl (%eax), %esi
movl (%eax), %edx
movl (%eax), %ecx
movl (%eax), %ebx
movl (%eax), %esp
pushl (%eax) # push eip
ret
首先,儲存前一個程序的執行現場,即
movl 4(%esp), %eax
和
popl 0(%eax)
兩行代碼。
然後接下來的七條指令如下:
movl %esp, (%eax)
movl %ebx, (%eax)
movl %ecx, (%eax)
movl %edx, (%eax)
movl %esi, (%eax)
movl %edi, (%eax)
movl %ebp, (%eax)
這些指令完成了儲存前一個程序的其他 7 個寄存器到
context
中的相應域中。至此前一個程序的執行現場儲存完畢。
再往後是恢複向一個程序的執行現場,這其實就是上述儲存過程的逆執行過程,即從 context 的高位址的域 ebp 開始,逐一把相關域的值指派給對應的寄存器。
最後的
pushl 0(%eax)
其實把 context 中儲存的下一個程序要執行的指令位址 context.eip 放到了堆棧頂,這樣接下來執行最後一條指令“ret”時,會把棧頂的内容指派給 EIP 寄存器,這樣就切換到下一個程序執行了,即目前程序已經是下一個程序了,進而完成了程序的切換。