實驗四:核心線程管理 練習1:分配並初始化一個進程式控制制塊
首先來看幾個比較重要的資料結構,kern/process/proc.h中定義的進程式控制制塊及kern/trap/trap.h中定義的中斷幀
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 + 1]; // Process name list_entry_t list_link; // Process link list list_entry_t hash_link; // Process hash list}struct trapframe { /* below here is a struct of general registers and defined by software */ // 當中斷異常發生時,此處結構內的通用寄存器資訊由軟體負責壓棧儲存 struct pushregs tf_regs; /* below here are segement registers and defined by software */ // 當中斷異常發生時,此處的段寄存器資訊由軟體負責壓棧儲存 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, defined by hardware */ // 僅發生特權級改變時,此處額外的資訊由硬體壓棧儲存 uintptr_t tf_esp; uint16_t tf_ss; uint16_t tf_padding5;}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;}
這裡可以看到struct context和struct trapframe中有很多寄存器是一樣的,前者用於進程環境切換,後者用於中斷環境切換。注意這兩者的含義是不一樣的,在本實驗中一個進程開始執行需要系統進行初始化,此時tf被用來儲存中斷幀,而進程執行時是通過context來完成切換的,詳細見練習3的說明。
結構中一些重要的成員變數說明如下 mm記憶體管理資訊,包括記憶體映射列表、頁表指標等 state進程所處的狀態,有PROC_UNINIT、PROC_SLEEPING、PROC_RUNNABLE、PROC_ZOMBIE四種,定義在enum proc_state中 parent父進程,在所有進程中只有核心建立的第一個核心線程idleproc沒有父進程,核心根據父子關係建立樹形結構維護一些特殊的操作 context進程的上下文,儲存寄存器,用於進程切換 tf中斷幀,總是指向核心棧的某個位置,當進程發生中斷異常,從使用者跳到核心時,中斷幀記錄了進程在中斷前的狀態,由於允許中斷嵌套,因此ucore在核心棧上維護了tf鏈,保證tf總是能夠指向當前的trapframe kstack每個線程都有核心棧,並且位於核心地址空間的不同位置,對於核心線程,該棧就是運行時程式使用的棧,對於普通進程,該棧就是發生特權級改變時需儲存被打斷硬體資訊的棧
另外為了管理系統中所有的進程式控制制塊,ucore維護了一些重要的全域變數 static struct proc *current當前佔用處理機並處於運行狀態的進程式控制制塊指標 static list_entry_t hash_list[HASH_LIST_SIZE]所有進程式控制制塊的雜湊表,hash_link將基於pid連結入這個雜湊表
根據實驗手冊及代碼的提示,練習1的代碼如下
把proc進行初步初始化(即把proc_struct中的各個成員變數清零)。但有些成員變數設定了特殊的值
// alloc_proc - alloc a proc_struct and init all fields of proc_structstatic 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; // 設定進程pid的未初始化值 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 + 1); } return proc;}
之後proc_init會進一步初始化idleproc核心線程 練習2:為新建立的核心線程分配資源
do_fork是建立線程的主要函數,kernel_thread通過調用do_fork函數來完成核心線程的建立工作,練習2中要求完成的do_fork函數主要完成了6項工作 分配並初始化進程式控制制塊(alloc_proc) 分配並初始化核心棧(setup_stack) 根據clone_flag標誌複製或共用記憶體管理結構(copy_mm) 設定進程在核心正常運行和調度所需要的中斷幀和上下文(copy_thread) 把設定好的進程式控制制塊放入hash_list和proc_list兩個全域進程鏈表中 將進程狀態設定為“就緒”態
/* 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 */intdo_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; //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){ goto fork_out; // 分配失敗,直接返回 } if(setup_kstack(proc) != 0){ goto bad_fork_cleanup_kstack; // 堆棧初始化失敗,釋放已佔用的空間並返回 } if(copy_mm(clone_flags, proc) != 0){ goto bad_fork_cleanup_proc; // 複製或共用記憶體管理結構失敗,釋放已佔用的空間並返回 } copy_thread(proc, stack, tf); // 複製中斷幀和上下文 proc->pid = get_pid(); // 分配pid hash_proc(proc); // 將新進程加入雜湊表 list_add(&proc_list, &(proc->list_link)); nr_process ++; wakeup_proc(proc); // 喚醒進程 ret = proc->pid; // 返回進程pidfork_out: return ret;bad_fork_cleanup_kstack: put_kstack(proc);bad_fork_cleanup_proc: kfree(proc); goto fork_out;}
參考答案中有更多的考慮,不同部分及說明如下
proc->parent = current; // 設定父進程為當前的進程 bool intr_flag; // 由於要操作全域資料結構,而ucore允許中斷嵌套,為了避免出現安全執行緒問題,這裡需要利用到kern/sync/sync.h中的鎖,至於安全執行緒問題可以參考相關的博文 local_intr_save(intr_flag); { proc->pid = get_pid(); hash_proc(proc); list_add(&proc_list, &(proc->list_link)); nr_process ++; } local_intr_restore(intr_flag); 練習3:閱讀代碼,理解proc_run函數和它調用的函數如何完成進程切換
參考piazza上同學的單步調式
https://piazza.com/class/i5j09fnsl7k5x0?cid=331
1、kern_init調用了proc_init函數,後者啟動了建立核心線程的步驟 idleproc = alloc_proc()通過kmalloc函數獲得了proc_struct作為第0個進程的控制塊,並初始化 proc_init函數對idleproc核心線程進行進一步初始化
idleproc->pid = 0; // 設定pid=0,即第0個核心線程idleproc->state = PROC_RUNNABLE; // 設定狀態為可運行,等待處理機調度執行該進程idleproc->kstack = (uintptr_t)bootstack; // 直接將ucore啟動時的核心棧作為改線程的核心棧(以後其他線程的核心棧需要分配獲得)idleproc->need_resched = 1; // 設定為可以被調度,根據cpu_idle函數,只要idleproc在執行且可以被調度,就立刻執行schedule函數切換到其他有用進程執行set_proc_name(idleproc, "idle"); // 線程命名為idlenr_process ++; // 線程數+1current = idleproc; // 設定當前線程為idleproc
調用pid = kernel_thread(init_main, "Hello world!!", 0)建立一個核心線程init_main
2、kernel_thread函數建立了核心線程的臨時中斷幀,並調用do_fork函數來進行進一步產生新的核心線程
// kernel_thread - create a kernel thread using "fn" function// NOTE: the contents of temp trapframe tf will be copied to // proc->tf in do_fork-->copy_thread functionintkernel_thread(int (*fn)(void *), void *arg, uint32_t clone_flags) { struct trapframe tf; // 臨時變數tf來儲存中斷幀,並傳遞給do_fork memset(&tf, 0, sizeof(struct trapframe)); // tf清零初始化 tf.tf_cs = KERNEL_CS; // 設定程式碼片段為核心程式碼片段KERNEL_CS tf.tf_ds = tf.tf_es = tf.tf_ss = KERNEL_DS; // 設定資料區段為核心資料區段KERNEL_DS tf.tf_regs.reg_ebx = (uint32_t)fn; tf.tf_regs.reg_edx = (uint32_t)arg; tf.tf_eip = (uint32_t)kernel_thread_entry; // 設定入口為函數kernel_thread_entry(定義在kern/process/entry.S中),該函數主要為函數fn做準備 return do_fork(clone_flags | CLONE_VM, 0, &tf); // 調用do_fork進一步完成建立工作,第二個參數為0代表建立的是核心線程}
kernel_thread_entry: # void kernel_thread(void) pushl %edx # push arg 將fn的參數壓棧 call *%ebx # call fn 調用fn pushl %eax # save the return value of fn(arg) 將fn的傳回值壓棧 call do_exit # call do_exit to terminate current thread 退出線程
3、do_fork完成的工作在練習2中已充分說明,這裡詳細說明最重要的copy_thread函數
// copy_thread - setup the trapframe on the process's kernel stack top and// - setup the kernel entry point and stack of processstatic voidcopy_thread(struct proc_struct *proc, uintptr_t esp, struct trapframe *tf) { proc->tf = (struct trapframe *)(proc->kstack + KSTACKSIZE) - 1; // proc->kstack指向給進程分配的KSTACKPAGE大小的空間,加上大小之後就指向最高地址,然後強制類型轉換後-1即在記憶體頂部空出一塊trap frame大小的空間,用於複製傳進來的臨時幀 *(proc->tf) = *tf; // 複製臨時幀 proc->tf->tf_regs.reg_eax = 0; proc->tf->tf_esp = esp; proc->tf->tf_eflags |= FL_IF; proc->context.eip = (uintptr_t)forkret; // 設定指令寄存器為forkret函數,則進入子進程後就會調用forkret proc->context.esp = (uintptr_t)(proc->tf); // 設定棧頂為核心棧存放了tf後的最大地址,即子進程棧頂從tf下面開始}
隨後到do_fork完成所有工作,返回到kernel_thread再返回到proc_init再返回到kern_init
4、此時在kern_init後續有cpu_idle和schedule進行進程調度
// cpu_idle - at the end of kern_init, the first kernel thread idleproc will do below worksvoidcpu_idle(void) { while (1) { if (current->need_resched) { // 顯然此時current = idleproc且已被設定為需要被調度,故進入schedule函數調度 schedule(); } }}
5、schedule將返回之前建立的進程,並調用proc_run進行運行
// proc_run - make process "proc" running on cpu// NOTE: before call switch_to, should load base addr of "proc"'s new PDTvoidproc_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); // 修改TSS任務狀態棧,將TSS的ts_esp0指向下一個進程的堆棧空間,參數值參考3中的注釋 lcr3(next->cr3); // 修改頁表基址 switch_to(&(prev->context), &(next->context)); // 切換,函數定義在kern\process\switch.S中 } local_intr_restore(intr_flag); }}
6、proc_run完成一些準備工作後,調用switch_to最終完成切換
高地址--------------------|arg1:next->context|--------------------|arg0:prev->context|--------------------|return address | --->esp--------------------switch_to: # switch_to(from, to) # save from's registers movl 4(%esp), %eax # eax points to from [esp+4]即prev->context,將指向prev->context的指標放入eax中 popl 0(%eax) # save eip !popl 按照context的順序儲存原進程的各寄存器 movl %esp, 4(%eax) # save esp::context of from movl %ebx, 8(%eax) # save ebx::context of from movl %ecx, 12(%eax) # save ecx::context of from movl %edx, 16(%eax) # save edx::context of from movl %esi, 20(%eax) # save esi::context of from movl %edi, 24(%eax) # save edi::context of from movl %ebp, 28(%eax) # save ebp::context of from高地址--------------------|arg1:next->context|--------------------|arg0:prev->context| --->esp-------------------- # restore to's registers movl 4(%esp), %eax # not 8(%esp): popped return address already 由於前面已經popl,故這裡不是8而是[esp+4]即next->context放入eax中 # eax now points to to 按照context的順序擷取新進程的各寄存器 movl 28(%eax), %ebp # restore ebp::context of to movl 24(%eax), %edi # restore edi::context of to movl 20(%eax), %esi # restore esi::context of to movl 16(%eax), %edx # restore edx::context of to movl 12(%eax), %ecx # restore ecx::context of to movl 8(%eax), %ebx # restore ebx::context of to movl 4(%eax), %esp # restore esp::context of to pushl 0(%eax) # push eip 前面將原進程的返回地址彈出,現在將next->context的eip壓入棧 ret
注意,新進程就是前面建立的init_main,參考3可以知道當時proc->context.eip = (uintptr_t)forkret,當switch_to返回時,把棧頂的內容賦值給EIP寄存器,此時跳轉到了forkret進行執行
7、forkret調用forkrets完成準備並最終進入init_main
// forkret -- the first kernel entry point of a new thread/process// NOTE: the addr of forkret is setted in copy_thread function// after switch_to, the current proc will execute here.static voidforkret(void) { forkrets(current->tf);} # return falls through to trapret....globl __trapret__trapret: # restore registers from stack popal # restore %ds, %es, %fs and %gs popl %gs popl %fs popl %es popl %ds # get rid of the trap number and error code addl $0x8, %esp iret .globl forkretsforkrets: # set stack to this new process's trapframe movl 4(%esp), %esp #esp指向當前進程的中斷幀即esp指向current->tf jmp __trapret
注意參考3可以知道proc->context.esp = (uintptr_t)(proc->tf),而在6中switch_to最後壓入了proc->context.eip,故在forkrets中[esp+4]即指向context.esp,這裡就是中斷幀proc->tf,參考棧的內容、struct trapframe及__trapret就會理解跳轉情況
switch_to之後的棧在記憶體中的存放:高地址-------------|context.ebp||...........| -------------|context.esp| --->[esp+4] = proc->tf-------------|context.eip| --->espstruct trapframe { struct pushregs tf_regs; // 通用寄存器,對應__trapret: popal uint16_t tf_gs; // 對應popl %gs uint16_t tf_padding0; uint16_t tf_fs; // 對應popl %fs uint16_t tf_padding1; uint16_t tf_es; // 對應popl %es uint16_t tf_padding2; uint16_t tf_ds; // 對應popl %ds uint16_t tf_padding3; uint32_t tf_trapno; // [esp]此時的esp指向這裡 /* below here defined by x86 hardware */ uint32_t tf_err; // [esp+4] uintptr_t tf_eip; // [esp+8]對應addl $0x8, %esp uint16_t tf_cs; ...};
當iret返回時就會進入此時的esp所指向的proc->tf.tf_eip,在2中這個值被初始化為kernel_thread_entry
.text.globl kernel_thread_entrykernel_thread_entry: # void kernel_thread(void) pushl %edx # push arg call *%ebx # call fn pushl %eax # save the return value of fn(arg) call do_exit # call do_exit to terminate current thread
由此進入init_main,當返回時調用do_exit完成所有過程
完成代碼後使用make qemu查看結果如下,程式正確
(THU.CST) os is loading ......省略部分輸出check_alloc_page() succeeded!check_pgdir() succeeded!check_boot_pgdir() succeeded!...省略部分輸出kmalloc_init() succeeded!check_vma_struct() succeeded!...省略部分輸出check_pgfault() succeeded!check_vmm() succeeded....省略部分輸出check_swap() succeeded!++ setup timer interruptsthis initproc, pid = 1, name = "init"To U: "Hello world!!".To U: "en.., Bye, Bye. :)"kernel panic at kern/process/proc.c:344: process exit!!.Welcome to the kernel debug monitor!!Type 'help' for a list of commands.