【自制作業系統12】熟悉而陌生的多執行緒
一、到目前為止的程式流程圖
為了讓大家清楚目前的程式進度,畫了到目前為止的程式流程圖,如下。紅色部分是我們今天要實現的
二、程序與執行緒簡述
相信看這篇文章的人,肯定不是對基本概念感興趣,這也不是我的主要目的。所以這裡真的是簡述一下
程序和執行緒都是 獨立的程式執行流,只不過程序有自己獨立的記憶體空間,同一個程序裡的執行緒共享記憶體空間,具體體現在 pcb 表中一個欄位上,指向頁表的地址值。
執行緒分 使用者執行緒 和 核心執行緒,使用者執行緒可以理解為就是沒有執行緒,只是使用者程式中寫了一個執行緒排程器程式在假裝切換,作業系統根本無感知。
三、實現一個簡單的單執行緒
我們分三步實現最終的多執行緒機制,其實就對應著下面三節的內容
- 第一步實現 多執行緒資料結構,並裝模做樣地把一個執行緒的函式跑起來
- 第二步實現 中斷訊號不斷遞減執行緒的時間,達到執行緒被換下 cpu 的條件
- 第三步實現 任務切換,即是第二步的條件達到時,真正的切換任務的函式實現
那麼本節先實現第一步,先看程式碼
程式碼鳥瞰
1 #include "print.h" 2 #include "init.h" 3 #include "thread.h" 4 5 void k_thread_a(void*); 6 7 int main(void){ 8 put_str("I am kernel\n"); 9 init_all(); 10 thread_start("k_thread_a", 31, k_thread_a, "argA "); 11 while(1); 12 return 0; 13 } 14 15 void k_thread_a(void* arg) { 16 char* para = arg; 17 while(1) { 18 put_str(para); 19 } 20 }
1 #include "thread.h" 2 #include "stdint.h" 3 #include "string.h" 4 #include "global.h" 5 #include "memory.h" 6 7 #define PG_SIZE 4096 8 9 // 由 kernel_thread 去執行 function(func_arg) 10 static void kernel_thread(thread_func* function, void* func_arg) { 11 function(func_arg); 12 } 13 14 // 初始化執行緒棧 thread_stack 15 void thread_create(struct task_struct* pthread, thread_func function, void* func_arg) { 16 // 先預留中斷使用棧的空間 17 pthread->self_kstack -= sizeof(struct intr_stack); 18 19 // 再留出執行緒棧空間 20 pthread->self_kstack -= sizeof(struct thread_stack); 21 struct thread_stack* kthread_stack = (struct thread_stack*)pthread->self_kstack; 22 kthread_stack->eip = kernel_thread; 23 kthread_stack->function = function; 24 kthread_stack->func_arg = func_arg; 25 kthread_stack->ebp = kthread_stack->ebx = kthread_stack->esi = kthread_stack->edi = 0; 26 } 27 28 // 初始化執行緒基本資訊 29 void init_thread(struct task_struct* pthread, char* name, int prio) { 30 memset(pthread, 0, sizeof(*pthread)); 31 strcpy(pthread->name, name); 32 pthread->status = TASK_RUNNING; 33 pthread->priority = prio; 34 // 執行緒自己在核心態下使用的棧頂地址 35 pthread->self_kstack = (uint32_t*)((uint32_t)pthread + PG_SIZE); 36 pthread->stack_magic = 0x19870916; // 自定義魔數 37 } 38 39 // 建立一優先順序為 prio 的執行緒,執行緒名為 name,執行緒所執行的函式為 function 40 struct task_struct* thread_start(char* name, int prio, thread_func function, void* func_arg) { 41 // pcb 都位於核心空間,包括使用者程序的 pcb 也是在核心空間 42 struct task_struct* thread = get_kernel_pages(1); 43 44 init_thread(thread, name, prio); 45 thread_create(thread, function, func_arg); 46 47 asm volatile("mov %0, %%esp; pop %%ebp; pop %%ebx; pop %%edi; pop %%esi; ret ": : "g" (thread->self_kstack) : "memory"); 48 return thread; 49 }
1 #ifndef __THREAD_THREAD_H 2 #define __THREAD_THREAD_H 3 #include "stdint.h" 4 5 // 自定義通用函式型別,它將在很多執行緒函式中作為形式引數型別 6 typedef void thread_func(void*); 7 8 // 程序或執行緒的狀態 9 enum task_status { 10 TASK_RUNNING, 11 TASK_READY, 12 TASK_BLOCKED, 13 TASK_WAITING, 14 TASK_HANGING, 15 TASK_DIED 16 }; 17 18 /*********** 中斷棧intr_stack *********** 19 * 此結構用於中斷髮生時保護程式(執行緒或程序)的上下文環境: 20 * 程序或執行緒被外部中斷或軟中斷打斷時,會按照此結構壓入上下文 21 * 暫存器, intr_exit中的出棧操作是此結構的逆操作 22 * 此棧線上程自己的核心棧中位置固定,所在頁的最頂端 23 ********************************************/ 24 struct intr_stack { 25 uint32_t vec_no; // 壓入的中斷號 26 uint32_t edi; 27 uint32_t esi; 28 uint32_t ebp; 29 uint32_t esp_dummy; 30 uint32_t ebx; 31 uint32_t edx; 32 uint32_t ecx; 33 uint32_t eax; 34 uint32_t gs; 35 uint32_t fs; 36 uint32_t es; 37 uint32_t ds; 38 39 // 以下由 cpu 從低特權級進入高特權級時壓入 40 uint32_t err_code; 41 void (*eip) (void); 42 uint32_t cs; 43 uint32_t eflags; 44 void* esp; 45 uint32_t ss; 46 }; 47 48 /*********** 執行緒棧thread_stack *********** 49 * 執行緒自己的棧,用於儲存執行緒中待執行的函式 50 * 此結構線上程自己的核心棧中位置不固定, 51 * 用在switch_to時儲存執行緒環境。 52 * 實際位置取決於實際執行情況。 53 ******************************************/ 54 struct thread_stack { 55 uint32_t ebp; 56 uint32_t ebx; 57 uint32_t edi; 58 uint32_t esi; 59 60 61 // 執行緒第一次執行時,eip指向待呼叫的函式kernel_thread 其它時候,eip是指向switch_to的返回地址 62 void (*eip) (thread_func* func, void* func_arg); 63 64 /***** 以下僅供第一次被排程上cpu時使用 ****/ 65 66 // 引數unused_ret只為佔位置充數為返回地址 67 void (*unused_retaddr); 68 thread_func* function; // 由kernel_thread所呼叫的函式名 69 void* func_arg; // 由kernel_thread所呼叫的函式所需的引數 70 }; 71 72 // 程序或執行緒的 pcb 程式控制塊 73 struct task_struct { 74 uint32_t* self_kstack; // 各核心執行緒都用自己的核心棧 75 enum task_status status; 76 uint8_t priority; // 執行緒優先順序 77 char name[16]; 78 uint32_t stack_magic; // 棧的邊界標記,用於檢測棧溢位 79 }; 80 81 #endifthread.h
程式碼解讀
寫程式碼的順序是先寫定義,再寫實現,最後再呼叫它。但看程式碼我還是喜歡正著看,這樣知道正向的呼叫邏輯
- main 方法:main 方法裡呼叫了一個 thread_start 函式,將執行緒名、優先順序、執行緒函式的地址、引數傳了進去
1 int main(void){ 2 put_str("I am kernel\n"); 3 init_all(); 4 thread_start("k_thread_a", 31, k_thread_a, "argA "); 5 while(1); 6 return 0; 7 } 8 9 void k_thread_a(void* arg) { 10 char* para = arg; 11 while(1) { 12 put_str(para); 13 } 14 }
- thread_start 函式:thread_start 函式首先申請了一塊記憶體用於儲存 task_struct 結構的 thread 變數,然後作為引數分別呼叫了 init_thread 和 thread_create,最後一句彙編語句結束。顯然最後的彙編語句是函式被執行起來的直接原因,我們先放一放。
1 struct task_struct* thread_start(char* name, int prio, thread_func function, void* func_arg) { 2 // 申請核心空間的一片記憶體 3 struct task_struct* thread = get_kernel_pages(1); 4 // pcb結構賦值 5 init_thread(thread, name, prio); 6 thread_create(thread, function, func_arg); 7 // 暫時用一句彙編把函式跑起來 8 asm volatile("mov %0, %%esp; pop %%ebp; pop %%ebx; pop %%edi; pop %%esi; ret ": : "g" (thread->self_kstack) : "memory"); 9 return thread; 10 }
- task_struct 結構:記住這個結構,我們看看後面的函式為其賦值為什麼了
1 struct task_struct { 2 uint32_t* self_kstack; // 各核心執行緒都用自己的核心棧 3 enum task_status status; // 執行緒狀態 4 uint8_t priority; // 執行緒優先順序 5 char name[16]; // 執行緒名字 6 uint32_t stack_magic; // 棧的邊界標記,用於檢測棧溢位 7 };
- init_thread 函式:該函式首先將 task_struct 結構的 pthread 全部賦值為 0,之後五行剛好分別給 task_struct 結構的五個變數附上值。其中執行緒的狀態被寫死賦值為 TASK_RUNNING,自己獨有的核心棧被賦值為 pthread 變數所在的記憶體頁的末尾。
1 void init_thread(struct task_struct* pthread, char* name, int prio) { 2 memset(pthread, 0, sizeof(*pthread)); 3 strcpy(pthread->name, name); 4 pthread->status = TASK_RUNNING; 5 pthread->priority = prio; 6 // 執行緒自己在核心態下使用的棧頂地址 7 pthread->self_kstack = (uint32_t*)((uint32_t)pthread + PG_SIZE); 8 pthread->stack_magic = 0x19870916; // 自定義魔數 9 }
- thread_create 函式:該函式就是為 pthread 中的 self_kstack 賦值,我們看賦值之後的結構,我下面畫了個圖
1 void thread_create(struct task_struct* pthread, thread_func function, void* func_arg) { 2 // 先預留中斷使用棧的空間 3 pthread->self_kstack -= sizeof(struct intr_stack); 4 // 再留出執行緒棧空間 5 pthread->self_kstack -= sizeof(struct thread_stack); 6 struct thread_stack* kthread_stack = (struct thread_stack*)pthread->self_kstack; 7 kthread_stack->eip = kernel_thread; 8 kthread_stack->function = function; 9 kthread_stack->func_arg = func_arg; 10 kthread_stack->ebp = kthread_stack->ebx = kthread_stack->esi = kthread_stack->edi = 0; 11 } 12 13 static void kernel_thread(thread_func* function, void* func_arg) { 14 function(func_arg); 15 }
- 最後的彙編語句:這句彙編有點難理解,先簡單看第一個語句,作用就是把 thread->self_kstack 地址作為棧頂,如上圖所示。經過四個 pop 動作後,指向了 *eip,也就是棧頂此時為 kernel_thread 函式,通過 ret 語句便成功執行了這個函式,至於為什麼用 ret 之後再說。該函式的作用,就是將我們最開始傳過去的 function 函式執行了一下。函式執行的直接原因這個謎題終於暫時解開了。
asm volatile("mov %0, %%esp; pop %%ebp; pop %%ebx; pop %%edi; pop %%esi; ret ": : "g" (thread->self_kstack) : "memory");
總結起來一句話:這麼多程式碼實現的,就僅僅給申請的一頁核心的記憶體空間附上值(按照task_struct結構來賦值),而已,為後續工作做準備。
執行
執行 make brun 後,執行效果如下,自然是 main 方法中的函式所寫的那樣,不斷列印 argA 字串
四、通過中斷訊號讓執行緒的時間片遞減
程式碼鳥瞰
1 #include "timer.h" 2 #include "io.h" 3 #include "print.h" 4 #include "thread.h" 5 6 #define IRQ0_FREQUENCY 100 7 #define INPUT_FREQUENCY 1193180 8 #define COUNTER0_VALUE INPUT_FREQUENCY / IRQ0_FREQUENCY 9 #define CONTRER0_PORT 0x40 10 #define COUNTER0_NO 0 11 #define COUNTER_MODE 2 12 #define READ_WRITE_LATCH 3 13 #define PIT_CONTROL_PORT 0x43 14 15 uint32_t ticks; // ticks是核心自中斷開啟以來總共的嘀嗒數 16 17 /* 把操作的計數器 counter_no? 讀寫鎖屬性 rwl? 計數器模式 counter_mode 寫入模式控制暫存器並賦予初始值 counter_value */ 18 static void frequency_set(uint8_t counter_port, uint8_t counter_no, uint8_t rwl, uint8_t counter_mode, uint16_t counter_value) { 19 /* 往控制字暫存器埠 0x43 中寫入控制字 */ 20 outb(PIT_CONTROL_PORT, (uint8_t)(counter_no << 6 | rwl << 4 | counter_mode << 1)); 21 /* 先寫入 counter_value 的低 8 位 */ 22 outb(counter_port, (uint8_t)counter_value); 23 /* 再寫入 counter_value 的高 8 位 */ 24 outb(counter_port, (uint8_t)counter_value >> 8); 25 } 26 27 // 時鐘的中斷處理函式 28 static void intr_timer_handler(void) { 29 struct task_struct* cur_thread = running_thread(); 30 cur_thread->elapsed_ticks++; 31 ticks++; 32 33 if (cur_thread->ticks == 0) { 34 //schedule(); 35 } else { 36 cur_thread->ticks--; 37 } 38 39 } 40 41 /* 初始化 PIT8253 */ 42 void timer_init() { 43 put_str("timer_init start\n"); 44 /* 設定 8253 的定時週期,也就是發中斷的週期 */ 45 frequency_set(CONTRER0_PORT, COUNTER0_NO, READ_WRITE_LATCH, COUNTER_MODE, COUNTER0_VALUE); 46 register_handler(0x20, intr_timer_handler); 47 put_str("timer_init done\n"); 48 }device/timer.c
1 #include "interrupt.h" 2 #include "stdint.h" 3 #include "global.h" 4 #include "io.h" 5 #include "print.h" 6 7 #define PIC_M_CTRL 0x20 // 這裡用的可程式設計中斷控制器是8259A,主片的控制埠是0x20 8 #define PIC_M_DATA 0x21 // 主片的資料埠是0x21 9 #define PIC_S_CTRL 0xa0 // 從片的控制埠是0xa0 10 #define PIC_S_DATA 0xa1 // 從片的資料埠是0xa1 11 12 #define IDT_DESC_CNT 0x81 // 目前總共支援的中斷數 13 14 #define EFLAGS_IF 0x00000200 // eflags暫存器中的if位為1 15 #define GET_EFLAGS(EFLAG_VAR) asm volatile("pushfl; popl %0" : "=g" (EFLAG_VAR)) 16 17 // 中斷門描述符結構體 18 struct gate_desc{ 19 uint16_t func_offset_low_word; 20 uint16_t selector; 21 uint8_t dcount; 22 uint8_t attribute; 23 uint16_t func_offset_high_word; 24 }; 25 26 // 靜態函式宣告,非必須 27 static void make_idt_desc(struct gate_desc* p_gdesc, uint8_t attr, intr_handler function); 28 // 中斷門描述符表的陣列 29 static struct gate_desc idt[IDT_DESC_CNT]; 30 // 用於儲存異常名 31 char* intr_name[IDT_DESC_CNT]; 32 // 定義中斷處理程式陣列,在kernel.asm中定義的intrXXentry。只是中斷處理程式的入口,最終呼叫idt_table中的處理程式 33 intr_handler idt_table[IDT_DESC_CNT]; 34 // 宣告引用定義在kernel.asm中的中斷處理函式入口陣列 35 extern intr_handler intr_entry_table[IDT_DESC_CNT]; 36 // 初始化可程式設計中斷控制器 8259A 37 static void pic_init(void) { 38 39 /*初始化主片 */ 40 outb (PIC_M_CTRL, 0x11); // ICW1: 邊沿觸發,級聯8259, 需要ICW4 41 outb (PIC_M_DATA, 0x20); // ICW2: 起始中斷向量號為0x20, 也就是IR[0-7] 為 0x20 ~ 0x27 42 outb (PIC_M_DATA, 0x04); // ICW3: IR2 接從片 43 outb (PIC_M_DATA, 0x01); // ICW4: 8086 模式, 正常EOI 44 45 /*初始化從片 */ 46 outb (PIC_S_CTRL, 0x11); // ICW1: 邊沿觸發,級聯8259, 需要ICW4 47 outb (PIC_S_DATA, 0x28); // ICW2: 起始中斷向量號為0x28, 也就是IR[8-15]為0x28 ~ 0x2F 48 outb (PIC_S_DATA, 0x02); // ICW3: 設定從片連線到主片的IR2 引腳 49 outb (PIC_S_DATA, 0x01); // ICW4: 8086 模式, 正常EOI 50 51 /*開啟主片上IR0,也就是目前只接受時鐘產生的中斷 */ 52 outb (PIC_M_DATA, 0xfe); 53 outb (PIC_S_DATA, 0xff); 54 55 put_str(" pic_init done\n"); 56 } 57 58 //建立中斷門描述符 59 static void make_idt_desc(struct gate_desc* p_gdesc, uint8_t attr, intr_handler function) { 60 p_gdesc->func_offset_low_word = (uint32_t)function & 0x0000FFFF; 61 p_gdesc->selector = SELECTOR_K_CODE; 62 p_gdesc->dcount = 0; 63 p_gdesc->attribute = attr; 64 p_gdesc->func_offset_high_word = ((uint32_t)function & 0xFFFF0000) >> 16; 65 } 66 67 // 初始化中斷描述符表 68 static void idt_desc_init(void) { 69 int i; 70 for(i = 0; i < IDT_DESC_CNT; i++) { 71 make_idt_desc(&idt[i], IDT_DESC_ATTR_DPL0, intr_entry_table[i]); 72 } 73 put_str(" idt_desc_init done\n"); 74 } 75 76 // 通用的中斷處理函式,一般用在異常出現時的處理 77 static void general_intr_handler(uint8_t vec_nr) { 78 if(vec_nr == 0x27 || vec_nr == 0x2f) { 79 return; 80 } 81 set_cursor(0); 82 int cursor_pos = 0; 83 while(cursor_pos < 320) { 84 put_char(' '); 85 cursor_pos++; 86 } 87 88 set_cursor(0); 89 put_str("!!!!!! exception message begin !!!!!!n"); 90 set_cursor(88); 91 put_str(intr_name[vec_nr]); 92 if (vec_nr == 14) { // PageFault 93 int page_fault_vaddr = 0; 94 asm ("movl %%cr2, %0" : "=r" (page_fault_vaddr)); 95 put_str("\npage fault addr is "); 96 put_int(page_fault_vaddr); 97 } 98 put_str("\n!!!!!!! exception message end !!!!!!\n"); 99 while(1); 100 } 101 102 // 完成一般中斷處理函式註冊及異常名稱註冊 103 static void exception_init(void) { 104 int i; 105 for(i = 0; i < IDT_DESC_CNT; i++) { 106 // 預設為這個,以後會由 register_handler 來註冊具體處理函式 107 idt_table[i] = general_intr_handler; 108 intr_name[i] = "unknown"; 109 } 110 intr_name[0] = "#DE Divide Error"; 111 intr_name[1] = "#DB Debug Exception"; 112 intr_name[2] = "NMI Interrupt"; 113 intr_name[3] = "#BP Breakpoint Exception"; 114 intr_name[4] = "#OF Overflow Exception"; 115 intr_name[5] = "#BR BOUND Range Exceeded Exception"; 116 intr_name[6] = "#UD Invalid Opcode Exception"; 117 intr_name[7] = "#NM Device Not Available Exception"; 118 intr_name[8] = "#DF Double Fault Exception"; 119 intr_name[9] = "Coprocessor Segment Overrun"; 120 intr_name[10] = "#TS Invalid TSS Exception"; 121 intr_name[11] = "#NP Segment Not Present"; 122 intr_name[12] = "#SS Stack Fault Exception"; 123 intr_name[13] = "#GP General Protection Exception"; 124 intr_name[14] = "#PF Page-Fault Exception"; 125 // intr_name[15] 第 15 項是 intel 保留項,未使用 126 intr_name[16] = "#MF x87 FPU Floating-Point Error"; 127 intr_name[17] = "#AC Alignment Check Exception"; 128 intr_name[18] = "#MC Machine-Check Exception"; 129 intr_name[19] = "#XF SIMD Floating-Point Exception"; 130 } 131 132 /* 開中斷並返回開中斷前的狀態*/ 133 enum intr_status intr_enable() { 134 enum intr_status old_status; 135 if (INTR_ON == intr_get_status()) { 136 old_status = INTR_ON; 137 return old_status; 138 } else { 139 old_status = INTR_OFF; 140 asm volatile("sti"); // 開中斷,sti指令將IF位置1 141 return old_status; 142 } 143 } 144 145 /* 關中斷,並且返回關中斷前的狀態 */ 146 enum intr_status intr_disable() { 147 enum intr_status old_status; 148 if (INTR_ON == intr_get_status()) { 149 old_status = INTR_ON; 150 asm volatile("cli" : : : "memory"); // 關中斷,cli指令將IF位置0 151 return old_status; 152 } else { 153 old_status = INTR_OFF; 154 return old_status; 155 } 156 } 157 158 /* 將中斷狀態設定為status */ 159 enum intr_status intr_set_status(enum intr_status status) { 160 return status & INTR_ON ? intr_enable() : intr_disable(); 161 } 162 163 /* 獲取當前中斷狀態 */ 164 enum intr_status intr_get_status() { 165 uint32_t eflags = 0; 166 GET_EFLAGS(eflags); 167 return (EFLAGS_IF & eflags) ? INTR_ON : INTR_OFF; 168 } 169 170 // 完成有關中斷到所有初始化工作 171 void idt_init() { 172 put_str("idt_init start\n"); 173 idt_desc_init(); // 初始化中斷描述符表 174 exception_init(); // 初始化通用中斷處理函式 175 pic_init(); // 初始化8259A 176 177 // 載入idt 178 uint64_t idt_operand = ((sizeof(idt) - 1) | ((uint64_t)((uint32_t)idt << 16))); 179 asm volatile("lidt %0" : : "m" (idt_operand)); 180 put_str("idt_init done\n"); 181 } 182 183 // 註冊中斷處理函式 184 void register_handler(uint8_t vector_no, intr_handler function) { 185 idt_table[vector_no] = function; 186 }interrupt.c
1 #include "thread.h" 2 #include "stdint.h" 3 #include "string.h" 4 #include "global.h" 5 #include "memory.h" 6 #include "list.h" 7 8 #define PG_SIZE 4096 9 10 struct task_struct* main_thread; // 主執行緒 PCB 11 struct list thread_ready_list; // 就緒佇列 12 struct list thread_all_list; // 所有任務佇列 13 static struct list_elem* thread_tag; // 用於儲存佇列中的執行緒結點 14 15 extern void switch_to(struct task_struct* cur, struct task_struct* next); 16 17 struct task_struct* running_thread() { 18 uint32_t esp; 19 asm ("mov %%esp, %0" : "=g" (esp)); 20 // 返回esp整數部分,即pcb起始地址 21 return (struct task_struct*)(esp & 0xfffff000); 22 } 23 24 // 由 kernel_thread 去執行 function(func_arg) 25 static void kernel_thread(thread_func* function, void* func_arg) { 26 intr_enable(); 27 function(func_arg); 28 } 29 30 // 初始化執行緒棧 thread_stack 31 void thread_create(struct task_struct* pthread, thread_func function, void* func_arg) { 32 // 先預留中斷使用棧的空間 33 pthread->self_kstack -= sizeof(struct intr_stack); 34 35 // 再留出執行緒棧空間 36 pthread->self_kstack -= sizeof(struct thread_stack); 37 struct thread_stack* kthread_stack = (struct thread_stack*)pthread->self_kstack; 38 kthread_stack->eip = kernel_thread; 39 kthread_stack->function = function; 40 kthread_stack->func_arg = func_arg; 41 kthread_stack->ebp = kthread_stack->ebx = kthread_stack->esi = kthread_stack->edi = 0; 42 } 43 44 // 初始化執行緒基本資訊 45 void init_thread(struct task_struct* pthread, char* name, int prio) { 46 memset(pthread, 0, sizeof(*pthread)); 47 strcpy(pthread->name, name); 48 49 if (pthread == main_thread) { 50 pthread->status = TASK_RUNNING; 51 } else { 52 pthread->status = TASK_READY; 53 } 54 pthread->priority = prio; 55 // 執行緒自己在核心態下使用的棧頂地址 56 pthread->self_kstack = (uint32_t*)((uint32_t)pthread + PG_SIZE); 57 pthread->ticks = prio; 58 pthread->elapsed_ticks = 0; 59 pthread->pgdir = NULL; 60 pthread->stack_magic = 0x19870916; // 自定義魔數 61 } 62 63 // 建立一優先順序為 prio 的執行緒,執行緒名為 name,執行緒所執行的函式為 function_start 64 struct task_struct* thread_start(char* name, int prio, thread_func function, void* func_arg) { 65 // pcb 都位於核心空間,包括使用者程序的 pcb 也是在核心空間 66 struct task_struct* thread = get_kernel_pages(1); 67 68 init_thread(thread, name, prio); 69 thread_create(thread, function, func_arg); 70 71 list_append(&thread_ready_list, &thread->general_tag); 72 list_append(&thread_all_list, &thread->all_list_tag); 73 74 return thread; 75 } 76 77 static void make_main_thread(void) { 78 main_thread = running_thread(); 79 init_thread(main_thread, "main", 31); 80 list_append(&thread_all_list, &main_thread->all_list_tag); 81 }thread.c
程式碼解讀
上節我們通過 main 函式呼叫
thread_start("k_thread_a", 31, k_thread_a, "argA ")
僅僅使得一個執行緒的結構,也就是 PCB 被附上了值。並且假裝讓它跑了起來,但跑起來就停不下來了。本節目的就是通過加入中斷,在中斷程式碼處用一些手段來改變這個現狀。
1 // 時鐘的中斷處理函式 2 static void intr_timer_handler(void) { 3 struct task_struct* cur_thread = running_thread(); 4 cur_thread->elapsed_ticks++; 5 ticks++; 6 if (cur_thread->ticks == 0) { 7 //schedule(); 8 } else { 9 cur_thread->ticks--; 10 } 11 } 12 13 /* 初始化 PIT8253 */ 14 void timer_init() { 15 frequency_set(CONTRER0_PORT, COUNTER0_NO, READ_WRITE_LATCH, COUNTER_MODE, COUNTER0_VALUE); 16 register_handler(0x20, intr_timer_handler); 17 }
首先從最頂層的 timer.c 看,時鐘中斷處理函式被註冊到了中斷向量表裡,這樣當中斷來臨時就會執行。每次時鐘中斷一來,就 獲取一下當前的執行緒,並判斷當前執行緒的 ticks 是否到 0 了,如果到了則執行函式 schedule(),也就是我們下一節要實現的 任務切換,如果沒到 0,就遞減。這段程式碼順理成章,很好理解。下面我們深入細節,也就是 ticks 是什麼意思呢?
首先我們看 task_struct 這個結構的變化,增加了一些引數
1 struct task_struct { 2 uint32_t* self_kstack; 3 pid_t pid; 4 enum task_status status; 5 char name[TASK_NAME_LEN]; 6 uint8_t priority; 7 uint8_t ticks; // 每次在處理器上執行的時間嘀嗒數 8 uint32_t elapsed_ticks; // 此任務自上cpu執行後至今佔用了多少cpu嘀嗒數 9 struct list_elem general_tag; // 執行緒在一般的佇列中的結點 10 struct list_elem all_list_tag; // 執行緒佇列thread_all_list中的結點 11 uint32_t* pgdir; // 程序自己頁表的虛擬地址 12 struct virtual_addr userprog_vaddr; // 使用者程序的虛擬地址 13 struct mem_block_desc u_block_desc[DESC_CNT]; // 使用者程序記憶體塊描述符 14 int32_t fd_table[MAX_FILES_OPEN_PER_PROC]; // 已開啟檔案陣列 15 uint32_t cwd_inode_nr; // 程序所在的工作目錄的inode編號 16 pid_t parent_pid // 父程序pid 17 int8_t exit_status; // 程序結束時自己呼叫exit傳入的引數 18 uint32_t stack_magic; // 用這串數字做棧的邊界標記,用於檢測棧的溢位 19 };
有些多,因為我把很久之後需要的也加上了,只看黃色部分即可。
前兩個就是時間,一個是 剩餘時間,一個是 流逝時間,很顯然是留給後面時鐘中斷去 遞減 和 遞增 的,毫無神祕感。
後面兩個 list 結構裡面的節點的變數,分別是指向兩個重要佇列的節點,佇列後面再說
下面看這些新增的結構,是怎麼被 thread.c 賦值並且利用的
1 .... 2 3 struct task_struct* main_thread; // 主執行緒 PCB 4 struct list thread_ready_list; // 就緒佇列 5 struct list thread_all_list; // 所有任務佇列 6 static struct list_elem* thread_tag; // 用於儲存佇列中的執行緒結點 7 8 ... 9 10 struct task_struct* running_thread() { 11 uint32_t esp; 12 asm ("mov %%esp, %0" : "=g" (esp)); 13 // 返回esp整數部分,即pcb起始地址 14 return (struct task_struct*)(esp & 0xfffff000); 15 } 16 17 ... 18 19 // 初始化執行緒基本資訊 20 void init_thread(struct task_struct* pthread, char* name, int prio) { 21 memset(pthread, 0, sizeof(*pthread)); 22 strcpy(pthread->name, name); 23 if (pthread == main_thread) { 24 pthread->status = TASK_RUNNING; 25 } else { 26 pthread->status = TASK_READY; 27 } 28 pthread->priority = prio; 29 // 執行緒自己在核心態下使用的棧頂地址 30 pthread->self_kstack = (uint32_t*)((uint32_t)pthread + PG_SIZE); 31 pthread->ticks = prio; 32 pthread->elapsed_ticks = 0; 33 pthread->pgdir = NULL; 34 pthread->stack_magic = 0x19870916; // 自定義魔數 35 } 36 37 struct task_struct* thread_start(char* name, int prio, thread_func function, void* func_arg) { 38 struct task_struct* thread = get_kernel_pages(1); 39 init_thread(thread, name, prio); 40 thread_create(thread, function, func_arg); 41 list_append(&thread_ready_list, &thread->general_tag); 42 list_append(&thread_all_list, &thread->all_list_tag); 43 return thread; 44 } 45 46 static void make_main_thread(void) { 47 main_thread = running_thread(); 48 init_thread(main_thread, "main", 31); 49 list_append(&thread_all_list, &main_thread->all_list_tag); 50 }
程式碼只需要看我們重要的變化部分,也就是黃色部分即可。
首先我們增加了兩個佇列(這個是個新資料結構,也是我們定義的,這個細節就不再講解了,相信佇列大家都知道)
- thread_ready_list:就緒佇列
- thread_all_list:所有佇列
接下來我們提供了一個可以獲取到當前執行緒的 task_struct 結構體的 running_thread 方法,其實就是取 esp 的整數頁的開頭部分
接下來我們把 init_thread 的方法,為 ticks 和 elapsed_ticks 賦值,ticks 簡單地等於 prio,說明優先順序與分的時間片呈簡單的線性關係(相等)
最後 thread_start 不再假裝地直接運行了,而是把執行緒加入到佇列中,由另一段程式碼不斷從佇列中取出然後執行
現在我們的執行緒,終於開始有點模樣了。
五、實現執行緒切換
執行緒的結構,以及通過時鐘改變關鍵的變數,都已經萬事俱備了,這部分主要就是實現還未實現的 schedule 函式,也就是執行緒切換
程式碼解讀
shedule 函式很簡單,就是把當前執行緒放到佇列中,再從佇列中取出一個執行緒開始執行,通過 c 和彙編的組合來實現
1 // 實現任務排程 2 void schedule() { 3 struct task_struct* cur = running_thread(); 4 if (cur->status == TASK_RUNNING) { 5 // 只是時間片到了,加入就緒佇列隊尾 6 list_append(&thread_ready_list, &cur->general_tag); 7 cur->ticks = cur->priority; 8 cur->status = TASK_READY; 9 } else { 10 // 需要等某事件發生後才能繼續上 cpu,不加入就緒佇列 11 } 12 13 thread_tag = NULL; 14 // 就緒佇列取第一個,準備上cpu 15 thread_tag = list_pop(&thread_ready_list); 16 struct task_struct* next = elem2entry(struct task_struct, general_tag, thread_tag); 17 next->status = TASK_RUNNING; 18 switch_to(cur, next); 19 }
1 [bits 32] 2 section .text 3 global switch_to 4 switch_to: 5 ;棧中此處時返回地址 6 push esi 7 push edi 8 push ebx 9 push ebp 10 mov eax,[esp+20] ;得到棧中的引數cur 11 mov [eax],esp ;儲存棧頂指標esp,task_struct的self_kstack欄位 12 13 mov eax,[esp+24] ;得到棧中的引數next 14 mov esp,[eax] 15 pop ebp 16 pop ebx 17 pop edi 18 pop esi 19 ret
該函式是任務切換的關鍵,但程式碼十分清晰,大家自己品味一下
還有一個問題沒有解決,就是我們每次開一個執行緒,都是將他加到佇列裡,那必然就得有第一個預設被執行的且加到了佇列裡的執行緒,不然一切無法開始呀
1 // 初始化執行緒環境 2 void thread_init(void) { 3 put_str("thread_init_start\n"); 4 list_init(&thread_ready_list); 5 list_init(&thread_all_list); 6 make_main_thread(); 7 put_str("thread_init done\n"); 8 }
就是這段程式碼,把我們 main 方法首先建立成了一個執行緒,這就是 一切的開始,之後的作業系統,便開啟了 中斷驅動的死迴圈 生涯。
最後 main 方法建立兩個執行緒看看效果
1 #include "print.h" 2 #include "init.h" 3 #include "thread.h" 4 5 void k_thread_a(void*); 6 void k_thread_b(void*); 7 8 int main(void){ 9 put_str("I am kernel\n"); 10 init_all(); 11 thread_start("k_thread_a", 31, k_thread_a, "argA "); 12 thread_start("k_thread_b", 8, k_thread_b, "argB "); 13 intr_enable(); 14 while(1) { 15 put_str("Main "); 16 } 17 return 0; 18 } 19 20 void k_thread_a(void* arg) { 21 char* para = arg; 22 while(1) { 23 put_str(para); 24 } 25 } 26 27 void k_thread_b(void* arg) { 28 char* para = arg; 29 while(1) { 30 put_str(para); 31 } 32 }
執行
還算符合預期,不過留了兩個坑,你發現了麼?哈哈我們得下講才能解決
寫在最後:開源專案和課程規劃
如果你對自制一個作業系統感興趣,不妨跟隨這個系列課程看下去,甚至加入我們(下方有公眾號和小助手微信),一起來開發。
參考書籍
《作業系統真相還原》這本書真的贊!強烈推薦
專案開源
專案開源地址:https://gitee.com/sunym1993/flashos
當你看到該文章時,程式碼可能已經比文章中的又多寫了一些部分了。你可以通過提交記錄歷史來檢視歷史的程式碼,我會慢慢梳理提交歷史以及專案說明文件,爭取給每一課都準備一個可執行的程式碼。當然文章中的程式碼也是全的,採用複製貼上的方式也是完全可以的。
如果你有興趣加入這個自制作業系統的大軍,也可以在留言區留下您的聯絡方式,或者在 gitee 私信我您的聯絡方式。
課程規劃
本課程打算出系列課程,我寫到哪覺得可以寫成一篇文章了就寫出來分享給大家,最終會完成一個功能全面的作業系統,我覺得這是最好的學習作業系統的方式了。所以中間遇到的各種坎也會寫進去,如果你能持續跟進,跟著我一塊寫,必然會有很好的收貨。即使沒有,交個朋友也是好的哈哈。
目前的系列包括
- 【自制作業系統01】硬核講解計算機的啟動過程
- 【自制作業系統02】環境準備與啟動區實現
- 【自制作業系統03】讀取硬碟中的資料
- 【自制作業系統04】從真實模式到保護模式
- 【自制作業系統05】開啟記憶體分頁機制
- 【自制作業系統06】終於開始用 C 語言了,第一行核心程式碼!
- 【自制作業系統07】深入淺出特權級
- 【自制作業系統08】中斷
- 【自制作業系統09】中斷的程式碼實現
- 【自制作業系統10】記憶體管理系統
- 【自制作業系統11】中場休息之細節是魔鬼
微信公眾號
我要去阿里(woyaoquali)
小助手微訊號
Angel(angel19980323)