【自製作業系統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.com/sunym1993/flashos

當你看到該文章時,程式碼可能已經比文章中的又多寫了一些部分了。你可以通過提交記錄歷史來查看歷史的程式碼,我會慢慢梳理提交歷史以及項目說明文檔,爭取給每一課都準備一個可執行的程式碼。當然文章中的程式碼也是全的,採用複製粘貼的方式也是完全可以的。

如果你有興趣加入這個自製作業系統的大軍,也可以在留言區留下您的聯繫方式,或者在 gitee 私信我您的聯繫方式。

課程規劃

本課程打算出系列課程,我寫到哪覺得可以寫成一篇文章了就寫出來分享給大家,最終會完成一個功能全面的作業系統,我覺得這是最好的學習作業系統的方式了。所以中間遇到的各種坎也會寫進去,如果你能持續跟進,跟著我一塊寫,必然會有很好的收貨。即使沒有,交個朋友也是好的哈哈。

目前的系列包括

 微信公眾號

  我要去阿里(woyaoquali)

 小助手微訊號

  Angel(angel19980323)