当前位置: 首页 > 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;当这个指针被用来…...

JavaSec-RCE

简介 RCE(Remote Code Execution)&#xff0c;可以分为:命令注入(Command Injection)、代码注入(Code Injection) 代码注入 1.漏洞场景&#xff1a;Groovy代码注入 Groovy是一种基于JVM的动态语言&#xff0c;语法简洁&#xff0c;支持闭包、动态类型和Java互操作性&#xff0c…...

Appium+python自动化(十六)- ADB命令

简介 Android 调试桥(adb)是多种用途的工具&#xff0c;该工具可以帮助你你管理设备或模拟器 的状态。 adb ( Android Debug Bridge)是一个通用命令行工具&#xff0c;其允许您与模拟器实例或连接的 Android 设备进行通信。它可为各种设备操作提供便利&#xff0c;如安装和调试…...

day52 ResNet18 CBAM

在深度学习的旅程中&#xff0c;我们不断探索如何提升模型的性能。今天&#xff0c;我将分享我在 ResNet18 模型中插入 CBAM&#xff08;Convolutional Block Attention Module&#xff09;模块&#xff0c;并采用分阶段微调策略的实践过程。通过这个过程&#xff0c;我不仅提升…...

大型活动交通拥堵治理的视觉算法应用

大型活动下智慧交通的视觉分析应用 一、背景与挑战 大型活动&#xff08;如演唱会、马拉松赛事、高考中考等&#xff09;期间&#xff0c;城市交通面临瞬时人流车流激增、传统摄像头模糊、交通拥堵识别滞后等问题。以演唱会为例&#xff0c;暖城商圈曾因观众集中离场导致周边…...

【RockeMQ】第2节|RocketMQ快速实战以及核⼼概念详解(二)

升级Dledger高可用集群 一、主从架构的不足与Dledger的定位 主从架构缺陷 数据备份依赖Slave节点&#xff0c;但无自动故障转移能力&#xff0c;Master宕机后需人工切换&#xff0c;期间消息可能无法读取。Slave仅存储数据&#xff0c;无法主动升级为Master响应请求&#xff…...

MySQL中【正则表达式】用法

MySQL 中正则表达式通过 REGEXP 或 RLIKE 操作符实现&#xff08;两者等价&#xff09;&#xff0c;用于在 WHERE 子句中进行复杂的字符串模式匹配。以下是核心用法和示例&#xff1a; 一、基础语法 SELECT column_name FROM table_name WHERE column_name REGEXP pattern; …...

[Java恶补day16] 238.除自身以外数组的乘积

给你一个整数数组 nums&#xff0c;返回 数组 answer &#xff0c;其中 answer[i] 等于 nums 中除 nums[i] 之外其余各元素的乘积 。 题目数据 保证 数组 nums之中任意元素的全部前缀元素和后缀的乘积都在 32 位 整数范围内。 请 不要使用除法&#xff0c;且在 O(n) 时间复杂度…...

【开发技术】.Net使用FFmpeg视频特定帧上绘制内容

目录 一、目的 二、解决方案 2.1 什么是FFmpeg 2.2 FFmpeg主要功能 2.3 使用Xabe.FFmpeg调用FFmpeg功能 2.4 使用 FFmpeg 的 drawbox 滤镜来绘制 ROI 三、总结 一、目的 当前市场上有很多目标检测智能识别的相关算法&#xff0c;当前调用一个医疗行业的AI识别算法后返回…...

Maven 概述、安装、配置、仓库、私服详解

目录 1、Maven 概述 1.1 Maven 的定义 1.2 Maven 解决的问题 1.3 Maven 的核心特性与优势 2、Maven 安装 2.1 下载 Maven 2.2 安装配置 Maven 2.3 测试安装 2.4 修改 Maven 本地仓库的默认路径 3、Maven 配置 3.1 配置本地仓库 3.2 配置 JDK 3.3 IDEA 配置本地 Ma…...

rnn判断string中第一次出现a的下标

# coding:utf8 import torch import torch.nn as nn import numpy as np import random import json""" 基于pytorch的网络编写 实现一个RNN网络完成多分类任务 判断字符 a 第一次出现在字符串中的位置 """class TorchModel(nn.Module):def __in…...