1. 程式人生 > >【自制作業系統12】熟悉而陌生的多執行緒

【自制作業系統12】熟悉而陌生的多執行緒

一、到目前為止的程式流程圖

為了讓大家清楚目前的程式進度,畫了到目前為止的程式流程圖,如下。紅色部分是我們今天要實現的

 

二、程序與執行緒簡述

相信看這篇文章的人,肯定不是對基本概念感興趣,這也不是我的主要目的。所以這裡真的是簡述一下

程序和執行緒都是 獨立的程式執行流,只不過程序有自己獨立的記憶體空間,同一個程序裡的執行緒共享記憶體空間,具體體現在 pcb 表中一個欄位上,指向頁表的地址值。

執行緒分 使用者執行緒 和 核心執行緒,使用者執行緒可以理解為就是沒有執行緒,只是使用者程式中寫了一個執行緒排程器程式在假裝切換,作業系統根本無感知。

 

三、實現一個簡單的單執行緒

我們分三步實現最終的多執行緒機制,其實就對應著下面三節的內容

  1. 第一步實現 多執行緒資料結構,並裝模做樣地把一個執行緒的函式跑起來
  2. 第二步實現 中斷訊號不斷遞減執行緒的時間,達到執行緒被換下 cpu 的條件
  3. 第三步實現 任務切換,即是第二步的條件達到時,真正的切換任務的函式實現

那麼本節先實現第一步,先看程式碼

程式碼鳥瞰

 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 }
main.c
 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 }
thread.c
 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 #endif
thread.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)