前言:我们昨天完成了内核的内存池以及内存管理程序,今天我们要揭开操作系统多任务执行的神秘面纱,来了解并实现一个多任务的操作系统。
一,实现内核线程
在聊线程之间我们先聊聊处理器吧,众所周之现在我们的CPU动不动就是几核的,所以大家理所应当的认为他的多任务是处理器之间并行完成的。但是实际上以前的计算机只有单核,那任务不可能串行执行吧,如果任务A执行一天,任务B执行2分钟,那我要为这个2分钟的B等A一天也太不值得了。于是便有了任务调度器这一玩意儿来实现任务之间来回切换,实现伪并行。
1,任务调度器
任务调度器就是把任务轮流调度上处理器运行的一个软件模块,他是操作系统的一部分,调度器中维护一个任务表,按照一定算法从中选定任务,然后放在处理器上面运行,当时间片到达的时候就重新找一个任务放上去,周而复始。
2,执行流
- 执行流就是一段逻辑上独立的指令区域,是人为给处理器安排的处理单元
- 任何代码块,无论大小都可以独立成为执行流,我们只需提前准备好他的上下文环境即可
- 执行流就是进程和线程
3,线程和进程
① 线程
线程就是运行函数的另一种方式。
线程与函数的区别?
线程是作为调度单元(执行流)在处理器上运行的,处理器能“看到”执行流。函数是伴随着调度单元顺带在处理器上执行的,处理器并不能看到函数
打个比方:
在餐馆中,一道菜需要食品+盘子组合起来,就相当于一个调度单位。用户点的宫保鸡丁这道菜就相当于是执行一个线程,而做菜需要需多材料,鸡,花生米这些都是顺带的这些就是函数。如果你想要吃花生米吃的过瘾,可以单点一道花生米,就可以让该函数变成线程。
② 进程 = 线程 + 资源
进程是运行的程序,即进行中的程序,程序必须获取运行所需的各类资源才能成为进程
进程是一种控制流集合,集合至少包括一条执行流,也就是说虽说有单线程进程和多线程进程,但是单线程进程也是拥有一个执行流的,你可以认为线程是进程的并行
线程资源容器
每个进程都有自己独立的资源空间,也就是说进程与进程之间的资源空间很难共享,但是线程是没有自己独立的资源空间的,他是“寄生"在进程之中的,也就是说使用的是进程中的资源,所以线程之间的资源是共享的也就容易发生线程安全问题
打个比方:
在餐馆中,厨子,配菜员,清洁工这些就是线程,而餐馆就是进程,这些员工各司其职,使用餐馆中的资源。
4,任务
任务就是指大的执行流单线程进程,或者是小的执行流线程
而只有线程才具备能动性,他才是处理器的执行单元,是调度器眼中调度资源
5,PCB
PCB是进程的身份证,方便操作系统识别,这里简单说一下寄存器映像,其就是保存进程的”现场“,所有寄存器的值都将保存于此,而栈是进程所使用的0特权级的内核栈,寄存器映像的位置随着栈指针变动而变动。
6,内核态线程与用户态线程
线程实现有两种方式一个是内核态线程一个是用户态线程
- 用户态线程:用户态线程是指在用户特权级空间下实现,也就是由用户实现线程调度器,线程机制等等,一般是由某个权威机构实现封装代码库由用户自己去调用
优点:每次开辟线程,上下文切换无需陷入内核
缺点:
- 在操作系统内核层面他并不认识线程,也就是说当进程中有线程阻塞,一般是进行系统调用等等,整个进程都会挂起
- 对于任务调度器来说,他并不认识线程,只认识进程,也就是说会出现一种情况,如果用户的线程调度器没有实现好,导致一个线程独占CPU,就会导致这个进程中只会有这个线程能进行处理直到时间片耗尽。
- 内核态线程:内核态线程是指在0特权级的特权级空间下实现,线程机制由内核提供
优点:
- 线程表与进程表都由内核管理,线程和进程都作为执行流来轮流使用CPU,这样使得进程的占用率大大提高,比如进程A有4个线程,进程B有1个线程,一共五个线程轮流执行,这样进程A就使用了80%的资源大大提速了
- 线程阻塞不会导致进程挂起,很简单操作系统是认识线程的,他会把线程和进程都当作执行流,这样一个线程阻塞就可以执行进程的另一个线程。
二,编写内核线程
首先我们已经知道了线程切换和调度实际上就是切换执行流,那既然是切换执行流改变CS或者EIP的值,我们可以用哪些指令呢,call?jmp?ret?。不卖关子了,这次我们使用ret指令,也就是返回指令,再讲函数调用之前 我们首先来聊聊ABI
ABI:ABI即应用程序二进制接口,比我们所熟知的API还要底层,他规定了参数如何传递,返回值如何存储,系统调用的实现方式。
在这里我们切换执行流时,需要调用其他线程的函数,这个时候便形成了主调函数和被调函数的关系,此时我们主调函数要维护五个寄存器 ebp,ebx,edi,esi,esp。被调函数维护其余的寄存器。
所以将 线程的函数地址压入栈中,然后函数地址位于栈顶时,利用ret返回栈最上层的函数地址,就形成了执行流的变化
1,线程实现
忘记介绍了我们的线程是拥有状态的,状态大概有这么几种
其中最主要的是RUNNING和READY
RUNNING:正在运行的线程
READY:准备就绪的线程,刚刚创建出来的线程或者时间片到了的线程都可以称位就绪线程
enum task_status {
TASK_RUNNING,
TASK_READY,
TASK_BLOCKED,
TASK_WAITING,
TASK_TIMEWAITING,
TASK_HANGINH,
TASK_DIED
};
thread/thread.h
#ifndef _THREAD_THREAD_H
#define _THREAD_THREAD_H
#include "stdint.h"
//#include "list.h"
typedef void thread_func(void*);
enum task_status {
TASK_RUNNING,
TASK_READY,
TASK_BLOCKED,
TASK_WAITING,
TASK_TIMEWAITING,
TASK_HANGINH,
TASK_DIED
};
/*中断栈*/
struct intr_stack {
uint32_t vec_no; //中断号
uint32_t edi;
uint32_t esi;
uint32_t ebp;
uint32_t esp_dummy;
uint32_t ebx;
uint32_t edx;
uint32_t ecx;
uint32_t eax;
uint32_t gs;
uint32_t fs;
uint32_t es;
uint32_t ds;
uint32_t err_code;
void(*eip) (void);
uint32_t cs;
uint32_t eflags;
void* esp;
uint32_t ss;
};
/*线程栈 thread_stack*/
struct thread_stack {
uint32_t ebp;
uint32_t ebx;
uint32_t edi;
uint32_t esi;
//线程第一次执行时,eip指向待调用的函数kernel_thread其他的时候指向switch_to的返回地址
void(*eip) (thread_func* func, void* func_arg);
//以下仅第一次被调度上CPU使用
void(*unused_retaddr);
thread_func* function; //由kernel_thread 所调用的函数名
void* func_arg; //由kernel_thread 所调用的函数所需的参数
};
/*PCB*/
struct task_struct {
uint32_t* self_kstack;
enum task_status status;
char name[16];
uint8_t priority;
//uint8_t ticks; //每次在处理器上执行的时间
//uint32_t elapsed_ticks; //此任务已经执行的时间
//struct list_elem general_tag; //线程在一般队列中的节点
//struct list_elem all_list_tag; //线程队列thread_all_list的节点
//uint32_t* pgdir;
uint32_t stack_magic; //标记栈溢出
};
struct task_struct* thread_start(char* name, int prio, thread_func function, void* func_arg);
#endif // !_THREAD_THREAD_H
thread/thread.c
#include "thread.h"
#include "stdint.h"
#include "string.h"
#include "global.h"
#include "memory.h"
//#include "list.h"
#define PG_SIZE 4096
/*
struct tasl_struct* main_thread; //主线程PCB
struct list thread_ready_list; //就绪队列
struct list thread_all_list; //全部队列
static struct list_elem* thread_tag; //保存队列中的线程节点
*/
//extern void switch_to(struct task_struct* cur, struct task_struct* next);
static void kernel_thread(thread_func* function, void* func_arg) {
//intr_enable(); //打开时钟,防止时钟中断被屏蔽
function(func_arg);
}
void thread_create(struct task_struct* pthread, thread_func function, void* func_arg) {
//预留中断栈空间和线程栈空间
pthread->self_kstack -= sizeof(struct intr_stack);
pthread->self_kstack -= sizeof(struct thread_stack);
struct thread_stack* kthread_stack = (struct thread_stack*)pthread->self_kstack;
kthread_stack->eip = kernel_thread;
kthread_stack->function = function;
kthread_stack->func_arg = func_arg;
kthread_stack->ebp = kthread_stack->ebx = kthread_stack->esi = kthread_stack->edi = 0;
}
/*初始化线程基本信息*/
void init_thread(struct task_struct* pthread, char* name, int prio) {
memset(pthread, 0, sizeof(*pthread));
strcpy(pthread->name, name);
pthread->status = TASK_RUNNING;
pthread->priority = prio;
//指向PCB顶端
pthread->self_kstack = (uint32_t*)((uint32_t)pthread + PG_SIZE);
pthread->stack_magic = 0x19870916; //自定义魔数 作为边缘数检测是否出现栈溢出
}
struct task_struct* thread_start(char* name, int prio, thread_func function, void* func_arg) {
struct task_struct* thread = get_kernel_pages(1);
init_thread(thread, name, prio);
thread_create(thread, function, func_arg);
//使相应的值弹入相应的寄存器,然后用ret调用eip中的方法
asm volatile("movl %0, %%esp; \
pop %%ebp; pop %%ebx; pop %%edi; pop %%esi; \
ret": : "g" (thread->self_kstack) : "memory");
return thread;
}
kernel/main.c
#include "print.h"
#include "init.h"
#include "debug.h"
#include "thread.h"
//注意这里不能将g_thread放在main上面,否则会改变main函数入口地址出现错误
void g_thread(void* arg);
void main(void) {
put_str("Hello GeniusOS\n");
put_int(2023);
put_str("\n");
init_all();
thread_start("genius", 5, g_thread, "genius");
intr_enable();
while (1) {
put_str("Main ");
}
}
void g_thread(void* arg) {
char* para = arg;
while (1) {
put_str(para);
}
}
(makefile省略相信你们应该会添加,最后我再放出全部的)
编译运行,运行结果,出现以下结果就说明成功了,但是现在还不是时候庆祝我们接着往下走。
三,双向链表--管理任务的关键数据结构
双向链表我就不多说了,如果你们学过链表的话,双向链表就是多了一个指向前节点的指针,就是这么简单,该部分的链表功能可以自己实现,也可以直接copy我的代码,这里不是重点,所以我贴代码直接跳过了。如果你不知道链表的话,建议点击链接了解一下:链表
lib/kernel/list.h
#ifndef __LIB_KERNEL_LIST_H
#define __LIB_KERNEL_LIST_H
#include "stdint.h"
#define offset(struct_type,member) (int) (&((struct_type*)0)->member) //不是很理解这里写的是什么
#define elem2entry(struct_type,struct_member_name,elem_ptr) \
(struct_type*)((int)elem_ptr - offset(struct_type,struct_member_name)) //也不是很理解这里 后面会介绍
struct list_elem
{
struct list_elem* prev; //前面的节点
struct list_elem* next; //后面的节点
};
struct list
{
struct list_elem head; // 亘古不变的头部
struct list_elem tail; // 亘古不变的尾部
};
typedef bool (function)(struct list_elem*, int arg);
void list_init(struct list*);
void insert(struct list_elem* before, struct list_elem* elem);
void push(struct list* plist, struct list_elem* elem);
void append(struct list* plist, struct list_elem* elem);
void remove(struct list_elem* pelem);
struct list_elem* pop(struct list* plist);
bool empty(struct list* plist);
uint32_t len(struct list* plist);
struct list_elem* traversal(struct list* plist, function func, int arg);
bool find(struct list* plist, struct list_elem* obj_elem);
#endif
lib/kernel/list.c
#include "list.h"
#include "interrupt.h"
#include "stdint.h"
#include "debug.h"
#define NULL 0
//初始化双向链表
void list_init(struct list* list)
{
list->head.prev = NULL;
list->head.next = &list->tail;
list->tail.prev = &list->head;
list->tail.next = NULL;
}
//把链表 elem放在 before前面
void insert(struct list_elem* before, struct list_elem* elem)
{
enum intr_status old_status = intr_disable();
elem->next = before;
elem->prev = before->prev;
before->prev->next = elem;
before->prev = elem;
intr_set_status(old_status);
}
//添加元素到链表队首
void push(struct list* plist, struct list_elem* elem)
{
insert(plist->head.next, elem);
}
//添加元素到链表队尾
void append(struct list* plist, struct list_elem* elem)
{
insert(&plist->tail, elem);
}
//让pelem脱离链表
void remove(struct list_elem* pelem)
{
enum intr_status old_status = intr_disable();
pelem->prev->next = pelem->next;
pelem->next->prev = pelem->prev;
intr_set_status(old_status);
}
//让链表的第一个元素脱离链表
struct list_elem* pop(struct list* plist)
{
ASSERT(plist->head.next != &plist->tail);
struct list_elem* ret = plist->head.next;
remove(plist->head.next);
return ret;
}
bool empty(struct list* plist)
{
return (plist->head.next == &plist->tail ? true : false);
}
uint32_t len(struct list* plist)
{
uint32_t ret = 0;
struct list_elem* next = plist->head.next;
while (next != &plist->tail)
{
next = next->next;
++ret;
}
return ret;
}
struct list_elem* traversal(struct list* plist, function func, int arg)
{
struct list_elem* elem = plist->head.next;
if (empty(plist)) return NULL;
while (elem != &plist->tail)
{
if (func(elem, arg)) return elem;
elem = elem->next;
}
return NULL;
}
bool find(struct list* plist, struct list_elem* obj_elem)
{
struct list_elem* ptr = plist->head.next;
while (ptr != &plist->tail)
{
if (ptr == obj_elem) return true;
ptr = ptr->next;
}
return false;
}
四,实现多线程调度
🆗,我们正式来进行多线程调度的编写,这里我们来理一下整个多线程调度的结构
策略: RR算法(时间片轮转算法),根据线程的时间片来轮流调度线程,当线程执行完便放入就绪队列。
队列:就绪队列和运行队列(main函数一开始就在运行队列中)
执行流程:在进行多线程调度的时候需要打开中断,由时钟中断不断计算时间片,然后触发中断事件,进行线程调度切换。
1,改造线程
话不多说我们先来改造我们的thread代码
thread/thread.h
#ifndef _THREAD_THREAD_H #define _THREAD_THREAD_H #include "stdint.h" #include "list.h" typedef void thread_func(void*); enum task_status { TASK_RUNNING, TASK_READY, TASK_BLOCKED, TASK_WAITING, TASK_TIMEWAITING, TASK_HANGINH, TASK_DIED }; /*中断栈*/ struct intr_stack { uint32_t vec_no; //中断号 uint32_t edi; uint32_t esi; uint32_t ebp; uint32_t esp_dummy; uint32_t ebx; uint32_t edx; uint32_t ecx; uint32_t eax; uint32_t gs; uint32_t fs; uint32_t es; uint32_t ds; uint32_t err_code; void(*eip) (void); uint32_t cs; uint32_t eflags; void* esp; uint32_t ss; }; /*线程栈 thread_stack*/ struct thread_stack { uint32_t ebp; uint32_t ebx; uint32_t edi; uint32_t esi; //线程第一次执行时,eip指向待调用的函数kernel_thread其他的时候指向switch_to的返回地址 void(*eip) (thread_func* func, void* func_arg); //以下仅第一次被调度上CPU使用 void(*unused_retaddr); thread_func* function; //由kernel_thread 所调用的函数名 void* func_arg; //由kernel_thread 所调用的函数所需的参数 }; /*PCB*/ struct task_struct { uint32_t* self_kstack; enum task_status status; char name[16]; uint8_t priority; uint8_t ticks; //每次在处理器上执行的时间 uint32_t elapsed_ticks; //此任务已经执行的时间 struct list_elem general_tag; //线程在一般队列中的节点 struct list_elem all_list_tag; //线程队列thread_all_list的节点 uint32_t* pgdir; //页表的虚拟地址 uint32_t stack_magic; //标记栈溢出 }; struct task_struct* thread_start(char* name, int prio, thread_func function, void* func_arg); void schedule(); void thread_init(void); #endif // !_THREAD_THREAD_H
我们在PCB中添加了以下属性:
ticks:该任务的时间片
elapsed_ticks:已经执行的时间
general_tag与all_list_tag在之后会介绍
stack_magic:PCB最后的元素,他是一个魔数,每次切换线程时都会检测魔数完整性,如果完整性错误说明栈溢出了
/kernel/thread.c
#include "thread.h" #include "stdint.h" #include "string.h" #include "global.h" #include "memory.h" #include "list.h" #include "interrupt.h" #include "debug.h" #define PG_SIZE 4096 struct task_struct* main_thread; //主线程PCB struct list thread_ready_list; //就绪队列 struct list thread_all_list; //全部队列 static struct list_elem* thread_tag; //保存队列中的线程节点 extern void switch_to(struct task_struct* cur, struct task_struct* next); /*获取当前正在运行线程的PCB*/ struct task_struct* running_thread(void) { uint32_t esp; asm("mov %%esp,%0" : "=g"(esp)); return (struct task_struct*)(esp & 0xfffff000); } void schedule() { //判断是否关闭中断 ASSERT(intr_get_status() == INTR_OFF); //如果线程在Task-RUNNING状态就进入就绪队列 struct task_struct* runing_task = running_thread(); if (runing_task->status == TASK_RUNNING) { ASSERT(!find(&thread_ready_list, &runing_task->general_tag)); runing_task->status = TASK_READY; runing_task->ticks = runing_task->priority; append(&thread_ready_list, &runing_task->general_tag); } else { /* 还没设计捏 */ } ASSERT(!empty(&thread_ready_list)); thread_tag = NULL; thread_tag = pop(&thread_ready_list); struct task_struct* next = elem2entry(struct task_struct, general_tag, thread_tag); next->status = TASK_RUNNING; switch_to(runing_task, next); } static void kernel_thread(thread_func* function, void* func_arg) { intr_enable(); //打开时钟,防止时钟中断被屏蔽 function(func_arg); } void thread_create(struct task_struct* pthread, thread_func function, void* func_arg) { //预留中断栈空间和线程栈空间 pthread->self_kstack -= sizeof(struct intr_stack); pthread->self_kstack -= sizeof(struct thread_stack); struct thread_stack* kthread_stack = (struct thread_stack*)pthread->self_kstack; kthread_stack->eip = kernel_thread; kthread_stack->function = function; kthread_stack->func_arg = func_arg; kthread_stack->ebp = kthread_stack->ebx = kthread_stack->esi = kthread_stack->edi = 0; } /*初始化线程基本信息*/ void init_thread(struct task_struct* pthread, char* name, int prio) { memset(pthread, 0, sizeof(*pthread)); strcpy(pthread->name, name); if (pthread == main_thread) { pthread->status = TASK_RUNNING; } else { pthread->status = TASK_READY; } pthread->priority = prio; pthread->ticks = prio; pthread->elapsed_ticks = 0; pthread->pgdir = NULL; //指向PCB顶端 pthread->self_kstack = (uint32_t*)((uint32_t)pthread + PG_SIZE); pthread->stack_magic = 0x66666666; //自定义魔数 作为边缘数检测是否出现栈溢出 } struct task_struct* thread_start(char* name, int prio, thread_func function, void* func_arg) { struct task_struct* thread = get_kernel_pages(1); init_thread(thread, name, prio); thread_create(thread, function, func_arg); //队列检查 ASSERT(!find(&thread_ready_list, &thread->general_tag)); append(&thread_ready_list, &thread->general_tag); ASSERT(!find(&thread_all_list, &thread->all_list_tag)); append(&thread_all_list, &thread->all_list_tag); //使相应的值弹入相应的寄存器,然后用ret调用eip中的方法 /*asm volatile("movl %0, %%esp; \ pop %%ebp; pop %%ebx; pop %%edi; pop %%esi; \ ret": : "g" (thread->self_kstack) : "memory");*/ return thread; } /* 将kernel的main函数完善为主线程 */ static void make_main_thread(void) { /* 主线程的PCB早在内存管理时就已经为其预留地址为0xc009e000,所以无需分配页 */ main_thread = running_thread(); init_thread(main_thread, "main", 31); /* main函数时当前线程不能再ready_list中 */ ASSERT(!find(&thread_all_list, &main_thread->all_list_tag)); append(&thread_all_list, &main_thread->all_list_tag); } void thread_init(void) { put_str("thread_init start\n"); list_init(&thread_ready_list); list_init(&thread_all_list); make_main_thread(); put_str("thread_init done"); }
- running_thread
我们之前有说过,线程的PCB内存地址严格按照 0xXXXXX000~0xXXXXXfff,所以获取当前栈指针的位置再和0xfffff000相与便可以得到当前线程PCB的地址
- thread_start
在这里我们便可以说说general_tag与all_list_tag的作用了,大家有没有想过为什么我们链表不存放PCB,而是一个个tag,首先各各线程在内存中是离散的我们需要用链表将其联系起来。其次大家想想一个PCB有多大?4KB,我们链表节点全部都是一个个4KB的PCB未免有点小材大用了,于是乎我们便直接使用其中的general_tag和all_list_tag作为各个链表联系,那么有人会问了,你不连PCB你怎么获取线程中PCB的内容啊?!,别急我们接着往下看
在list.h中我们定义了offset和elem2entry函数, 其作用是获取一个属性在结构体位置中的偏移,大家想想 当前这个属性的位置-在该属性结构体偏移 是不是就等于 结构体的地址 ,所以根据这个函数我们可以通过tag来获取PCB的位置。
2,实现任务调度和任务切换
线程每次在处理器的执行时间有ticks决定,每产生一次时钟中断就将ticks-1,当ticks为0时则返回就绪队列。
(1) 时钟中断处理函数
先修改interrupt.c常规函数添加中断注册函数
static void general_intr_handler(uint8_t vec_nr) {
if (vec_nr == 0x27 || vec_nr == 0x2f) {
return;
}
set_cursor(0);
//清空上方屏幕
int cursor_pos = 0;
while (cursor_pos < 320) {
put_char(' ');
cursor_pos++;
}
set_cursor(0);
put_str("!!!! excetion message begin !!!!\n");
set_cursor(88);
if (vec_nr == 14) {
int page_fault_vaddr = 0;
asm("movl %%cr2,%0": "=r" (page_fault_vaddr)); //缺页问题会将导致PageFault的虚拟地址存访到CR2中
put_str("\npage fault addr is ");put_int(page_fault_vaddr);
}
put_str("!!!! excetion message begin !!!!\n");
while (1);
}
//注册中断
void register_intr(uint32_t vectr, intr_handler func,char* name) {
idt_table[vectr] = func;
intr_name[vectr] = name;
}
device/time.c
#include "timer.h"
#include "io.h"
#include "print.h"
#include "interrupt.h"
#include "thread.h"
#include "debug.h"
#define IRQ0_FREQUENCY 100
#define INPUT_FREQUENCY 1193180
#define COUNTER0_VALUE INPUT_FREQUENCY/IRQ0_FREQUENCY
#define COUNTER0_PORT 0x40
#define COUNTER0_NO 0
#define COUNTER_MODE 2
#define READ_WRITE_LATCH 3
#define PIT_CONTROL_PORT 0x43
uint32_t ticks; //ticks是内核自中断开启以来总共的滴答声
static void intr_timer_handler(void) {
struct task_struct* cur_thread = running_thread();
ASSERT(cur_thread->stack_magic == 0x66666666);
cur_thread->elapsed_ticks++;
ticks++;
if (cur_thread->ticks == 0) {
schedule();
}
else {
cur_thread->ticks--;
}
}
/** 初始化频率 **/
static void frequency_set(uint8_t counter_port,
uint8_t counter_no,
uint8_t rw,
uint8_t mode,
uint16_t counter_value) {
outb(PIT_CONTROL_PORT, (uint8_t)(counter_no<<6 | rw<<4 |mode<<1)); //规定PIT的工作模式
outb(counter_port, (uint8_t)counter_value);
outb(counter_port, (uint8_t)counter_value >> 8);
}
/** 初始化timer **/
void timer_init() {
put_str("timer_init start\n");
frequency_set(COUNTER0_PORT,
COUNTER0_NO,
READ_WRITE_LATCH,
COUNTER_MODE,
COUNTER0_VALUE);
register_intr(0x20, intr_timer_handler,"time");
put_str("timer_init done\n");
}
(2) 调度器 schedule
就是前面thread的schedule()函数
(3) 任务切换函数switch_to
接下来便是我们的重头戏,switch_to函数
首先我们要明白一点为什么要保护任务的上下文,每个任务都有一个执行流,按道理来说应该是从头执行到尾部,结果临时改道,是不是要保护原有的资源和路径才能恢复执行。那么问题又来了,这种“改道”可能是深度多层的,也就是有点类似于递归不断向下执行,那我们的任务切换设计了几层呢 ?
① 首先我们来逐步分析,一开始我们任务时间片到了后,进入时钟中断程序,此时是第一层,该层我们要保护整个任务的上下文环境所有寄存器都需要保存,这个在kernel.S的中断汇编文件中已经实现,此时任务进入内核态
② 由中断进入switch函数要在进行一次执行流变道,此时我们遵循ABI原则,来保护主调函数需要保存的那4个寄存器即可
thread/switch.S
[bits 32]
section .text
global switch_to
switch_to:
;保存内核栈
push esi
push edi
push ebx
push ebp
;得到栈中参数
mov eax,[esp+20] ;获取cur,将cur的esp指针保存在self_kstack中
mov [eax],esp
;上半部分是保护cur线程的栈数据,下半部分是恢复next的栈数据
mov eax,[esp+24] ;得到next参数
mov esp,[eax] ;pcb的第一个成员self_kstack
pop ebp
pop ebx
pop edi
pop esi
ret ; 此时的ret执行的是栈顶的kernel_thread
inti.c
#include "init.h"
#include "print.h"
#include "interrupt.h"
#include "../device/timer.h"
#include "memory.h"
#include "thread.h"
void init_all(void) {
put_str("init all\n");
idt_init();
timer_init();
mem_init();
thread_init();
}
main.c
#include "print.h"
#include "init.h"
#include "debug.h"
#include "thread.h"
void g_thread(void* arg);
void g_thread2(void* arg);
void main(void) {
put_str("Hello GeniusOS\n");
put_int(2023);
put_str("\n");
init_all();
thread_start("genius", 5, g_thread, "genius");
thread_start("genius2", 31, g_thread2, "genius2");
intr_enable();
while (1) {
put_str("Main ");
}
}
void g_thread(void* arg) {
char* para = arg;
while (1) {
put_str(para);
}
}
void g_thread2(void* arg) {
char* para = arg;
while (1) {
put_str(para);
}
}
运行后的结果就是这样啦,好的今天的任务就到此结束了!