当前位置: 首页 > news >正文

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;
}

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 4096struct 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 0x43uint32_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 esipush edipush ebxpush 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_kstackpop ebppop ebxpop edipop esiret		;  此时的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);}
}

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

相关文章:

13 Day:实现内核线程

前言&#xff1a;我们昨天完成了内核的内存池以及内存管理程序&#xff0c;今天我们要揭开操作系统多任务执行的神秘面纱&#xff0c;来了解并实现一个多任务的操作系统。 一&#xff0c;实现内核线程 在聊线程之间我们先聊聊处理器吧&#xff0c;众所周之现在我们的CPU动不动…...

GPU服务器安装显卡驱动、CUDA和cuDNN

GPU服务器安装cuda和cudnn1. 服务器驱动安装2. cuda安装3. cudNN安装4. 安装docker环境5. 安装nvidia-docker25.1 ubuntu系统安装5.2 centos系统安装6. 测试docker容调用GPU服务1. 服务器驱动安装 显卡驱动下载地址https://www.nvidia.cn/Download/index.aspx?langcn显卡驱动…...

结构体变量

C语言允许用户自己建立由不同类型数据组成的组合型的数据结构&#xff0c;它称为结构体&#xff08;structre&#xff09;。 在程序中建立一个结构体类型&#xff1a; 1.结构体 建立结构体 struct Student { int num; //学号为整型 char name[20]; //姓名为字符串 char se…...

Java 多态

文章目录1、多态的介绍2、多态的格式3、对象的强制类型转换4、instanceof 运算符5、案例&#xff1a;笔记本USB接口1、多态的介绍 多态&#xff08;Polymorphism&#xff09;按字面意思理解就是“多种形态”&#xff0c;即一个对象拥有多种形态。 即同一种方法可以根据发送对…...

九龙证券|一夜暴跌36%,美股走势分化,标普指数创近2月最差周度表现

当地时间2月10日&#xff0c;美股三大指数收盘涨跌纷歧。道指涨0.5%&#xff0c;标普500指数涨0.22%&#xff0c;纳指跌0.61%。 受国际油价明显上升影响&#xff0c;动力板块领涨&#xff0c;埃克森美孚、康菲石油涨超4%。大型科技股走低&#xff0c;特斯拉、英伟达跌约5%。热门…...

【数据库】 mysql用户授权详解

目录 MySQL用户授权 一&#xff0c;密码策略 1&#xff0c;查看临时密码 2&#xff0c;查看数据库当前密码策略&#xff1a; 二&#xff0c; 用户授权和撤销授权 1、创建用户 2&#xff0c;删除用户 3&#xff0c;授权和回收权限 MySQL用户授权 一&#xff0c;密码策略…...

【性能】性能测试理论篇_学习笔记_2023/2/11

性能测试的目的验证系统是否能满足用户提出的性能指标发现性能瓶颈&#xff0c;优化系统整体性能性能测试的分类注&#xff1a;这些测试类型其实是密切相关&#xff0c;甚至无法区别的&#xff0c;例如几乎所有的测试都有并发测试。在实际中不用纠结具体的概念。而是要明确测试…...

C语言(输入printf()函数)

printf()的细节操作很多&#xff0c;对于现阶段的朋友来说&#xff0c;主要还是以理解为主。因为很多的确很难用到。 目录 一.转换说明&#xff08;占位符&#xff09; 二.printf()转换说明修饰符 1.数字 2.%数字1.数字2 3.整型转换字符补充 4.标记 -符号 符号 空格符…...

Zabbix 构建监控告警平台(四)

Zabbix ActionZabbix Macros1.Zabbix Action 1.1动作Action简介 当某个触发器状态发生改变(如Problem、OK)&#xff0c;可以采取相应的动作&#xff0c;如&#xff1a; 执行远程命令 邮件&#xff0c;短信&#xff0c;微信告警,电话 1.2告警实验简介 1. 创建告警media type&…...

2004-2019年285个地级市实际GDP与名义GDP

2004-2019年285个地级市实际GDP和名义GDP 1、时间&#xff1a;2004-2019年 2、范围&#xff1a;285个地级市 3、说明&#xff1a;GDP平减指数采用地级市所在省份当年平减指数 4、代码&#xff1a; "gen rgdp gdp if year 2003 gen rgdp gdp if year 2003" re…...

Node.js笔记-Express(基于Node.js的web开发框架)

目录 Express概述 Express安装 基本使用 创建服务器 编写请求接口 接收请求参数 获取路径参数(/login/2) 静态资源托管-express.static&#xff08;内置中间件&#xff09; 什么是静态资源托管&#xff1f; express.static() 应用举例 托管多个静态资源 挂载路径前缀…...

力扣sql简单篇练习(十五)

力扣sql简单篇练习(十五) 1 直线上的最近距离 1.1 题目内容 1.1.1 基本题目信息 1.1.2 示例输入输出 1.2 示例sql语句 SELECT min(abs(p1.x-p2.x)) shortest FROM point p1 INNER JOIN point p2 ON p1.x <>p2.x1.3 运行截图 2 只出现一次的最大数字 2.1 题目内容 2…...

浅谈动态代理

什么是动态代理&#xff1f;以下为个人理解:动态代理就是在程序运行的期间&#xff0c;动态地针对对象的方法进行增强操作。并且这个动作的执行者已经不是"this"对象了&#xff0c;而是我们创建的代理对象&#xff0c;这个代理对象就是类似中间人的角色&#xff0c;帮…...

Idea超好用的管理工具ToolBox(附带idea工具)

文章目录为什么要用ToolBox总结idea管理安装、更新、卸载寻找ide配置、根路径idea使用准备工作配置为什么要用ToolBox 快速轻松地更新,轻松管理您的 JetBrains 工具 安装自动更新同时更新插件和 IDE回滚和降级通过下载补丁或一组补丁而不是整个包&#xff0c;节省维护 IDE 的…...

Spring 中 ApplicationContext 和 BeanFactory 的区别

文章目录类图包目录不同国际化强大的事件机制&#xff08;Event&#xff09;底层资源的访问延迟加载常用容器类图 包目录不同 spring-beans.jar 中 org.springframework.beans.factory.BeanFactoryspring-context.jar 中 org.springframework.context.ApplicationContext 国际…...

情人节有哪些数码好物值得送礼?情人节实用性强的数码好物推荐

转瞬间&#xff0c;情人节快到了&#xff0c;大家还在为送什么礼物而烦恼&#xff1f;在这个以科技为主的时代&#xff0c;人们正在享受着科技带来的便利&#xff0c;其中&#xff0c;数码产品也成为了日常生活中必不可少的存在。接下来&#xff0c;我来给大家推荐几款比较实用…...

java中flatMap用法

java中map是把集合每个元素重新映射&#xff0c;元素个数不变&#xff0c;但是元素值发生了变化。而flatMap从字面上来说是压平这个映射&#xff0c;实际作用就是将每个元素进行一个一对多的拆分&#xff0c;细分成更小的单元&#xff0c;返回一个新的Stream流&#xff0c;新的…...

【MySQL Shell】8.9.2 InnoDB ClusterSet 集群中的不一致事务集(GTID集)

AdminAPI 的 clusterSet.status() 命令警告您&#xff0c;如果 InnoDB 集群的 GTID 集与 InnoDB ClusterSet 中主集群上的 GTID 集不一致。与 InnoDB ClusterSet 中的其他集群相比&#xff0c;处于此状态的集群具有额外的事务&#xff0c;并且具有全局状态 OK_NOT_CONSISTENT 。…...

logstash毫秒时间戳转日期以及使用业务日志时间戳替换原始@timestamp

文章目录问题解决方式参考问题 在使用Kibana观察日志排查问题时发现存在很多组的timestamp 数据一样&#xff0c;如下所示 详细观察内部数据发现其中日志数据有一个timestamp字段保存的是业务日志的毫秒级时间戳&#xff0c;经过和timestamp数据对比发现二者的时间不匹配。经…...

【C语言】qsort——回调函数

目录 1.回调函数 2.qsort函数 //整形数组排序 //结构体排序 3.模拟实现qsort //整型数组排序 //结构体排序 1.回调函数 回调函数就是一个通过函数指针调用的函数。如果你把函数的指针&#xff08;地址&#xff09;作为参数传递给另一个函数&#xff0c;当这个指针被用来…...

8年软件测试工程师经验感悟

不知不觉在软件测试行业&#xff0c;野蛮生长了8年之久。这一路上拥有了非常多的感受。有迷茫&#xff0c;有踩过坑&#xff0c;有付出有收获&#xff0c; 有坚持&#xff01; 我一直都在软件测试行业奋战&#xff0c; 毕业时一起入职的好友已经公司内部转岗&#xff0c;去选择…...

腾讯云安全组配置参考版

官方文档参考: 云服务器 安全组应用案例-操作指南-文档中心-腾讯云 新建安全组时&#xff0c;您可以选择腾讯云为您提供的两种安全组模板&#xff1a; 放通全部端口模板&#xff1a;将会放通所有出入站流量。放通常用端口模板&#xff1a;将会放通 TCP 22端口&#xff08;Lin…...

代码覆盖率工具OpenCppCoverage在Windows上的使用

OpenCppCoverage是用在Windows C上的开源的代码覆盖率工具&#xff0c;源码地址为https://github.com/OpenCppCoverage/OpenCppCoverage &#xff0c;最新发布版本为0.9.9.0&#xff0c;License为GPL-3.0。 从https://github.com/OpenCppCoverage/OpenCppCoverage/releases 下载…...

代码随想录算法训练营第24天25天|● 77. 组合● 216.组合总和III ● 17.电话号码的字母组合

77组合 看完题后的思路 void f&#xff08;数组&#xff0c;startIndex&#xff09;递归终止 if&#xff08;startIndex数组长度||path.sizek&#xff09;{ if(path.sizek){ 加入} }递归 for&#xff08;&#xff1b;startIndex<num.size&#xff1b;startIndex&#xff0…...

Python_pytorch

python_pytorch 小土堆pytotch学习视频链接 from的是一个个的包&#xff08;package) import 的是一个个的py文件(file.py) 所使用的一般是文件中的类(.class) 第一步实例化所使用的类,然后调用类中的方法&#xff08;def) Dataset 数据集处理 import os from PIL impo…...

【Java|golang】2335. 装满杯子需要的最短总时长

现有一台饮水机&#xff0c;可以制备冷水、温水和热水。每秒钟&#xff0c;可以装满 2 杯 不同 类型的水或者 1 杯任意类型的水。 给你一个下标从 0 开始、长度为 3 的整数数组 amount &#xff0c;其中 amount[0]、amount[1] 和 amount[2] 分别表示需要装满冷水、温水和热水的…...

shell编程之sed

文章目录八、shell编程之sed8.1 工作原理8.2 sed基本语法8.3 模式空间中的编辑操作8.3.1 地址定界8.3.2 常用编辑命令8.4 sed扩展八、shell编程之sed 8.1 工作原理 sed是一种流编辑器&#xff0c;它是文本处理中非常有用的工具&#xff0c;能够完美的配合正则表达式使用&…...

安全寒假作业nginx反向代理+负载均衡上传webshell重难点+apache漏洞

1.应用场景 负载均衡作为现今解决web应用承载大流量访问问题的一种方案&#xff0c;在真实环境中得到广泛的部署。实现负载均衡的方式有很多种&#xff0c;比如 DNS 方式、HTTP 重定向方式、IP 负载均衡方式、反向代理方式等等。 比如基于dns的负载均衡&#xff1a; 当然还有…...

day35|01背包问题、416. 分割等和子集

01背包问题 有n件物品和一个最多能背重量为w的背包。第i件物品的重量是weight[i]&#xff0c;得到的价值是value[i] 。每件物品只能用一次&#xff0c;求解将哪些物品装入背包里物品价值总和最大。 例&#xff1a;背包最大重量为4。 物品为&#xff1a; 重量价值物品0115物品…...

Linux内核启动(3,0.11版本)内核启动完成与进入内核main函数

这一部分是在讲解head.s代码&#xff0c;这个代码与bootsect.s和setup.s在同一目录下&#xff0c;但是head.s程序在被编译生成目标文件后会与内核其他程序一起被链接成system模块&#xff0c;位于system模块的最前面开始部分。system模块将被放置在磁盘上setup模块之后开始的扇…...

成都网站建设求职简历/如何推广网站链接

Shiro入门视频课程&#xff08;适合初学者的教程&#xff09;—210人已学习 课程介绍 本课程以通俗易懂的方式讲解Shiro权限技术&#xff0c;内容包括&#xff1a;Shiro简介、第一个Shiro程序、Realm、认证策略、加密服务、授权、Spring整合Shiro等。 适合初学者的教程&…...

西安企业网站建设托管/辽阳网站seo

发现plt.scatter()不仅能画散点&#xff0c;还能调节点的大小&#xff0c;做成气泡图&#xff0c;所以自己准备写个总结&#xff0c;记录下&#xff01;函数&#xff1a;matplotlib.pyplot.scatter(x, y, sNone, cNone, markerNone, cmapNone, normNone, vminNone, vmaxNone, a…...

重庆网站建设 最便宜/网站竞价推广都有哪些

今天去青岛会展中心看车展了&#xff0c;拍了好多照片&#xff0c;把它传上来跟大家分享一下&#xff0c;也好在工作之余放松一下&#xff01; 转载于:https://blog.51cto.com/370135415/577182...

郑州微信公众号外包/广州seo托管

题目要求&#xff1a; 本题要求编写程序&#xff0c;输入N个学生的MOOC成绩&#xff0c;统计优秀、合格证书的数量&#xff0c;以及没有获得证书的数量。学生修读程序设计MOOC&#xff0c;85分及以上获得优秀证书&#xff0c;不到85分但是60分及以上获得合格证书&#xff0c;不…...

做网上商城网站哪家好/国内最好用免费建站系统

2019独角兽企业重金招聘Python工程师标准>>> HTML提供了5种空格实体&#xff08;space entity&#xff09;&#xff0c;它们拥有不同的宽度&#xff0c;非断行空格&#xff08; &#xff09;是常规空格的宽度&#xff0c;可运行于所有主流浏览器。其他几种空格&…...

韩国化妆品网站金色flash片头/网站推广搜索

终于鼓起勇气想在图书馆预习&#xff08;。 &#xff09;下电路的&#xff0c;果然一上午又花在Java入门上了... Applet 其实就是一段Java代码&#xff0c;但这段代码可以以适当的方式嵌入到HTML页面。 1、编写一个Java Applet 用记事本编写如下代码&#xff0c;保存在某一目录…...