多线程操作时操作系统时间片_从零开始自制操作系统(15):内核多线程
1.多線程原理:
(1)概述:
多線程是指CPU可以在一段時間中并行執行多個程序,比如我們可以一邊聽音樂、一邊寫代碼(這兩個程序可以“同時進行”,我們稱之為多進程,而多進程實現的本質就是內核多線程)。實際上,針對多核CPU,可以將這兩個程序放在不同的核上邊執行,但是單核CPU沒法這樣,所以單核CPU的多線程只能在一個核上邊短時間切換執行程序任務,由于切換的速度很快,這樣使用者會感受到多個任務似乎是同時執行的。(當然,多核CPU也會快速切換任務,畢竟可以同時執行的程序是比CPU核數量多的)
(2)線程上下文:
線程的執行需要用到多個通用寄存器,并且為了安全性考慮,每個線程的物理空間應該是隔離的,所以每個線程的執行也需要各自的物理空間,由于線程執行的??臻g是必要的,所以每個線程的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(); //線程結束函數 關閉中斷->移出執行鏈表->回收內存空間->開啟中斷 #endiftask_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_CONTmain_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字符(當然 此處沒有放圖 因為后續開發還在進行中 無法測試之前的代碼了)
下一節預告:
實現虛擬內存管理,為實現用戶進程做準備
總結
以上是生活随笔為你收集整理的多线程操作时操作系统时间片_从零开始自制操作系统(15):内核多线程的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: linux目录隐藏技术,Linux环境下
- 下一篇: ecplice中class.fornam