13 Day:实现内核线程

前言:我们昨天完成了内核的内存池以及内存管理程序,今天我们要揭开操作系统多任务执行的神秘面纱,来了解并实现一个多任务的操作系统。


一,实现内核线程 

在聊线程之间我们先聊聊处理器吧,众所周之现在我们的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;
}

值得注意的一点是,结构体越往下的成员,地址越高。所以在init_trhead中

将栈指针指向了栈顶端,thread_create,将栈指针指向栈底层,同时汇编语言的栈POP大家应该都知道是从下往上,于是乎整个线程栈的构成如下

 

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_tagall_list_tag的作用了,大家有没有想过为什么我们链表不存放PCB,而是一个个tag,首先各各线程在内存中是离散的我们需要用链表将其联系起来。其次大家想想一个PCB有多大?4KB,我们链表节点全部都是一个个4KB的PCB未免有点小材大用了,于是乎我们便直接使用其中的general_tagall_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);
	}
}

 运行后的结果就是这样啦,好的今天的任务就到此结束了!

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值