【自制操作系统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 私信我您的联系方式。

课程规划

本课程打算出系列课程,我写到哪觉得可以写成一篇文章了就写出来分享给大家,最终会完成一个功能全面的操作系统,我觉得这是最好的学习操作系统的方式了。所以中间遇到的各种坎也会写进去,如果你能持续跟进,跟着我一块写,必然会有很好的收货。即使没有,交个朋友也是好的哈哈。

目前的系列包括

微信公众号

  我要去阿里(woyaoquali)

 小助手微信号

  Angel(angel19980323)

原文地址:https://www.cnblogs.com/flashsun/p/12385901.html

时间: 2024-11-08 20:59:40

【自制操作系统12】熟悉而陌生的多线程的相关文章

【自制操作系统14】实现键盘输入

一.到目前为止的程序流程图 为了让大家清楚目前的程序进度,画了到目前为止的程序流程图,如下.(红色部分就是我们今天要实现的) 二.简单打通键盘中断 既然要打通键盘中断,那必然需要你回顾一下 [自制操作系统08]中断 所讲述的外部中断的流程,下面我把图贴上. 如图所示,将上图中的某外部设备,换成下图中的具体的键盘,就是键盘中断流程啦.简单说就是: 因此每当有击键发生时,键盘中的设备 8048 会把键盘扫描码发给主板上的设备 8042. 8042 是按字节来处理的,每处理一个字节的扫描码后,将其存储

30天自制操作系统之第12天 定时器

定时器的中断处理程序要保证高效率,需要进行一些优化,这里介绍优化的方法.对于一个操作系统来说,会有多个定时器,假设该操作系统维护了500个定时器,当每一次定时中断发生时(这里我们设定1秒发生100次中断),调用中断处理程序,中断处理程序会对这500个定时器进行if判断,看哪些正在被使用,这样1秒内,就会有500X100=10000次if判断,而中断处理程序最讲究节省时间.实际上,我们不必每发生一次定时中断就去对这500个定时器进行判断.因为假设我们使用了500个定时器中的10个,而10个定时器中

《30天自制操作系统》笔记(12)——多任务入门

<30天自制操作系统>笔记(12)——多任务入门 进度回顾 上一篇介绍了设置显示器高分辨率的方法.本篇讲一下操作系统实现多任务的方法. 什么是多任务 对程序员来说,也许这是废话,不过还是说清楚比较好. 多任务就是让电脑同时运行多个程序(如一边写代码一边听音乐一边下载电影). 电脑的CPU只有固定有限的那么一个或几个,不可能真的同时运行多个程序.所以就用近似的方式,让多个程序轮换着运行.当轮换速度够快(0.01秒),给人的感觉就是"同时"运行了. 多任务之不实用版 我们首先从

《30天自制操作系统》读书笔记(2)hello, world

让系统跑起来 要写一个操作系统,我们首先要有一个储存系统的介质,原版书似乎是06年出版的,可惜那时候没有电脑,没想到作者用的还是软盘,现在的电脑谁有软驱?不得已我使用一张128M的SD卡来代替,而事实上你用的是U盘还是软盘对我们的操作系统没有影响,缺点是你的U盘刷入系统后容量只能是1440 MB,即当年流行的3.5英寸软盘的大小,当然不用担心,再格式化一次(用DiskGeniu),就可以恢复. 我做事情的话,总是怕自己的努力的结果白费了,害怕辛辛苦苦看完这本书但是发现做出来的东西现在根本没法用,

自制操作系统(十) 图像叠加处理

2016.07.12 参考书籍:<30天自制操作系统>.<自己动手写操作系统> qq:992591601  欢迎交流 图像叠加处理的原理很简单,就是给图像分层,从低下往上面画,便可以实现叠加的效果.例如,屏幕背景+一个窗口+鼠标的情况. 例如下面的情形: 计算机桌面上有三个窗口程序,A.B.C.B位于A之上,C位于B之上.要实现这种效果,只需要,先画A(盖住了桌面一部分).再画B(盖住了A和桌面一部分).再画C(盖住了B和桌面一部分).然后每隔一定时间刷新画面即可. 为此引入一个图

《30天自制操作系统》读书笔记(4) 绘图

暑假果然是滋生懒散的温床. (╯‵□′)╯︵┻━┻ 好久不动都忘记之前做到哪里了, 上次好像做到了C语言的引入, 这一节所做的东西都相当轻松, 将会绘制出操作系统的基本界面. 绘图的原理 按照书中所说, 将值写入到显存中就能在屏幕上显示相应的像素, 在asmhead.nas 中有这一段: 1 CYLS EQU 0x0ff0 ; 设定启动区 2 LEDS EQU 0x0ff1 3 VMODE EQU 0x0ff2 ; 关于颜色数目的信息,颜色的位数 4 SCRNX EQU 0x0ff4 ; 分辨率

&lt;&lt;30天自制操作系统&gt;&gt;(1)初体验汇编程序

我们这次使用的汇编语言编译器是原书作者自己开发的,名为“nask”,很多语法和著名的汇编语言编译器nasm很像.由于原书作者没有给出有哪些不同,这里就无法给出不同了! 现在仅仅使用汇编语言中的DB指令来写个“操作系统”吧. DB指令是"define byte"的缩写,往文件里写入1个字节 超长的源代码 1 DB 0xeb, 0x4e, 0x90, 0x48, 0x45, 0x4c, 0x4c, 0x4f 2 DB 0x49, 0x50, 0x4c, 0x00, 0x02, 0x01,

《30天自制操作系统》笔记(08)——叠加窗口刷新

<30天自制操作系统>笔记(08)--叠加窗口刷新 进度回顾 上一篇中介绍了内存管理的思路和算法,我们已经可以动态申请和释放内存了.这不就是堆(Heap)么.在此基础上,本篇要做一段程序,一并解决窗口和鼠标的叠加处理问题. 问题 在之前的<<30天自制操作系统>笔记(05)--启用鼠标键盘>篇,已经能够移动鼠标了.但是遗留了如下图所示的一个小问题. 我们希望的情形是这样的: 实际上,当前版本的OS还没有窗口图层的东西.本篇要做一段程序,一并解决窗口和鼠标的叠加处理问题.

《30天自制操作系统》笔记(01)——hello bitzhuwei’s OS!

<30天自制操作系统>笔记(01)--hello bitzhuwei's OS! 最初的OS代码 1 ; hello-os 2 ; TAB=4 3 4 ORG 0x7c00 ; 指明程序的装载地址 5 6 ; 以下这段是标准FAT32格式软盘专用的代码 7 8 JMP entry 9 DB 0x90 10 DB "HELLOIPL" ; freeparam 启动区的名称可以是任意的字符串(8字节) 11 DW 512 ; 每个扇区(sector)的大小(必须为512字节)