這個項目系列到這裏差很少到一半了,前半部分的 segment,虛擬內存,中斷等,其實能夠看做只是準備工做。是的,咱們花了大量時間去準備這些基石工做,以致於到如今,整個所謂的 kernel 好像仍然處於一種「靜止「狀態,可能已經讓你以爲睏倦了。從本篇開始,這個 kernel 將會真正地」動「起來,開始搭建一個操做系統應有的核心能力,那就是任務管理。java
從用戶的角度來說,操做系統的本質功能就是爲他們運行任務,不然就難以稱之爲操做系統了。這是一個複雜的工程,萬事開頭難,因此做爲開始階段的本篇,會盡量地簡單,從運行一個單線程開始。在後面的篇章中,會逐漸進入多線程切換與管理,同步與競爭等問題,以及最終來到更上層的進程管理。git
thread
和 process
的相關概念應該不須要多解釋了,都是老生常談。在接下來的行文和代碼中,我會將 task
等同於 thread
,二者混用,都表示線程;而 process
則是進程。shell
操做系統調度的對象是 thread
,也是接下來須要討論和實現的核心概念。 也許 thread
聽上去很抽象,從本質上來講,它能夠歸結爲如下兩個核心要素:segmentfault
代碼 + stack
代碼控制它時間維度上的流轉,stack
則是它空間維度的依託,這二者構成了 thread
運行的核心。多線程
因此每一個 thread 都有它本身的 stack,例如運行在 kernel 態的一堆 threads,大概是這樣的格局:app
每一個 thread 運行在它本身的 stack 上,而操做系統則負責調度這些 threads 的啓停。從本質上說,自從咱們進入 kernel 的 main 函數運行到如今,也能夠歸爲一個 thread,它是一個引導。再日後,操做系統將建立更多的 threads,而且 CPU 將會在操做系統的控制下,在這些 threads 之間來回跳轉切換,其實質就是在這些 threads 各自所屬代碼(指令)和 stack 上進行跳轉切換。函數
本篇代碼主要在 src/task/thread.c,僅供參考。gitlab
首先創建 thread 結構 task_struct
,或者叫 tcb_t
,即 task control block
:佈局
struct task_struct { uint32 kernel_esp; uint32 kernel_stack; uint32 id; char name[32]; // ... }; typedef struct task_struct tcb_t;
這裏有兩個關鍵字段,是關於這個 thread 的 stack 信息:測試
每一個 thread 都以 page 爲單位分配 kernel stack 空間,Linux 好像是 2 pages,因此咱們也分配 2 pages,用 kernel_stack
字段指向它,這個字段後面再也不變化:
#define KERNEL_STACK_SIZE 8192 tcb_t* init_thread(char* name, thread_func function, uint32 priority, uint8 is_user_thread) { // ... thread = (tcb_t*)kmalloc(sizeof(struct task_struct)); // ... uint32 kernel_stack = (uint32)kmalloc_aligned(KERNEL_STACK_SIZE); for (int32 i = 0; i < KERNEL_STACK_SIZE / PAGE_SIZE; i++) { map_page(kernel_stack + i * PAGE_SIZE); } memset((void*)kernel_stack, 0, KERNEL_STACK_SIZE); thread->kernel_stack = kernel_stack; // ... }
注意這裏分配了 2 pages 給 kernel_stack 後,馬上爲它創建了 physical 內存的映射。這是由於 page fault
做爲一箇中斷,它的處理是要在 kernel stack 上完成的,所以對 kernel stack 自己的訪問不能夠再觸發 page fault
,因此這裏提早解決了這個問題。
另外一個字段 kernel_esp
,標識的是當前這個 thread 在 kernel stack 上運行的 esp 指針。目前是 thread 建立階段,咱們須要初始化這個 esp,因此首先須要對整個 stack 的版圖作一個初始化。咱們爲 stack 定義以下結構:
struct switch_stack { // Switch context. uint32 edi; uint32 esi; uint32 ebp; uint32 ebx; uint32 edx; uint32 ecx; uint32 eax; // For thread first run. uint32 start_eip; void (*unused_retaddr); thread_func* function; };
這個 stack 結構第一眼看上去可能比較奇怪,後面會慢慢展開解釋。它既是 thread 第一次啓動運行時的初始 stack,也是後面 multi-threads 上下文切換時的 stack,因此也能夠叫 context switch stack
,或者 switch stack
。咱們將它鋪設到剛纔分配的 2 pages 的 stack 空間上去:
switch stack
上方的虛線空間是預留的,這是之後做爲返回用戶空間用的 interrupt stack
,暫時能夠無視。目前你只須要知道它的結構定義爲 interrupt_stack_t
,也就是以前的 src/interrupt/interrupt.h 裏定義的 isr_params
這個結構,你能夠回顧一下中斷處理這一篇,它是中斷髮生時的 CPU 和操做系統壓棧,用於保存中斷 context 的。
因此整個 stack 的初始化,就是在最上方分配了一個 interrupt stack
+ switch stack
結構:
thread->kernel_esp = kernel_stack + KERNEL_STACK_SIZE - (sizeof(interrupt_stack_t) + sizeof(switch_stack_t));
因而 kernel_esp
就被初始化爲上圖標出的位置,實際上指向了 switch stack
這個結構。
接下來就是初始化這個 switch stack
:
switch_stack_t* switch_stack = (switch_stack_t*)thread->kernel_esp; switch_stack->edi = 0; switch_stack->esi = 0; switch_stack->ebp = 0; switch_stack->ebx = 0; switch_stack->edx = 0; switch_stack->ecx = 0; switch_stack->eax = 0; switch_stack->start_eip = (uint32)kernel_thread; switch_stack->function = function;
start_eip
是 thread 入口地址,設置爲 kernel_thread
這個函數;function
是 thread 真正要運行的工做函數,它由 kernel_thread
函數來啓動運行;static void kernel_thread(thread_func* function) { function(); schedule_thread_exit(0); }
這裏若是不明白的話能夠先接着往下看 thread 的運行,而後再來回顧。
建立 thread,而且運行 thread:
void test_thread() { monitor_printf("first thread running ...\n"); while (1) {} } int main() { tcb_t* thread = init_thread( "test", test_thread, THREAD_DEFAULT_PRIORITY, false); asm volatile ( "movl %0, %%esp; \ jmp resume_thread": : "g" (thread->kernel_esp) : "memory"); }
測試代碼很簡單,建立了一個 thread,它要運行的函數是 test_thread
,僅僅是打印一下。
這裏在 C 語言裏用內聯 asm 代碼,觸發了 thread 開始運行,來看一下它的原理。首先將 esp 寄存器賦值爲該 thread 的 kernel_esp
,而後跳轉到 resume_thread 這個函數:
resume_thread: pop edi pop esi pop ebp pop ebx pop edx pop ecx pop eax sti ret
它實際上是 context_switch 函數的下半部分,這個是用於 multi-threads 切換的,這個下一篇再講。
來看 resume_thread
作的事情,對照圖中的 kernel_esp
位置開始,運行代碼:
pop
了全部通用寄存器,在 multi-threads 切換裏它是用來恢復 thread 的 context 數據的,可是如今 thread 是第一次運行,因此它們這裏全被 pop 成了 0;ret
指令,使程序跳轉到了 start_eip
處,它被初始化爲爲函數 kernel_thread
,從這裏 thread 正式開始運行,它的運行 stack 爲右圖中淺藍色部分;注意到 kernel_thread
函數,傳入並運行了參數 function
,這是 thread
真正的工做函數:
static void kernel_thread(thread_func* function) { function(); schedule_thread_exit(0); }
這裏可能有幾個問題須要解釋一下:
[問題 1] 爲何不直接運行 function
,而要在外面嵌套一層 kernel_thread
函數做爲 wrapper?
由於 thread 運行結束後須要一個退出機制,函數 schedule_thread_exit
會完成 thread 的收尾和清理工做;schedule_thread_exit
函數不會返回,而是直接引導該 thread 消亡,而後切換到下一個待運行的 thread。關於 schedule_thread_exit
也會在下一篇中再細講。
[問題 2] 圖中灰色的 unused
部分是什麼?
它是 kernel_thread
函數的返回值,實際上 kernel_thread
不會返回,由於函數 schedule_thread_exit
不會返回。這裏 unused
僅僅是一個佔位。
OK,至此咱們的第一個 thread 就運行起來了,能夠看到它的打印:
本篇運行了第一個 thread,它作的事情其實比較簡單,就是找了一塊內存作 stack
,而後在上面創造出了一個函數運行的環境,而後跳轉指令到 thread 的入口處開始運行。可能有不少細節處仍是霧裏看花,不知其因此然,本篇都沒有詳細展開,例如:
stack
佈局?這些都留待下一篇詳解,待到下一篇完成後,結合這兩篇的內容,應該會對 threaad 的運行機制有一個全面的認識。