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

《操作系统真象还原》第九章(2)——线程

《操作系统真象还原》第九章(2)——线程

文章目录

  • 《操作系统真象还原》第九章(2)——线程
    • 前言
    • 多线程调度
      • 简单优先级调度的基础
      • 任务调度器和任务切换
        • 注册时钟中断处理函数
        • 实现调度器schedule
        • 实现任务切换函数switch_to
        • 启用线程调度
      • 运行测试
    • 结语

前言

本篇博客是第九章线程的第二部分,第一部分我们实现了线程,实现了我们自己系统的链表,本章就是要实现多线程轮询机制。

第一部分链接:《操作系统真象还原》第九章(1)——线程-CSDN博客


多线程调度

简单优先级调度的基础

本节任务是把thread.c 和 thread.h 进一步完善,在原有线程的基础上添加新的功能。

thread.h新增部分

#include "../lib/kernel/list.h"
...
/* 线程或进程的pcb程序控制块 */
struct task_struct {uint32_t *self_kstack; // 线程自己的内核栈栈顶指针enum thread_status status; // 线程的状态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; // 如果是进程,这是进程的页表的虚拟地址,线程则置为NULLchar name[16]; // 线程的名字uint32_t stack_magic; // 线程栈的魔数,边界标记,用来检测栈溢出};
...

thread.c更新后


#include "thread.h"
#include "../lib/kernel/stdint.h"
#include "../lib/string.h"
#include "global.h"
#include "../kernel/memory.h"
#include "interrupt.h"#define PAGE_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 volatile("movl %%esp, %0" : "=g"(esp)); // 获取当前线程的栈顶地址/* 取得esp整数部分 */return (struct task_struct *)(esp & 0xfffff000); // 返回pcb指针
}/* 由kernel_thread执行function(func_arg) */
static void kernel_thread(thread_func *function, void *func_arg) {intr_enable(); // 开中断,避免后面的时钟中断被屏蔽,而无法调度其他线程function(func_arg);//...
}/* 初始化线程栈thread_stack */
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; // eip指向kernel_threadkthread_stack->function = function; // 函数指针kthread_stack->func_arg = func_arg; // 函数参数kthread_stack->ebp = 0; // ebp寄存器值kthread_stack->ebx = 0; // ebx寄存器值kthread_stack->edi = 0; // edi寄存器值kthread_stack->esi = 0; // esi寄存器值
}/* 初始化线程基本信息 */
void init_thread(struct task_struct *pthread, char *name, int prio) {memset(pthread, 0, sizeof(*pthread)); // 清空线程pcbstrcpy(pthread->name, name); // 线程名字if(pthread == main_thread) { // 线程状态pthread->status = TASK_RUNNING;} else {pthread->status = TASK_READY;}/* self_kstack 是线程自己在内核态下使用的栈顶地址 */pthread->self_kstack = (uint32_t *)((uint32_t)pthread + PAGE_SIZE); // 线程内核栈pthread->priority = prio; // 线程优先级pthread->ticks = prio; // 线程时间片pthread->elapsed_ticks = 0; // 线程运行时间pthread->pgdir = NULL; // 线程页表pthread->stack_magic = 0x19870916; // 线程栈的魔数,边界标记,用来检测栈溢出
}/* 创建一个优先级为prio的线程,线程名是name,执行的函数是function(func_arg) */
struct task_struct *thread_start(char *name, int prio, thread_func function, void *func_arg) {struct task_struct *thread = get_kernel_pages(1); // 分配1页的内存空间给pcbinit_thread(thread, name, prio); // 初始化线程基本信息thread_create(thread, function, func_arg); // 初始化线程栈/* 确保线程不在就绪队列 */ASSERT(!elem_find(&thread_ready_list, &thread->general_tag));list_append(&thread_ready_list, &thread->general_tag); // 将线程加入就绪队列/* 确保线程不在所有线程队列 */ASSERT(!elem_find(&thread_all_list, &thread->all_list_tag));list_append(&thread_all_list, &thread->all_list_tag); // 将线程加入所有线程队列return thread;
}/* 将kernel中的main函数完善成主线程 */
static void make_main_thread(void) {/* 因为main线程早已运行, *咱们在loader.S中进入内核时的mov esp,0xc009f000, * 就是为其预留pcb的,因此pcb地址为0xc009e000, * 不需要通过get_kernel_page另分配一页*/main_thread = running_thread(); // 获取主线程pcbinit_thread(main_thread, "main", 31); // 初始化主线程基本信息ASSERT(!elem_find(&thread_ready_list, &main_thread->all_list_tag)); // 确保主线程不在就绪队列list_append(&thread_ready_list, &main_thread->general_tag); // 将主线程加入就绪队列
}

任务调度器和任务切换

本节要实现调度器和任务切换,调度器的工作就是根据任务的状态将其从处理器上换上换下。

我们先梳理思路:调度器读写就绪队列,增删里面的节点,节点是general_tag,代表一个个pcb。

每发生一次时钟中断,时钟中断的处理程序便将当前运行 线程的ticks 减 1。当ticks 为 0 时,时钟的中断处理程序调用调度器schedule,也就是该把当前线程换下 处理器了,让调度器选择另一个线程上处理器。

调度器从就绪队列取出队首元素,将pcb里线程的状态置为running。然后通过switc_to函数将新线程的寄存器环境恢复,开始执行新线程。

其他函数也可以调用schedule,例如后续的thread_block函数,到时候再说。

总结一下,完整的调度过程需要三部分的配合。

  1. 时钟中断处理函数。
  2. 调度器schedule。
  3. 任务切换函数switch_to。
注册时钟中断处理函数

我们之前的时钟中断函数使用的是通用函数general_intr_handler,也就是默认的中断处理函数。我们有两个打算:1.简单修改默认中断处理函数。2.写专门的时钟中断处理函数。

修改interrupt.c的general_intr_handler

/* 通用的中断处理函数,一般用在异常出现时的处理 */
static void general_intr_handler(uint8_t vec_nr) {if (vec_nr == 0x27 || vec_nr == 0x2f) {	// 0x2f是从片8259A上的最后一个irq引脚,保留return;		//IRQ7和IRQ15会产生伪中断(spurious interrupt),无须处理。}/* 将光标置为0,从屏幕左上角清出一片打印异常信息的区域,方便阅读 */ set_cursor(0);	// 设置光标位置为0int cursor_pos = 0;	// 光标位置while (cursor_pos < 320) {	// 清除屏幕put_char(' ');	// 打印空格cursor_pos++;	// 光标位置加1}set_cursor(0);	// 设置光标位置为0put_str("!!! exception message begin !!!\n");	// 打印异常信息set_cursor(88);put_str(intr_name[vec_nr]);	// 打印异常名称if (vec_nr == 14) {	// 如果是页面错误异常int page_fault_vaddr = 0;	// 页面错误地址/* cr2寄存器存放造成page_fault的地址 */asm volatile ("movl %%cr2, %0" : "=r" (page_fault_vaddr));	// 获取页面错误地址put_str("\npage fault addr is ");	// 打印页面错误地址put_int(page_fault_vaddr);	// 打印页面错误地址}put_str("  \n!!! exception message end !!!\n");	// 打印异常信息结束// 目前处在关中断状态,不再调度进程,下面的死循环不再被中断覆盖while (1);    // 进入死循环
}

特别说明一下新增的函数set_cursor,这个函数声明在print.h,实现是print.S中.set_cursor,我们先修改print.S

global set_cursor
.set set_cursor,.set_cursor

然后修改print.h

//这个头文件定义了打印函数的原型
//包括打印单个字符、打印字符串和打印16进制整数的函数
#ifndef __LIB_KERNEL_PRINT_H
#define __LIB_KERNEL_PRINT_H
#include "stdint.h"
extern void put_char (uint8_t char_asci);
extern void put_str  (char* message);
extern void put_int  (uint32_t number);
extern void set_cursor (uint32_t position);
#endif

修改time.c,完成时钟中断处理函数

//时钟中断相关
#include "timer.h"
#include "io.h"
#include "print.h"
#include "../kernel/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_COUNTROL_PORT	0x43uint32_t ticks;	// ticks是时钟中断的次数void frequency_set(uint8_t counter_port ,uint8_t counter_no,uint8_t rwl,uint8_t counter_mode,uint16_t counter_value) {outb(PIT_COUNTROL_PORT,(uint8_t) (counter_no << 6 | rwl << 4 | counter_mode << 1));outb(counter_port,(uint8_t)counter_value);outb(counter_port,(uint8_t)counter_value >> 8);return;
} /* 时钟的中断处理函数 */
static void intr_timer_handler(void) {struct task_struct* cur_thread = running_thread();ASSERT(cur_thread->stack_magic == 0x19870916); // 检测栈溢出cur_thread->elapsed_ticks++; // 线程运行的时间加1// 每次时钟中断,ticks加1ticks++;if (cur_thread->ticks == 0) { // 如果当前线程的时间片用完schedule(); // 调度其他线程} else {cur_thread->ticks--; // 否则,当前线程的时间片减1}
}/* 初始化PIT8253 */ 
void timer_init(void) {put_str("timer_init start!\n");frequency_set(COUNTER0_PORT, \COUNTER0_NO, \READ_WRITE_LATCH, \COUNTER_MODE, \COUNTER0_VALUE);register_handler(0x20, intr_timer_handler); // 注册时钟中断处理函数put_str("timer_init done!\n");return;
}

关于新函数register_handler,它的实现在interrupt.c里,在exception_init函数之后,在idt_init函数之前

/* 在中断处理程序数组第vector_no个元素中 注册安装中断处理程序function */ 
void register_handler(uint8_t vector_no, intr_handler function) {idt_table[vector_no] = function;	// 注册中断处理函数
}
实现调度器schedule

依然是修改thread.c,给出新增部分

/* 实现任务调度 */
void scheduler(void) {ASSERT(intr_get_status() == INTR_OFF); // 确保中断关闭struct task_struct *cur = running_thread(); // 获取当前线程pcbif(cur->status == TASK_RUNNING) { // 如果当前线程是运行状态ASSERT(!elem_find(&thread_ready_list, &cur->general_tag)); // 确保当前线程不在就绪队列list_append(&thread_ready_list, &cur->general_tag); // 将当前线程加入就绪队列cur->ticks = cur->priority; // 重置时间片cur->status = TASK_READY; // 设置当前线程状态为就绪}else {/* 如果当前线程不是运行状态,则不需要重置时间片 */}ASSERT(!list_empty(&thread_ready_list)); // 确保就绪队列不为空thread_tag = NULL; // 清空线程节点thread_tag = list_pop(&thread_ready_list); // 从就绪队列中取出一个线程节点struct task_struct *next = elem2entry(struct task_struct, \general_tag, \thread_tag); // 获取下一个线程pcbnext->status = TASK_RUNNING; // 设置下一个线程状态为运行switch_to(cur, next); // 任务切换
}

关于宏elem2entry:这个宏定义在list.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))

这个elem2entry宏的作用获取某个结构体成员对应的结构体的地址,原理是是用 elem_ptr 的地址减去elem_ptr 在结构体struct_type 中的偏移量。偏移量通过宏offset获取。

实现任务切换函数switch_to

书上先提了一下上下文保护,放一张图片

说一下我的理解:开机->bios->mbr->loader->kernel_main,主线程完成中断初始化、计时器初始化、内存池初始化、多次线程初始化等。目前这一系列过程完成后,main线程被多线程系统管理。

中断初始化后,我们的计时器硬件每过一段时间就给cpu发一个中断信号,也就是进行所谓的时钟中断,执行任意中断处理程序时,先保存进入中断前的上下文环境,这部分是kernel.S完成的,称为上下文保护的第一部分。第一部分保护的寄存器环境放在中断栈里。

中断处理程序有始有终,其中调用了schedule,为了后续能结束,要保存中断处理程序运行过程中的上下文环境,称为上下文保护的第二部分。第二部分保护的寄存器环境放在线程栈里。

通过时钟中断的处理程序,运行schedule,schedule运行switch_to,switch_to中实现上下文保护的第二部分。

;switch_to函数接受两个参数,第1个参数是当前线程cur
;第2个参数是下一个上处理器的线程next
;此函数的功能是保存cur线程的寄存器映像,将下一个线程next的寄存器映像装载到处理器。
[bits 32]
section .text
global switch_to
switch_to:;-----以下是备份当前线程的上下文环境-----;第一个位置是返回地址push ESIpush EDIpush EBXpush EBPmov EAX,[esp+20]    mov [EAX],esp   ;获取cur,把esp保存到cur的结构体的self_kstack里;-----以下是恢复下一个线程的环境-----mov EAX,[esp+24]    mov esp,[EAX]   ;获取next,把esp指向next的结构体的self_kstackpop EBPpop EBXpop EDIpop ESIret             ;返回到scheduler
启用线程调度

thread.h完整代码


#ifndef __THREAD_THREAD_H
#define __THREAD_THREAD_H
#include "../lib/kernel/stdint.h"
#include "../lib/kernel/list.h"/* 自定义通用函数类型,用来承载线程中函数的类型 */
typedef void thread_func(void*);/* 进程、线程的状态 */
enum thread_status {TASK_RUNNING,  // 运行中TASK_READY,    // 就绪TASK_BLOCKED,  // 阻塞TASK_WAITING,  // 等待TASK_HANGING,  // 挂起TASK_DIED     // 死亡
};/***********   中断栈intr_stack   *********** 
* 此结构用于中断发生时保护程序(线程或进程)的上下文环境: 
* 进程或线程被外部中断或软中断打断时,会按照此结构压入上下文 
* 寄存器,intr_exit中的出栈操作是此结构的逆操作 
* 此栈在线程自己的内核栈中位置固定,所在页的最顶端 
********************************************/ 
/*进入中断后,在kernel.S中的中断入口程序“intr%1entry”所执行的上下
文保护的一系列压栈操作都是压入了此结构中。*/
struct intr_stack {uint32_t vec_no;   // 中断号uint32_t edi;      // 被中断的线程的edi寄存器值uint32_t esi;      // 被中断的线程的esi寄存器值uint32_t ebp;      // 被中断的线程的ebp寄存器值uint32_t esp_dummy; // 被中断的线程的esp寄存器值// 虽然pushad把esp也压入,但esp是不断变化的,所以会被popad忽略uint32_t ebx;      // 被中断的线程的ebx寄存器值uint32_t edx;      // 被中断的线程的edx寄存器值uint32_t ecx;      // 被中断的线程的ecx寄存器值uint32_t eax;      // 被中断的线程的eax寄存器值uint32_t gs;       // 被中断的线程的gs寄存器值uint32_t fs;       // 被中断的线程的fs寄存器值uint32_t es;       // 被中断的线程的es寄存器值uint32_t ds;       // 被中断的线程的ds寄存器值/* 以下由cpu从低特权级到高特权级时压入 */uint32_t err_code; // 错误码void (*eip)(void);      // 被中断的线程的eip寄存器值// eip是指向无参数无返回值的函数的指针uint32_t cs;       // 被中断的线程的cs寄存器值uint32_t eflags;   // 被中断的线程的eflags寄存器值void * esp;      // 被中断的线程的esp寄存器值uint32_t ss;       // 被中断的线程的ss寄存器值
};/***********  线程栈thread_stack  *********** 
* 线程自己的栈,用于存储线程中待执行的函数 
* 此结构在线程自己的内核栈中位置不固定, 
* 仅用在switch_to(任务切换)时保存线程环境。 
* 实际位置取决于实际运行情况。 
******************************************/
struct thread_stack {/*:关于下面四个寄存器,在被调函数运行完之后,这4个寄存器的值必须和运行前一样,它必须在自己的栈中存储这些寄存器的值。*/uint32_t ebp;   // 线程的ebp寄存器值uint32_t ebx;   // 线程的ebx寄存器值uint32_t edi;   // 线程的edi寄存器值uint32_t esi;   // 线程的esi寄存器值/* 线程第一次执行时,eip指向待调用的函数。其他时候,eip指向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 thread_status status; // 线程的状态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; // 如果是进程,这是进程的页表的虚拟地址,线程则置为NULLchar name[16]; // 线程的名字uint32_t stack_magic; // 线程栈的魔数,边界标记,用来检测栈溢出};struct task_struct *running_thread(void); // 获取当前线程的pcb指针
void thread_create(struct task_struct *pthread, thread_func function, void *func_arg); // 初始化线程栈
void init_thread(struct task_struct *pthread, char *name, int prio); // 初始化线程基本信息
struct task_struct *thread_start(char *name, int prio, thread_func function, void *func_arg); // 创建一个优先级为prio的线程,线程名是name,执行的函数是function(func_arg)
void schedule(void); // 线程调度函数
void thread_init(void); // 线程初始化函数
#endif

thread.c完整代码


#include "thread.h"
#include "../lib/string.h"
#include "../kernel/memory.h"
#include "interrupt.h"
#include "debug.h"
#include "print.h"#define PAGE_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 volatile("movl %%esp, %0" : "=g"(esp)); // 获取当前线程的栈顶地址/* 取得esp整数部分 */return (struct task_struct *)(esp & 0xfffff000); // 返回pcb指针
}/* 由kernel_thread执行function(func_arg) */
static void kernel_thread(thread_func *function, void *func_arg) {intr_enable(); // 开中断,避免后面的时钟中断被屏蔽,而无法调度其他线程function(func_arg);//...
}/* 初始化线程栈thread_stack */
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; // eip指向kernel_threadkthread_stack->function = function; // 函数指针kthread_stack->func_arg = func_arg; // 函数参数//kthread_stack->ebp = (uint32_t*)((uint32_t)pthread->self_kstack + sizeof(struct thread_stack)); // ebp指向线程栈kthread_stack->ebp = kthread_stack->ebx = \kthread_stack->esi = kthread_stack->edi = 0; // ebp, ebx, edi, esi寄存器值
}/* 初始化线程基本信息 */
void init_thread(struct task_struct *pthread, char *name, int prio) {memset(pthread, 0, sizeof(*pthread)); // 清空线程pcbstrcpy(pthread->name, name); // 线程名字if(pthread == main_thread) { // 线程状态pthread->status = TASK_RUNNING;} else {pthread->status = TASK_READY;}/* self_kstack 是线程自己在内核态下使用的栈顶地址 */pthread->self_kstack = (uint32_t *)((uint32_t)pthread + PAGE_SIZE); // 线程内核栈pthread->priority = prio; // 线程优先级pthread->ticks = prio; // 线程时间片pthread->elapsed_ticks = 0; // 线程运行时间pthread->pgdir = NULL; // 线程页表pthread->stack_magic = 0x19870916; // 线程栈的魔数,边界标记,用来检测栈溢出
}/* 创建一个优先级为prio的线程,线程名是name,执行的函数是function(func_arg) */
struct task_struct *thread_start(char *name, int prio, thread_func function, void *func_arg) {struct task_struct *thread = get_kernel_pages(1); // 分配1页的内存空间给pcbinit_thread(thread, name, prio); // 初始化线程基本信息thread_create(thread, function, func_arg); // 初始化线程栈/* 确保线程不在就绪队列 */ASSERT(!elem_find(&thread_ready_list, &thread->general_tag));list_append(&thread_ready_list, &thread->general_tag); // 将线程加入就绪队列/* 确保线程不在所有线程队列 */ASSERT(!elem_find(&thread_all_list, &thread->all_list_tag));list_append(&thread_all_list, &thread->all_list_tag); // 将线程加入所有线程队列return thread;
}/* 将kernel中的main函数完善成主线程 */
static void make_main_thread(void) {/* 因为main线程早已运行, * 咱们在loader.S中进入内核时的mov esp,0xc009f000, * 就是为其预留pcb的,因此pcb地址为0xc009e000, * 不需要通过get_kernel_page另分配一页*/main_thread = running_thread(); // 获取主线程pcbinit_thread(main_thread, "main", 31); // 初始化主线程基本信息ASSERT(!elem_find(&thread_all_list, &main_thread->all_list_tag)); // 确保主线程不在就绪队列list_append(&thread_all_list, &main_thread->all_list_tag); // 将主线程加入就绪队列
}/* 实现任务调度 */
void schedule(void) {ASSERT(intr_get_status() == INTR_OFF); // 确保中断关闭struct task_struct *cur = running_thread(); // 获取当前线程pcbif(cur->status == TASK_RUNNING) { // 如果当前线程是运行状态ASSERT(!elem_find(&thread_ready_list, &cur->general_tag)); // 确保当前线程不在就绪队列list_append(&thread_ready_list, &cur->general_tag); // 将当前线程加入就绪队列cur->ticks = cur->priority; // 重置时间片cur->status = TASK_READY; // 设置当前线程状态为就绪}else {/* 如果当前线程不是运行状态,则不需要重置时间片 */}ASSERT(!list_empty(&thread_ready_list)); // 确保就绪队列不为空thread_tag = NULL; // 清空线程节点thread_tag = list_pop(&thread_ready_list); // 从就绪队列中取出一个线程节点struct task_struct *next = elem2entry(struct task_struct, \general_tag, \thread_tag); // 获取下一个线程pcbnext->status = TASK_RUNNING; // 设置下一个线程状态为运行switch_to(cur, next); // 任务切换
}/* 初始化线程环境 */
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\n");
}

得把thread_init加入到init.c 中

//完成所有的初始化工作
#include "init.h" 
#include "print.h" 
#include "interrupt.h" 
#include "timer.h"
#include "memory.h"
#include "thread.h"/*负责初始化所有模块 */ 
void init_all() {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 "memory.h"
#include "thread.h"
#include "interrupt.h"void k_thread_a(void *); //线程函数声明
void k_thread_b(void *); //线程函数声明
/* 主线程 */
int main(void){put_str("HongBai's OS\n");init_all();//初始化所有模块//中断测试,目前只完成了响应时钟中断//asm volatile("sti");//打开中断//asm volatile("cli");//关闭中断//断言测试//ASSERT(1 == 2); // 断言失败,会调用panic_spin函数//申请内存测试//void *addr = get_kernel_pages(3); //申请3页内存//put_str("get_kernel_pages start vaddr is: ");//put_int((uint32_t)addr); //输出虚拟地址//put_str("\n");//线程测试thread_start("k_thread_a", 31, k_thread_a, "threadA ");thread_start("k_thread_b", 8, k_thread_b, "threadB ");intr_enable(); //打开中断while(1) {//主线程循环put_str("main ");}return 0;
}/* 在线程中运行的函数 */
void k_thread_a(void *arg){char *para = arg;while(1){put_str(para);}
}/* 在线程中运行的函数 */
void k_thread_b(void *arg){char *para = arg;while(1){put_str(para);}
}

运行测试

可以看到三个线程同时交错运行。

结语

调试代码真的让人心力憔悴。6点钟初步完成这部分的代码,先是编译连接出若干报错,解决后运行出错,一直调试3个小时到现在。累得够呛,懒得再写测试过程了,前面的代码也不一定全部正确,仅供参考吧。

相关文章:

《操作系统真象还原》第九章(2)——线程

《操作系统真象还原》第九章&#xff08;2&#xff09;——线程 文章目录 《操作系统真象还原》第九章&#xff08;2&#xff09;——线程前言多线程调度简单优先级调度的基础任务调度器和任务切换注册时钟中断处理函数实现调度器schedule实现任务切换函数switch_to启用线程调度…...

Windows程序包管理器WinGet实战

概述 WinGet&#xff0c;Windows Package Manager&#xff0c;Windows软件包管理器&#xff0c;开源在GitHub&#xff0c;GitHub Releases可下载&#xff0c;官方文档。 WinGet由一个命令行工具和一组用于在Windows 10/11等版本上安装应用的服务组成&#xff0c;可帮助用户快…...

【特殊场景应对1】视觉设计:信息密度与美学的博弈——让简历在HR视网膜上蹦迪的科学指南

写在最前 作为一个中古程序猿,我有很多自己想做的事情,比如埋头苦干手搓一个低代码数据库设计平台(目前只针对写java的朋友),比如很喜欢帮身边的朋友看看简历,讲讲面试技巧,毕竟工作这么多年,也做到过高管,有很多面人经历,意见还算有用,大家基本都能拿到想要的offe…...

番外篇 | SEAM-YOLO:引入SEAM系列注意力机制,提升遮挡小目标的检测性能

前言:Hello大家好,我是小哥谈。SEAM(Squeeze-and-Excitation Attention Module)系列注意力机制是一种高效的特征增强方法,特别适合处理遮挡和小目标检测问题。该机制通过建模通道间关系来自适应地重新校准通道特征响应。在遮挡小目标检测中的应用优势包括:1)通道注意力增强…...

Top100(26-30)

二叉树的中序遍历 给定一个二叉树的根节点 root &#xff0c;返回 它的 中序 遍历 。 示例 1&#xff1a; 输入&#xff1a;root [1,null,2,3] 输出&#xff1a;[1,3,2] 示例 2&#xff1a; 输入&#xff1a;root [] 输出&#xff1a;[] 示例 3&#xff1a; 输入&#x…...

在 Vue 3 中将拆分后的数组合并回原数组

接上文Vue 3 中按照某个字段将数组分成多个数组_vue3怎么进行数组对象--分割对象-CSDN博客 方法一&#xff1a;使用 flat() 方法 // 假设这是拆分后的多维数组 const splitArrays [[{id: 1, category: A}, {id: 3, category: A}],[{id: 2, category: B}, {id: 5, category: …...

MyBatis如何配置数据库连接并实现交互?

如果你用过MyBatis&#xff0c;肯定知道它的核心功能之一就是数据库连接管理。但很多新手在第一次配置时总会遇到各种问题&#xff1a;数据源怎么配&#xff1f;连接池参数如何调优&#xff1f;XML和注解方式有什么区别&#xff1f;今天我们就来彻底搞懂MyBatis连接数据库的每一…...

PyTorch入门------卷积神经网络

前言 参考&#xff1a;神经网络 — PyTorch Tutorials 2.6.0cu124 文档 - PyTorch 深度学习库 一个典型的神经网络训练过程如下&#xff1a; 定义一个包含可学习参数&#xff08;或权重&#xff09;的神经网络 遍历输入数据集 将输入通过神经网络处理 计算损失&#xff08;即…...

Qt官方案例知识点总结(图形视图——Colliding Mice)

Colliding Mice 案例 图元可重写下面的方法&#xff0c;返回一个QPainterPath(形状)&#xff0c;该形状基于图形项自己的坐标系 返回的形状用于碰撞检测、命中测试等&#xff0c;形状越精确&#xff0c;那么碰撞检测等就越准确 不重写的话&#xff0c;默认取 boundingRect()…...

人工智能在后端开发中的革命:从架构到运维

后端开发作为应用程序的"大脑",正在经历人工智能带来的深刻变革。从智能API设计到自动化数据库优化,从异常预测到资源调度,AI技术正在重塑后端开发的各个方面。本文将全面探讨AI如何赋能现代后端系统开发,并通过实际案例展示这些技术的应用价值。 一、智能API开…...

电子电器架构 --- EOL 工厂刷写(产线)

我是穿拖鞋的汉子,魔都中坚持长期主义的汽车电子工程师。 老规矩,分享一段喜欢的文字,避免自己成为高知识低文化的工程师: 周末洗了一个澡,换了一身衣服,出了门却不知道去哪儿,不知道去找谁,漫无目的走着,大概这就是成年人最深的孤独吧! 旧人不知我近况,新人不知我过…...

AI数据分析与BI可视化结合:解锁企业决策新境界

大家好&#xff0c;今天我们来聊聊一个前沿而热门的话题——AI数据分析与BI可视化结合&#xff0c;如何携手推动企业决策迈向新高度。在数据爆炸的时代&#xff0c;企业如何高效利用这些数据&#xff0c;成为制胜的关键。AI数据分析与BI可视化的结合&#xff0c;正是解锁这一潜…...

深度学习3.2 线性回归的从零开始实现

3.2.1 生成数据集 %matplotlib inline import random import torch from d2l import torch as d2ldef synthetic_data(w, b, num_examples):# 生成特征矩阵X&#xff0c;形状为(num_examples, len(w))&#xff0c;符合标准正态分布X torch.normal(0, 1, (num_examples, len(w…...

ArcPy工具箱制作(下)

在上一篇博客中&#xff0c;我们已经初步了解了如何制作ArcPy工具箱&#xff0c;包括工具箱的基本概念、准备工作、脚本编写以及将脚本转换为工具箱的步骤。今天&#xff0c;我们将继续深入探讨ArcPy工具箱的制作&#xff0c;重点介绍一些进阶技巧和优化方法. 一、优化工具箱的…...

if/switch语句初始化功能

基础介绍 这个特性是在c17版本引入的&#xff0c;在这之前是不允许在if语句或者switch语句中使用赋值语句&#xff0c;不仅仅是if语句和switch语句&#xff0c;包括lambda表达式在c17版本也支持类在捕获表达式中支持赋值操作。言归正传&#xff0c;下面阐述这个特性的基本语法…...

cmake 语法大纲

1&#xff0c;基础语法 CMakeLists.txt 目录组织文件&#xff1b; *.cmake 脚本文件 运行: $ cmake -P xxx.cmake *.cmake 模块文件 include 命令来引用 模块文件。 自定义模块&#xff1b; cmake 预制模块&#xff1b; 单行注释 # com 括号注释 #…...

前端单元测试实战:如何开始?

实战&#xff1a;如何开始单元测试 1.安装依赖 npm install --save-dev jest2.简单的例子 首先&#xff0c;创建一个 sum.js 文件 ./sum.js function sum(a, b) {return a b; }module.exports sum;创建一个名为 sum.test.js 的文件&#xff0c;这个文件包含了实际测试内…...

《软件设计师》复习笔记(12.2)——成本管理、配置管理

目录 一、项目成本管理 1. 定义 2. 主要过程 3. 成本类型 4. 其他概念 真题示例&#xff1a; 二、软件配置管理 1. 定义 2. 主要活动 3. 配置项 4. 基线&#xff08;Baseline&#xff09; 5. 配置库类型 真题示例&#xff1a; 一、项目成本管理 1. 定义 在批准…...

edge browser for linux debian

下载地址 https://www.microsoft.com/en-us/edge/download?formMA13FJ 安装 # 下载安装包 wget https://packages.microsoft.com/repos/edge/pool/main/m/microsoft-edge-stable/microsoft-edge-stable_135.0.3179.85-1_amd64.deb?brandM102 # 安装 sudo dpkg -i microsoft…...

Python读取Excel表格数据并写成JSON格式文件(精简版)

&#x1f91f;致敬读者 &#x1f7e9;感谢阅读&#x1f7e6;笑口常开&#x1f7ea;生日快乐⬛早点睡觉 &#x1f4d8;博主相关 &#x1f7e7;博主信息&#x1f7e8;博客首页&#x1f7eb;专栏推荐&#x1f7e5;活动信息 文章目录 1. 步骤步骤 1: 安装必要的库步骤 2: 读取Ex…...

服务器的算力已经被被人占用了,我如何能“无缝衔接”?

今天遇到一个问题&#xff0c;服务器已经被别人占用了&#xff0c;我又不知道什么时候他能结束&#xff0c;因此很难去训练自己的模型&#xff0c;隔一会去看看别人是否结束又太麻烦&#xff0c;于是便可以写这个脚本文件来自动检测服务器是否空闲&#xff0c;一有空闲就可以自…...

rulego-server是一个开源程序,是一个轻量级、无依赖性的工作流自动化平台。支持 iPaaS、流式计算和 AI 能力。

一、软件介绍 文末提供程序和源码下载学习 RuleGo-Server 是一个基于 RuleGo 构建的轻量级、高性能、模块化和集成友好的自动化工作流程平台。可用于自动化编排、iPaaS&#xff08;集成平台即服务&#xff09;、API 编排、应用编排、AI 编排、数据处理、IoT 规则引擎、AI 助手…...

『前端样式分享』联系我们卡片式布局 自适应屏幕 hover动效 在wikijs中使用 (代码拿来即用)

目录 预览效果分析要点响应式网格布局卡片样式&#xff1a;阴影和过渡效果 代码优化希望 长短不一的邮箱地址在左右居中的同时,做到左侧文字对齐(wikijs可用)总结 欢迎关注 『前端布局样式』 专栏&#xff0c;持续更新中 欢迎关注 『前端布局样式』 专栏&#xff0c;持续更新中…...

航电系统之通信技术篇

航电系统&#xff08;航空电子系统&#xff09;的通信技术是现代航空器的核心技术之一&#xff0c;其核心目标是实现飞行器内部各系统之间以及飞行器与外部设备&#xff08;如地面控制中心、其他飞行器等&#xff09;之间高效、可靠的信息交互。随着航空技术的不断发展&#xf…...

4.3 熟悉字符串处理函数

作为一名C语言初学者&#xff0c;掌握字符串处理函数是编程道路上不可或缺的一步。字符串是C语言中处理文本数据的基础&#xff0c;而标准库提供了一系列强大的字符串处理函数&#xff0c;极大地方便了我们的开发工作。本文将带领大家熟悉这些常用的字符串处理函数&#xff0c;…...

二叉树理论基础

二叉树种类 满二叉树&#xff1a;每个非叶子节点都有且只有两个子节点。 和完全二叉树&#xff1a;除了最底层外&#xff0c;其他各层都是满的&#xff1b;最底层的节点都集中在左侧。 二叉搜索树&#xff1a;对于任意节点 u&#xff0c;左子树上所有节 点的值都小于 u.val…...

yarn的三个资源调度策略

### YARN 的三种资源调度策略及其工作原理与区别 #### 1. **FIFO Scheduler (先进先出调度器)** FIFO Scheduler 是一种最简单的调度方式&#xff0c;所有的应用程序都按顺序排队等待执行。其基本逻辑如下&#xff1a; - 应用程序按照提交的时间先后顺序依次进入队列。 - 当集…...

leetcode0112. 路径总和-easy

1 题目&#xff1a;路径总和 官方标定难度&#xff1a;易 给你二叉树的根节点 root 和一个表示目标和的整数 targetSum 。判断该树中是否存在 根节点到叶子节点 的路径&#xff0c;这条路径上所有节点值相加等于目标和 targetSum 。如果存在&#xff0c;返回 true &#xff1…...

铁氧体和纳米晶:车载定制电感的材料选择

最近有个做车载产品的粉丝问到&#xff1a;我们的定制电感产品既会用到铁氧体磁芯&#xff0c;也会用到纳米晶磁芯&#xff0c;那么这两种材料&#xff0c;该如何选择呢&#xff1f; 要回答这个问题&#xff0c;我们首先要对两种材料做一个基本的对比。 铁氧体材料成本低&…...

MCP认证难题破解

一、MCP 认证体系现状与核心挑战 微软认证专家(MCP)体系在 2020 年后逐步向基于角色的认证转型,例如 Azure 管理员(AZ-104)、数据分析师(DP-100)等,传统 MCP 考试已被取代。当前备考的核心难题集中在以下方面: 1. 技术栈快速迭代 云原生技术占比提升:Azure 认证中,…...

ROS机器人一般用哪些传感器?

以下是ROS机器人常用传感器的分层详解及思维导图总结,涵盖传感器分类、核心参数、ROS支持及典型应用: 一、环境感知传感器 1. 视觉传感器 类型 原理 ROS支持 数据类型 典型型号/驱动 优缺点及应用场景 单目摄像头 单镜头成像,通过透视变换获取2D图像,依赖算法推断深度 驱…...

【ubuntu】在Linux Yocto的基础上去适配Ubuntu的wifi模块

一、修改wifi的节点名 1.找到wifi模块的PID和VID ifconfig查看wifi模块网络节点的名字&#xff0c;发现是wlx44876393bb3a&#xff08;wlxmac地址&#xff09; 通过udevadm info -a /sys/class/net/wlx44876393bba路径的命令去查看wlx44876393bba的总线号&#xff0c;端口号…...

基于WebRTC技术的EasyRTC:支持任意平台设备的实时音视频通信解决方案

一、技术架构与核心优势 EasyRTC是一套基于WebRTC技术的实时音视频通信框架&#xff0c;旨在为开发者提供高效、稳定、跨平台的通信解决方案。其核心优势在于支持任意平台设备&#xff0c;包括Web端、移动端、桌面端和嵌入式设备&#xff0c;真正实现“一次开发&#xff0c;多…...

51单片机实验四:键盘检测原理及应用实现

目录 一、实验环境与实验器材 二、实验内容及实验步骤 1.独立键盘检测 2.独立键盘(简易版本&#xff09; 3&#xff0e;矩阵键盘检测 4.矩阵键盘&#xff08;简单版&#xff0c;单数码管&#xff09;&#xff1a; 一、实验环境与实验器材 环境&#xff1a;Keli&#xff0c…...

GN ninja 工程化构建例程

文章目录 1. 前言✨2. 工程实例🚩2.1 工程目录结构2.2 工程顶层.gn文件2.3 工具链配置.gn文件2.4 编译配置.gn文件2.5 编译目标配置.gn文件2.6 工程接口文件2.7 动态库编译.gn文件2.8 动态库源文件2.9 静态库编译.gn文件2.10 静态库源文件2.11 主程序编译.gn文件2.12 主程序源…...

STC定时器频率占空比程序

// // 一、宏定义区 // #include <STC15.H> //头文件 #include <intrins.h> //库函数文件 #define FOSC 12000000L //IRC频率 typedef …...

观察者 ➜ 事件总线:一路走来的碎碎念

写给未来的自己:每次手敲事件模型都要 Google,干脆把思路和踩坑一次性记清楚。文章很长,都是唠叨,目的是让自己看两眼就能把设计理由找回来。 目录 为什么我要折腾事件模型?V0 ─ 单一事件的观察者模式V1 ─ 多事件同步总线(类型拆分)V2 ─ 订阅者优先级(链式调用可控)…...

AOP基本概念

上述语句解释感觉太过玄妙不似常人能够听懂&#xff0c;所以结合自己理解&#xff0c;给自己留点备注&#xff1a; 首先 目标对象&#xff1a; 就是这要对哪个对象进行代理&#xff0c;因为AOP是面向切面编程&#xff0c;在OOP的基础上再次解耦合&#xff0c;这个过程需要提…...

不确定与非单调推理的概率方法

前文我们学习了“不确定与非单调推理的基本概念”,了解了不确定性推理是人工智能领域中处理不完整、不精确或模糊信息的推理方法,其核心是在前提条件或推理规则存在不确定性时,通过某种数学或逻辑机制推导出合理结论,并对结论的可靠性进行量化。不确定与非单调推理的基本概…...

device_fingerprint、device_id、hmac生成

文章目录 1. 写在前面2. 设备信息3. 数美指纹 【&#x1f3e0;作者主页】&#xff1a;吴秋霖 【&#x1f4bc;作者介绍】&#xff1a;擅长爬虫与JS加密逆向分析&#xff01;Python领域优质创作者、CSDN博客专家、阿里云博客专家、华为云享专家。一路走来长期坚守并致力于Python…...

centos下openjdk报:getVersion(FontConfiguration.java)异常,安装fontconfig无效问题的处理

TOC centos下openjdk报:getVersion(FontConfiguration.java)异常,安装fontconfig无效问题的处理 官网jdk包&#xff1a;Releases dragonwell-project/dragonwell8 背景&#xff1a; 为了适应国产化&#xff0c;使用东方通和国产jdk&#xff0c;从tomcat改为tongweb&#x…...

Banana Pi BPI-RV2 RISC-V 路由器开发板发售, 全球首款RISC-V路由器

Banana Pi BPI-RV2 开源路由器是矽昌通信和⾹蕉派开源社区&#xff08;Banana Pi &#xff09;合作设计, 联合打造全球首款RISC-V架构路由器开发板。 这是香蕉派开源社区与矽昌通信继BPI-Wifi5 低成本Wifi5 路由器合作之后的又一力作&#xff0c;为全球开发者与商业客户提供基于…...

自学新标日第十九课复习版本

第十九课 基本–》否定 うー&#xff1e;わ 单词 单词假名声调词义品物しなもの0物品&#xff0c;商品お皿おさら0盘子ごみごみ2垃圾初心者しょしんしゃ2初学者上級者じょうきゅうしゃ3熟练者高級こうきゅう0高级上級クラス5高级版英会話えいかいわ3英语会话コース1路线&a…...

网安加·百家讲坛 | 刘志诚:AI安全风险与未来展望

作者简介&#xff1a;刘志诚&#xff0c;乐信集团信息安全中心总监、OWASP广东区域负责人、网安加社区特聘专家。专注于企业数字化过程中网络空间安全风险治理&#xff0c;对大数据、人工智能、区块链等新技术在金融风险治理领域的应用&#xff0c;以及新技术带来的技术风险治理…...

2025年03月中国电子学会青少年软件编程(Python)等级考试试卷(二级)真题

青少年软件编程&#xff08;Python&#xff09;等级考试试卷&#xff08;二级&#xff09; 分数&#xff1a;100 题数&#xff1a;37 答案解析&#xff1a;https://blog.csdn.net/qq_33897084/article/details/147340870 一、单选题(共25题&#xff0c;共50分) 1. 老师要求大…...

@JsonView + 单一 DTO:如何实现多场景 JSON 字段动态渲染

JsonView 单一 DTO&#xff1a;如何实现多场景 JSON 字段动态渲染 JsonView 单一 DTO&#xff1a;如何实现多场景 JSON 字段动态渲染1、JsonView 注解产生的背景2、为了满足不同场景下返回对应的属性的做法有哪些&#xff1f;2.1 最快速的实现则是针对不同场景新建不同的 DTO…...

《深入探秘JavaScript原型链与继承机制:解锁前端编程的核心密码》

在JavaScript的奇妙世界里&#xff0c;原型链与继承机制犹如隐藏的宝藏&#xff0c;掌握它们&#xff0c;就如同拿到了开启高效编程大门的钥匙。对于前端开发者来说&#xff0c;这不仅是写出简洁、可维护代码的关键&#xff0c;更是深入理解JavaScript面向对象编程的基石。今天…...

Cursor 生成java测试用例

1. 安装cursor 站点&#xff1a;https://www.cursor.com/cn 安装后登录 2. 使用cursor 2.1 安装扩展&#xff1a; 组合键 CtrlShiftX&#xff0c;进入扩展程序页面&#xff0c;安装如下&#xff1a; Chinese&#xff1a;中文支持&#xff0c; 安装后 CtrlShiftP&#xff0…...

常见免杀框架的使用(3款)---【AniYaGUI1.2.0、AV_Evasion_Tool掩日、FoxBypass_V1.0】

一、AniYaGUI1.2.0免杀框架 环境&#xff1a;虚拟机Win10 、云服务器 工具&#xff1a;Xshell、CobaltStrike 项目下载地址&#xff1a; https://github.com/piiperxyz/AniYa 1. 安装Go语言环境 确保Win10虚拟机安装 Golang 且环境变量中包含 go 否则⽆法编译&#xff08;注…...

PHP腾讯云人脸核身生成 SDK 接口调用步骤使用签名

参考腾讯云官方文档&#xff1a; 人脸核身 生成 SDK 接口调用步骤使用签名_腾讯云 前提条件&#xff1a;成功获取NonceTicket。 获取参考文档&#xff1a; PHP腾讯云人脸核身获取NONCE ticket-CSDN博客 function getTxFaceSign(){$appId ;$userId ;$version 1.0.0;$tic…...