【自製作業系統15】用戶進程
一、到目前為止的程式流程圖
為了讓大家清楚目前的程式進度,畫了到目前為止的程式流程圖,如下。
二、CPU 原生支援多任務切換
沒錯,本來多任務同分頁、中斷、段選擇子一樣,都是軟硬體配合的產物,CPU 廠商也在硬體層面用 TSS 結構支援多任務,同中斷的邏輯一樣,也是有個 TSS 描述符存在 GDT 全局描述符表裡,有個 TR 暫存器存儲 TSS 的初始記憶體地址,然後只需要用一個簡單的 call 指令,後面地址指向的描述符是一個 TSS 描述符的時候,就會發生任務切換,一條指令,很方便。
但硬體其實也是通過 很多微指令 實現的任務切換,雖然程式設計師很方便用了一條指令就切換了任務,但實際上會產生一個很複雜很耗時的一些列操作,具體是啥我也沒研究。
所以現在的作業系統幾乎都沒有用原生的方式實現多任務,而是用軟體方式自己實現,僅僅把 TSS 當作為 0 特權級的任務提供棧,不過那是因為硬體要求必須這麼做,不然作業系統可能完全會忽視 TSS 的所有支援。比如 Linux 的做法就是,一次性載入 TSS 到 TR,之後不斷修改同一個 TSS 的內容,不再進行重複載入操作。 Linux 在 TSS 中只初始化了 SS0、esp0 和 I/O 點陣圖欄位,除此之外 TSS 便沒用了,就是個空架子,不再做保存任務狀態之用。
三、為應付 CPU 實現 TSS
正如上文所說,我們只是應付一下
userprog/tss.c
1 #include "tss.h" 2 #include "stdint.h" 3 #include "global.h" 4 #include "string.h" 5 #include "print.h" 6 7 /* 任務狀態段tss結構 */ 8 struct tss { 9 uint32_t backlink; 10 uint32_t* esp0; 11 uint32_t ss0; 12 uint32_t* esp1; 13 uint32_t ss1; 14 uint32_t* esp2; 15 uint32_t ss2; 16 uint32_t cr3; 17 uint32_t (*eip) (void); 18 uint32_t eflags; 19 uint32_t eax; 20 uint32_t ecx; 21 uint32_t edx; 22 uint32_t ebx; 23 uint32_t esp; 24 uint32_t ebp; 25 uint32_t esi; 26 uint32_t edi; 27 uint32_t es; 28 uint32_t cs; 29 uint32_t ss; 30 uint32_t ds; 31 uint32_t fs; 32 uint32_t gs; 33 uint32_t ldt; 34 uint32_t trace; 35 uint32_t io_base; 36 }; 37 static struct tss tss; 38 39 /* 更新tss中esp0欄位的值為pthread的0級線 */ 40 void update_tss_esp(struct task_struct* pthread) { 41 tss.esp0 = (uint32_t*)((uint32_t)pthread + PG_SIZE); 42 } 43 44 /* 創建gdt描述符 */ 45 static struct gdt_desc make_gdt_desc(uint32_t* desc_addr, uint32_t limit, uint8_t attr_low, uint8_t attr_high) { 46 uint32_t desc_base = (uint32_t)desc_addr; 47 struct gdt_desc desc; 48 desc.limit_low_word = limit & 0x0000ffff; 49 desc.base_low_word = desc_base & 0x0000ffff; 50 desc.base_mid_byte = ((desc_base & 0x00ff0000) >> 16); 51 desc.attr_low_byte = (uint8_t)(attr_low); 52 desc.limit_high_attr_high = (((limit & 0x000f0000) >> 16) + (uint8_t)(attr_high)); 53 desc.base_high_byte = desc_base >> 24; 54 return desc; 55 } 56 57 /* 在gdt中創建tss並重新載入gdt */ 58 void tss_init() { 59 put_str("tss_init start\n"); 60 uint32_t tss_size = sizeof(tss); 61 memset(&tss, 0, tss_size); 62 tss.ss0 = SELECTOR_K_STACK; 63 tss.io_base = tss_size; 64 65 /* gdt段基址為0x900,把tss放到第4個位置,也就是0x900+0x20的位置 */ 66 67 /* 在gdt中添加dpl為0的TSS描述符 */ 68 *((struct gdt_desc*)(GDT_BASE_ADDR+(0x8*4)))= make_gdt_desc((uint32_t*)&tss, tss_size - 1, TSS_ATTR_LOW, TSS_ATTR_HIGH); 69 70 /* 在gdt中添加dpl為3的數據段和程式碼段描述符 */ 71 *((struct gdt_desc*)(GDT_BASE_ADDR+(0x8*5))) = make_gdt_desc((uint32_t*)0, 0xfffff, GDT_CODE_ATTR_LOW_DPL3, GDT_ATTR_HIGH); 72 *((struct gdt_desc*)(GDT_BASE_ADDR+(0x8*6))) = make_gdt_desc((uint32_t*)0, 0xfffff, GDT_DATA_ATTR_LOW_DPL3, GDT_ATTR_HIGH); 73 74 /* gdt 16位的limit 32位的段基址 */ 75 uint64_t gdt_operand = ((8 * 7 - 1) | ((uint64_t)(uint32_t)GDT_BASE_ADDR << 16)); // 7個描述符大小 76 asm volatile ("lgdt %0" : : "m" (gdt_operand)); 77 asm volatile ("ltr %w0" : : "r" (SELECTOR_TSS)); 78 put_str("tss_init and ltr done\n"); 79 }
上述程式碼我們在 GDT 里增加了 TSS 描述符,和兩個為後續用戶進程準備的 程式碼段 和 數據段,我們分別用 bochs 的 info gdt 和 info tss 看下目前的 GDT 結構,以及我們載入的唯一一個 TSS 的結構
GDT
可以看到,序號 0x04 就是 TSS 描述符,05 和 06 是新準備的程式碼段和數據段。
TSS
四、實現用戶進程
鋪墊工作都做好了,下面開始最關鍵的實現用戶進程部分
還記得之前我們實現多執行緒的時候,定義的 task_struct 么,我們在之前的基礎上加了屬性 userprog_vaddr 用於指向用戶進程的虛擬地址
thread.h
1 struct task_struct { 2 uint32_t* self_kstack; // 各內核執行緒都用自己的內核棧 3 pid_t pid; 4 enum task_status status; 5 char name[TASK_NAME_LEN]; 6 uint8_t priority; 7 uint8_t ticks; // 每次在處理器上執行的時間嘀嗒數 8 /* 此任務自上cpu運行後至今佔用了多少cpu嘀嗒數, 9 * 也就是此任務執行了多久*/ 10 uint32_t elapsed_ticks; 11 /* general_tag的作用是用於執行緒在一般的隊列中的結點 */ 12 struct list_elem general_tag; 13 /* all_list_tag的作用是用於執行緒隊列thread_all_list中的結點 */ 14 struct list_elem all_list_tag; 15 uint32_t* pgdir; // 進程自己頁表的虛擬地址 16 struct virtual_addr userprog_vaddr; // 用戶進程的虛擬地址 17 struct mem_block_desc u_block_desc[DESC_CNT]; // 用戶進程記憶體塊描述符 18 int32_t fd_table[MAX_FILES_OPEN_PER_PROC]; // 已打開文件數組 19 uint32_t cwd_inode_nr; // 進程所在的工作目錄的inode編號 20 pid_t parent_pid; // 父進程pid 21 int8_t exit_status; // 進程結束時自己調用exit傳入的參數 22 uint32_t stack_magic; // 用這串數字做棧的邊界標記,用於檢測棧的溢出 23 };
之後我們按照程式碼調用順序來看
main.c
1 ... 2 int test_var_a = 0, test_var_b = 0; 3 4 int main(void){ 5 put_str("I am kernel\n"); 6 init_all(); 7 thread_start("threadA", 31, k_thread_a, "AOUT_"); 8 thread_start("threadB", 31, k_thread_b, "BOUT_"); 9 process_execute(u_prog_a, "userProcessA"); 10 process_execute(u_prog_b, "userProcessB"); 11 intr_enable(); 12 while(1); 13 return 0; 14 } 15 16 void k_thread_a(void* arg) { 17 char* para = arg; 18 while(1) { 19 console_put_str("threadA:"); 20 console_put_int(test_var_a); 21 console_put_str("\n"); 22 } 23 } 24 25 void k_thread_b(void* arg) { 26 char* para = arg; 27 while(1) { 28 console_put_str("threadB:"); 29 console_put_int(test_var_b); 30 console_put_str("\n"); 31 } 32 } 33 34 void u_prog_a(void) { 35 while(1) { 36 test_var_a++; 37 } 38 } 39 40 void u_prog_b(void) { 41 while(1) { 42 test_var_b++; 43 } 44 }
process.c 中創建進程的主函數
1 /* 創建用戶進程 */ 2 void process_execute(void* filename, char* name) { 3 /* pcb內核的數據結構,由內核來維護進程資訊,因此要在內核記憶體池中申請 */ 4 struct task_struct* thread = get_kernel_pages(1); 5 init_thread(thread, name, default_prio); 6 create_user_vaddr_bitmap(thread); 7 thread_create(thread, start_process, filename); 8 thread->pgdir = create_page_dir(); 9 10 enum intr_status old_status = intr_disable(); 11 list_append(&thread_ready_list, &thread->general_tag); 12 list_append(&thread_all_list, &thread->all_list_tag); 13 intr_set_status(old_status); 14 }
裡面連續調用了 5 個函數(其中黃色的是比創建執行緒多出來的),再加上兩個添加鏈表函數,完成了創建進程的功能,下面我們看這五個函數都幹了什麼


1 // 從內核物理記憶體池中申請1頁記憶體,成功返回虛擬地址,失敗NULL 2 void* get_kernel_pages(uint32_t pg_cnt) { 3 void* vaddr = malloc_page(PF_KERNEL, pg_cnt); 4 if (vaddr != NULL) { 5 memset(vaddr, 0, pg_cnt * PG_SIZE); 6 } 7 return vaddr; 8 }
get_kernel_pages


1 // 初始化執行緒基本資訊 2 void init_thread(struct task_struct* pthread, char* name, int prio) { 3 memset(pthread, 0, sizeof(*pthread)); 4 strcpy(pthread->name, name); 5 6 if (pthread == main_thread) { 7 pthread->status = TASK_RUNNING; 8 } else { 9 pthread->status = TASK_READY; 10 } 11 pthread->priority = prio; 12 // 執行緒自己在內核態下使用的棧頂地址 13 pthread->self_kstack = (uint32_t*)((uint32_t)pthread + PG_SIZE); 14 pthread->ticks = prio; 15 pthread->elapsed_ticks = 0; 16 pthread->pgdir = NULL; 17 pthread->stack_magic = 0x19870916; // 自定義魔數 18 }
init_thread


1 /* 創建用戶進程虛擬地址點陣圖 */ 2 void create_user_vaddr_bitmap(struct task_struct* user_prog) { 3 user_prog->userprog_vaddr.vaddr_start = USER_VADDR_START; 4 uint32_t bitmap_pg_cnt = DIV_ROUND_UP((0xc0000000 - USER_VADDR_START) / PG_SIZE / 8 , PG_SIZE); 5 user_prog->userprog_vaddr.vaddr_bitmap.bits = get_kernel_pages(bitmap_pg_cnt); 6 user_prog->userprog_vaddr.vaddr_bitmap.btmp_bytes_len = (0xc0000000 - USER_VADDR_START) / PG_SIZE / 8; 7 bitmap_init(&user_prog->userprog_vaddr.vaddr_bitmap); 8 }
create_user_vaddr_bitmap


1 // 初始化執行緒棧 thread_stack 2 void thread_create(struct task_struct* pthread, thread_func function, void* func_arg) { 3 // 先預留中斷使用棧的空間 4 pthread->self_kstack -= sizeof(struct intr_stack); 5 // 再留出執行緒棧空間 6 pthread->self_kstack -= sizeof(struct thread_stack); 7 struct thread_stack* kthread_stack = (struct thread_stack*)pthread->self_kstack; 8 kthread_stack->eip = kernel_thread; 9 kthread_stack->function = function; 10 kthread_stack->func_arg = func_arg; 11 kthread_stack->ebp = kthread_stack->ebx = kthread_stack->esi = kthread_stack->edi = 0; 12 }
thread_create


1 // 創建頁目錄表,將當前頁表的表示內核空間的pde複製 2 uint32_t* create_page_dir(void) { 3 4 /* 用戶進程的頁表不能讓用戶直接訪問到,所以在內核空間來申請 */ 5 uint32_t* page_dir_vaddr = get_kernel_pages(1); 6 if (page_dir_vaddr == NULL) { 7 console_put_str("create_page_dir: get_kernel_page failed!"); 8 return NULL; 9 } 10 11 /************************** 1 先複製頁表 *************************************/ 12 /* page_dir_vaddr + 0x300*4 是內核頁目錄的第768項 */ 13 memcpy((uint32_t*)((uint32_t)page_dir_vaddr + 0x300*4), (uint32_t*)(0xfffff000+0x300*4), 1024); 14 /*****************************************************************************/ 15 16 /************************** 2 更新頁目錄地址 **********************************/ 17 uint32_t new_page_dir_phy_addr = addr_v2p((uint32_t)page_dir_vaddr); 18 /* 頁目錄地址是存入在頁目錄的最後一項,更新頁目錄地址為新頁目錄的物理地址 */ 19 page_dir_vaddr[1023] = new_page_dir_phy_addr | PG_US_U | PG_RW_W | PG_P_1; 20 /*****************************************************************************/ 21 return page_dir_vaddr; 22 }
create_page_dir
這裡卡了我好多天,一直就調不通,煩得我連部落格都不想繼續寫了,於是放棄了… 後面還有文件系統這一塊,不打算寫啦
後面直接讀 linux 源碼來了解作業系統,敬請期待吧
寫在最後:開源項目和課程規劃
如果你對自製一個作業系統感興趣,不妨跟隨這個系列課程看下去,甚至加入我們(下方有公眾號和小助手微信),一起來開發。
參考書籍
《作業系統真相還原》這本書真的贊!強烈推薦
項目開源
當你看到該文章時,程式碼可能已經比文章中的又多寫了一些部分了。你可以通過提交記錄歷史來查看歷史的程式碼,我會慢慢梳理提交歷史以及項目說明文檔,爭取給每一課都準備一個可執行的程式碼。當然文章中的程式碼也是全的,採用複製粘貼的方式也是完全可以的。
如果你有興趣加入這個自製作業系統的大軍,也可以在留言區留下您的聯繫方式,或者在 gitee 私信我您的聯繫方式。
課程規劃
本課程打算出系列課程,我寫到哪覺得可以寫成一篇文章了就寫出來分享給大家,最終會完成一個功能全面的作業系統,我覺得這是最好的學習作業系統的方式了。所以中間遇到的各種坎也會寫進去,如果你能持續跟進,跟著我一塊寫,必然會有很好的收貨。即使沒有,交個朋友也是好的哈哈。
目前的系列包括
- 【自製作業系統01】硬核講解電腦的啟動過程
- 【自製作業系統02】環境準備與啟動區實現
- 【自製作業系統03】讀取硬碟中的數據
- 【自製作業系統04】從實模式到保護模式
- 【自製作業系統05】開啟記憶體分頁機制
- 【自製作業系統06】終於開始用 C 語言了,第一行內核程式碼!
- 【自製作業系統07】深入淺出特權級
- 【自製作業系統08】中斷
- 【自製作業系統09】中斷的程式碼實現
- 【自製作業系統10】記憶體管理系統
- 【自製作業系統11】中場休息之細節是魔鬼
- 【自製作業系統12】熟悉而陌生的多執行緒
- 【自製作業系統13】鎖
- 【自製作業系統14】實現鍵盤輸入
微信公眾號
我要去阿里(woyaoquali)
小助手微訊號
Angel(angel19980323)