【自制操做系統12】熟悉而陌生的多線程

1、到目前爲止的程序流程圖

爲了讓你們清楚目前的程序進度,畫了到目前爲止的程序流程圖,以下。紅色部分是咱們今天要實現的html

 

2、進程與線程簡述

相信看這篇文章的人,確定不是對基本概念感興趣,這也不是個人主要目的。因此這裏真的是簡述一下node

進程和線程都是 獨立的程序執行流,只不過進程有本身獨立的內存空間,同一個進程裏的線程共享內存空間,具體體如今 pcb 表中一個字段上,指向頁表的地址值。git

線程分 用戶線程內核線程,用戶線程能夠理解爲就是沒有線程,只是用戶程序中寫了一個線程調度器程序在僞裝切換,操做系統根本無感知。編程

 

3、實現一個簡單的單線程

咱們分三步實現最終的多線程機制,其實就對應着下面三節的內容數組

  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 字符串app

 

4、經過中斷信號讓線程的時間片遞減

代碼鳥瞰

 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 函數調用ide

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 再也不僞裝地直接運行了,而是把線程加入到隊列中,由另外一段代碼不斷從隊列中取出而後運行

如今咱們的線程,終於開始有點模樣了。

 

5、實現線程切換

線程的結構,以及經過時鐘改變關鍵的變量,都已經萬事俱備了,這部分主要就是實現還未實現的 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 私信我您的聯繫方式。

課程規劃

本課程打算出系列課程,我寫到哪以爲能夠寫成一篇文章了就寫出來分享給你們,最終會完成一個功能全面的操做系統,我以爲這是最好的學習操做系統的方式了。因此中間遇到的各類坎也會寫進去,若是你能持續跟進,跟着我一塊寫,必然會有很好的收貨。即便沒有,交個朋友也是好的哈哈。

目前的系列包括

 微信公衆號

  我要去阿里(woyaoquali)

 小助手微信號

Angel(angel19980323)

相關文章
相關標籤/搜索