1.多线程原理:
(1)概述:
多线程是指CPU可以在一段时间中并行执行多个程序,比如我们可以一边听音乐、一边写代码(这两个程序可以“同时进行”,我们称之为多进程,而多进程实现的本质就是内核多线程)。实际上,针对多核CPU,可以将这两个程序放在不同的核上边执行,但是单核CPU没法这样,所以单核CPU的多线程只能在一个核上边短时间切换执行程序任务,由于切换的速度很快,这样使用者会感受到多个任务似乎是同时执行的。(当然,多核CPU也会快速切换任务,毕竟可以同时执行的程序是比CPU核数量多的)
(2)线程上下文:
线程的执行需要用到多个通用寄存器,并且为了安全性考虑,每个线程的物理空间应该是隔离的,所以每个线程的执行也需要各自的物理空间,由于线程执行的栈空间是必要的,所以每个线程的esp、ss等寄存器的值也是不同的。线程的上下文实际上就是指的这一系列寄存器的数据,当我们切换线程执行时,我们也需要同时将现在线程的上下文保存起来,再将目标线程的上下文加载到寄存器中,这个过程我们称之为线程上下文切换。
同样,线程的切换也是伴随着执行流的改变,而执行流的改变是通过修改cs以及eip寄存器实现的,这也就是线程上下文切换的内容之一。
(3)TCB
每个线程需要对应一个线程控制结构块(TCB),TCB存放了线程的相关信息,比如线程ID等等,并且TCB可以存放线程切换上下文。所有线程的TCB会串起来存放(单向链表),而线程控制模块的功能就是维护这个链表。
2.动手实现吧
内核线程的内容比较少,实现起来也非常简单
/include/thread.h
#ifndef THREADS_H
#define THREADS_H
#include "types.h"
typedef //定义线程或进程状态
enum task_status_t{ TASK_RUNNING,TASK_READY,TASK_BLOCKED,TASK_WAITING,TASK_HANGING,TASK_DIED
} task_status_t;typedef
struct context_t{ //存放在内核栈中的任务上下文uint32_t ebp;uint32_t ebx;uint32_t ecx;uint32_t edx;uint32_t esi;uint32_t edi;uint32_t eflags;uint32_t esp; //esp是保存在kern_stack_top中的
} __attribute__((packed)) context_t; //由于要在汇编中使用 要编译成连续的分布typedef
struct TCB_t{uint32_t * kern_stack_top; //对应的内核栈顶地址task_status_t task_status; uint32_t time_counter; //记录运行总的时钟中断数uint32_t time_left; //剩余时间片struct TCB_t * next; //下一个TCB(用于线程调度)//uint32_t idt_addr; 在用户进程中使用的页表uint32_t tid; //线程iduint32_t page_counte; //分配的页空间大小uint32_t page_addr; //page_counte与page_addr用于释放内存context_t context;
} TCB_t;typedef void * thread_function(void * args); //定义线程的实际执行函数类型extern void switch_to(void * cur_coontext_ptr,void * next_context_ptr); //使用汇编完成的切换上下文函数extern uint32_t get_esp();void schedule();void create_thread(uint32_t tid,thread_function *func,void * args,uint32_t addr,uint32_t page_counte); //创建线程函数void threads_init(); //线程模块初始化 需要把主线程加入运行表中void exit(); //线程结束函数 关闭中断->移出执行链表->回收内存空间->开启中断
#endif
task_status_t枚举类定义了线程的状态,不过目前我们简单的实现当中只用到了RUNNING(正在运行中)。 context_t定义了线程上下文,主要是线程切换需要保存的几个寄存器。TCB_T定义了线程的TCB结构。
/kernel/init/threads.c
#include "threads.h"
#include "types.h"
#include "pmm.h"
#include "printk.h"
#define TIME_CONT 2 //默认时间片计数
TCB_t main_TCB; //内核主线程TCB
TCB_t* cur_tcb;void threads_init(){TCB_t *tcb_buffer_addr = &main_TCB;tcb_buffer_addr->tid = 0; //主线程的编号为0 tcb_buffer_addr->time_counter=0;tcb_buffer_addr->time_left=TIME_CONT;tcb_buffer_addr->task_status = TASK_RUNNING;tcb_buffer_addr->page_counte=0; //主线程不会被回收内存 所以可以任意赋值tcb_buffer_addr->page_addr=0;tcb_buffer_addr->next = tcb_buffer_addr;tcb_buffer_addr->kern_stack_top=0;cur_tcb = tcb_buffer_addr;
}uint32_t create_TCB(uint32_t tid,uint32_t page_addr,uint32_t page_counte){TCB_t * tcb_buffer_addr = (TCB_t*)page_addr;tcb_buffer_addr->tid = tid; tcb_buffer_addr->time_counter=0;tcb_buffer_addr->time_left=TIME_CONT;tcb_buffer_addr->task_status = TASK_RUNNING;tcb_buffer_addr->page_counte=page_counte; tcb_buffer_addr->page_addr=page_addr;tcb_buffer_addr->kern_stack_top=page_addr+page_counte*4096;return page_addr;
}void create_thread(uint32_t tid,thread_function *func,void *args,uint32_t addr,uint32_t page_counte){ asm volatile("cli"); //由于创建过程会使用到共享的数据 不使用锁的话会造成临界区错误 所以我们在此处关闭中断TCB_t * new_tcb = create_TCB(tid,addr,page_counte);TCB_t * temp_next = cur_tcb->next;cur_tcb->next = new_tcb;new_tcb->next = temp_next;*(--new_tcb->kern_stack_top)=args; //压入初始化的参数与线程执行函数*(--new_tcb->kern_stack_top)=exit;*(--new_tcb->kern_stack_top)=func;new_tcb->context.eflags = 0x200;new_tcb->context.esp =new_tcb->kern_stack_top;asm volatile("sti");
}void schedule(){ //调度函数 检测时间片为0时调用此函数if(cur_tcb->next==cur_tcb){cur_tcb->time_left = TIME_CONT; //如果只有一个线程 就再次给此线程添加时间片return ;}TCB_t *now = cur_tcb;TCB_t *next_tcb = cur_tcb->next;next_tcb->time_left = TIME_CONT;cur_tcb = next_tcb;get_esp(); //有一个隐藏bug 需要call刷新寄存器switch_to(&(now->context),&(next_tcb->context));
}void remove_thread(){asm volatile("cli");if(cur_tcb->tid==0)printk("ERRO:main thread can`t use function exitn");else{TCB_t *temp = cur_tcb;for(;temp->next!=cur_tcb;temp=temp->next);temp->next = cur_tcb->next;}
}void exit(){remove_thread();TCB_t *now = cur_tcb;TCB_t *next_tcb = cur_tcb->next;next_tcb->time_left = TIME_CONT;cur_tcb = cur_tcb->next;switch_to(&(now->context),&(next_tcb->context));//注意 暂时没有回收此线程页
}#undef TIME_CONT
main_TCB即目前正在运行的主线程的上下文,其初始化是在thread_init函数中实现的。cur_tcb是指向目前正在运行的线程的控制块的指针。schedule函数用于将线程切换到cur_tcb中的next对应的PCB的线程,最终线程的切换是通过switch_to实现的,这个函数定义在后边一个汇编文件中。其他剩下的函数是用来维护TCB链表的(增加 删除 修改等操作)。值得注意的一点:每个线程都有自己的栈,而栈的分配是在create_thread中通过pmm模块分配对应物理内存页。
但是,一些函数是不能用c语言实现的,比如上下文切换中会涉及到寄存器的访问。
/kernel/init/threads_asm.s
;内核线程模块的汇编函数文件
[bits 32]
[GLOBAL get_esp]
get_esp:mov eax,espret
[GLOBAL get_eflags]
get_eflags:pushfpop eaxret
[GLOBAL switch_to]
switch_to:;保存上下文mov [eax+28],espmov eax,[esp+4] ;第一个参数 mov [eax],ebpmov [eax+4],ebxmov [eax+8],ecxmov [eax+12],edxmov [eax+16],esimov [eax+20],edipush ebxmov ebx,eaxpushfpop eaxmov [ebx+24],eaxmov eax,ebxpop ebx;加载上下文mov eax,[esp+8] ;第二个参数mov esp,[eax+28]mov ebp,[eax]mov ebx,[eax+4]mov ecx,[eax+8]mov edx,[eax+12]mov esi,[eax+16]mov edi,[eax+20]add eax,24push dword [eax] ;eflagspopf ;由于8259a设置的手动模式 所以必须给主片与从片发送信号 否则8259a会暂停;这个bug找了一下午才找到 顺便吐槽下 内核级的代码debug太难了(GDB在多线程与汇编级会失效 只有print调试法) mov al,0x20 out 0xA0,alout 0x20,alret ;执行下一个函数
switch_to的实现比较简单,函数传入的第一个参数是正在执行的线程的上下文,第二个参数是要切换到的函数的上下文。
当然,由于线程的切换是由时钟中断驱动的,所以要修改时钟中断的处理函数
extern TCB_t * cur_tcb;
//时钟中断函数 主要用于线程调度
void timer_server_func(void *args){if(cur_tcb->time_left!=0){(cur_tcb->time_left)--;(cur_tcb->time_counter)++;}else{schedule();}
}
在时钟中断时,会检查现在正在执行的线程的TCB时间片是否为0,为0的话就会切换到下一个函数执行,否者,就会将时间片-1
最后,来测试一下多线程:
/kernel/entry.c
#include "types.h"
#include "vga_basic.h"
#include "printk.h"
#include "init.h"
#include "pmm.h"
#include "threads.h"
void clear_screen();
void kputc(char);
void screen_uproll_once();
uint32_t get_eflags();
extern TCB_t * cur_tcb;
extern TCB_t main_TCB;
void kern_entry(){void func(void* args);vga_init();idt_init(); pmm_init();threads_init();create_thread(1,(thread_function *)func,0,pmm_alloc_one_page().addr,1);asm volatile ("sti"); //要在主线程加载完后开中断while(True){asm volatile("cli");printk("A");asm volatile("sti");}while(True)asm volatile ("hlt");
}void func(void* args){while(True){asm volatile("cli");printk_color("B",15,0);asm volatile("sti");}
}
最后的测试结果应该是交替打印出A和B字符(当然 此处没有放图 因为后续开发还在进行中 无法测试之前的代码了)
下一节预告:
实现虚拟内存管理,为实现用户进程做准备