十二. 實現用戶進程

TSS

單核CPU想要實現多任務,唯一的方案就是多個任務共享同一個CPU,也就是讓CPU在多個任務間輪轉。

CPU執行任務時,需要把任務運行所需要的數據載入到寄存器、棧和內存中,因為CPU只能直接處理這些數據,這是CPU在設計上就直接決定的。任務的數據和指令是CPU的處理對象,任務的執行會佔用寄存器和內存。

內存相對於CPU來說是低速設備,裡面的數據往往被載入到高速寄存器之後被CPU處理,再將結果寫到內存中。所以寄存器中的數據就是當前任務的最新狀態。採用輪流使用CPU的方式運行多任務,在當前任務被換下CPU的時候,任務的狀態應該被保存一份,TSS就是用來關聯任務的。

TSS(任務狀態段)是由程序員來提供,CPU進行維護。程序員提供是指需要我們定義一個結構體,裡面存放任務要用的寄存器數據。CPU維護是指切換任務時,CPU會自動把舊任務的數據存放的結構體變數中,然後將新任務的TSS數據載入到相應的寄存器中

TSS和之前所說的段一樣,本質上也是一片存儲數據的內存區域,CPU用這塊內存區域保存任務的最新狀態。所以也需要一個描述符結構來表示它,這個描述符就是TSS描述符,它的結構如下

這個描述符主要關注於它type欄位中的B位,B位為1時表示任務繁忙。

任務繁忙有兩方面的意義,一是此任務正在CPU上運行,二是此任務嵌套調用了新的任務,CPU此時正在執行這個新的任務,此任務暫時掛起,等到新任務運行完了之後返回此任務繼續執行

這個B位是由CPU自動維護的,任務被調到上CPU的時候,CPU自動將此B位置1,被換下CPU的時候,自動將其置0

前面說的TSS描述符是用來描述TSS的,TSS的結構如下圖

TSS結構中的數據就是我們保存任務時需要存儲的數據,我們提供的保存TSS的數據結構也要照著這個設計。

Linux中採用的任務切換方式

Linux為了提高任務切換的速度,通過如下方式來進行任務切換

一個CPU上的所有任務共享一個TSS,通過TR寄存器保存這個TSS,在使用ltr指令載入TSS之後,該TR寄存器永遠指向同一個TSS,之後在進行任務切換的時候也不會重新載入TSS,只需要把TSS中的SS0和esp0更新為新任務的內核棧的段地址及棧指針。

在當初硬體廠商設計TSS的時候,本意是想讓一個任務保存一份TSS,這樣在切換任務的時候,重新從內存中載入TSS,讓TR寄存器指向該TSS,從而實現任務切換。但是這種方式切換任務,每次都要從內存中載入數據,對於CPU來說,速度太慢了,而且切換的步驟也十分繁瑣。

Linux的任務切換方式只需要修改TSS中的SS0和esp0,進行任務切換的速度當然是大幅度提升了。

初始化TSS

/* 任務狀態段tss結構 */struct tss { uint32_t backlink; uint32_t* esp0; uint32_t ss0; uint32_t* esp1; uint32_t ss1; uint32_t* esp2; uint32_t ss2; uint32_t cr3; uint32_t (*eip) (void); uint32_t eflags; uint32_t eax; uint32_t ecx; uint32_t edx; uint32_t ebx; uint32_t esp; uint32_t ebp; uint32_t esi; uint32_t edi; uint32_t es; uint32_t cs; uint32_t ss; uint32_t ds; uint32_t fs; uint32_t gs; uint32_t ldt; uint32_t trace; uint32_t io_base;};

該TSS的結構完全是根據上面圖中所需而定義的。

TSS的初始化工作主要是初始化TSS結構的ss0和esp0,然後將TSS描述符載入到全局描述符表中。

void update_tss_esp(task_struct* pthread) { tss.esp0 = (uint32_t*)((uint32_t)pthread + PG_SIZE);}static struct gdt_desc make_gdt_desc(uint32_t* desc_addr, uint32_t limit, uint8_t attr_low, uint8_t attr_high) { uint32_t desc_base = (uint32_t)desc_addr; struct gdt_desc desc; desc.limit_low_word = limit & 0x0000ffff; desc.base_low_word = desc_base & 0x0000ffff; desc.base_mid_byte = ((desc_base & 0x00ff0000) >> 16); desc.attr_low_byte = (uint8_t)(attr_low); desc.limit_high_attr_high = (((limit & 0x000f0000) >> 16) + (uint8_t)(attr_high)); desc.base_high_byte = desc_base >> 24; return desc;}void tss_init() { put_str("tss_init start
"); uint32_t tss_size = sizeof(tss); memset(&tss, 0, tss_size); tss.ss0 = SELECTOR_K_STACK; tss.io_base = tss_size; /* gdt段基址為0x900,把tss放到第4個位置,也就是0x900+0x20的位置 */ /* 在gdt中添加dpl為0的TSS描述符 */ *((struct gdt_desc*)0xc0000920) = make_gdt_desc((uint32_t*)&tss, tss_size - 1, TSS_ATTR_LOW, TSS_ATTR_HIGH); /* 在gdt中添加dpl為3的數據段和代碼段描述符 */ *((struct gdt_desc*)0xc0000928) = make_gdt_desc((uint32_t*)0, 0xfffff, GDT_CODE_ATTR_LOW_DPL3, GDT_ATTR_HIGH); *((struct gdt_desc*)0xc0000930) = make_gdt_desc((uint32_t*)0, 0xfffff, GDT_DATA_ATTR_LOW_DPL3, GDT_ATTR_HIGH); /* gdt 16位的limit 32位的段基址 */ uint64_t gdt_operand = ((8 * 7 - 1) | ((uint64_t)(uint32_t)0xc0000900 << 16)); // 7個描述符大小 asm volatile ("lgdt %0" : : "m" (gdt_operand)); asm volatile ("ltr %w0" : : "r" (SELECTOR_TSS)); put_str("tss_init and ltr done
");}

通過上面的步驟TSS描述符就被載入到gdt中了,可以直接在模擬器中觀察當前gdt的數據

實現用戶進程

實現原理

實現進程的過程是在之前的線程基礎上進行的,在當初創建線程的時候是將棧的返回地址指向了kernel_thread函數,通過該函數調用線程函數實現的,其執行流程如下

這裡我們只需要把執行線程的函數換成創建進程的函數就可以啦。

進程與線程最大的區別就每個進程都擁有單獨的4GB虛擬地址空間,所以,需要單獨為每個進程維護一個虛擬地址池,用來標記該進程中哪些地址被分配了,哪些地址沒有被分配

typedef struct tag_task_struct{ uint32_t *self_kstack; // 內核線程的棧頂地址 task_status status; // 當前線程的狀態 char name[16]; uint8_t priority; uint8_t ticks; // 線程執行的時間 uint32_t elapsed_ticks; // 線程已經執行的時間 struct list_elem general_tag; struct list_elem all_list_tag; uint32_t *pgdir; //進程頁表的虛擬地址 struct virtual_addr userprog_vaddr; // 用戶進程的虛擬地址池 uint32_t stack_magic; // 棧的邊界標記,用來檢測棧溢出}task_struct;

該結構是進程線程通用的,是用來管理進程或線程數據的。有些數據是進程特用的,這樣在線程使用該結構的時候只需要將這些數據置0即可。在這裡將這個結構作為進程的pcb使用。

為用戶進程創建頁表

// 在虛擬內存池中申請pg_cnt個虛擬頁static void *vaddr_get(enum pool_flags pf, uint32_t pg_cnt){ int vaddr_start = 0; int bit_idx_start = -1; uint32_t cnt = 0; if(pf == PF_KERNEL) { //...內核內存池 } else { // 用戶內存池 task_struct *cur = running_thread(); bit_idx_start = bitmap_scan(&cur->userprog_vaddr.vaddr_bitmap, pg_cnt); if(bit_idx_start == -1) return NULL; while (cnt < pg_cnt) { bitmap_set(&cur->userprog_vaddr.vaddr_bitmap, bit_idx_start + cnt++, 1); } vaddr_start = cur->userprog_vaddr.vaddr_start + bit_idx_start * PG_SIZE; ASSERT((uint32_t)vaddr_start < (0xc0000000 - PG_SIZE)); } return (void *)vaddr_start;}void *get_user_pages(uint32_t pg_cnt){ lock_acquire(&user_pool.lock); void *vaddr = malloc_page(PF_USER, pg_cnt); memset(vaddr, 0, pg_cnt * PG_SIZE); lock_release(&user_pool.lock); return vaddr;}

進入3特權級

到目前為止我們一直工作在0級特權級下,這裡既然是模仿操作系統的實現,用戶進程肯定還是要工作在3特權級下的。這個kernel再簡陋,基本的功能還是要有的。

一般情況下,CPU不允許從高特權級轉向低特權級,只有從中斷返回或者從調用門返回的情況下才可以。

這裡採用從中斷返回的方式進入3特權級。由於目前還沒有用戶進程,也就別談從中斷返回了,都沒有進入中斷何談從中斷返回呢,這裡就只能再次欺騙一下CPU,就像之前創建線程一樣,製造從中斷返回的條件,執行iretd指令了。

iretd指令會用帶棧中的數據作為返回地址,還會載入棧中的eflags的值到eflags寄存器,如果棧中的cs.rpl為更低的特權級,處理器的特權級檢查通過之後會將棧中的cs載入到CS寄存器。從中斷返回的過程就是進入中斷的逆過程,所以我們只需要在棧中準備好數據,調用iretd指令即可。

構建用戶進程初始上下文信息

void start_process(void *filename_){ void *function = filename_; task_struct *cur = running_thread(); cur->self_kstack += sizeof(thread_stack); intr_stack *proc_stack = (struct intr_stack *)cur->self_kstack; proc_stack->edi = proc_stack->esi = proc_stack->ebp = proc_stack->esp_dummy = 0; proc_stack->ebx = proc_stack->edx = proc_stack->ecx = proc_stack->eax = 0; proc_stack->gs = 0; // 用戶態用不上,直接初始為0 proc_stack->ds = proc_stack->es = proc_stack->fs = SELECTOR_U_DATA; proc_stack->eip = function; // 待執行的用戶程序地址 proc_stack->cs = SELECTOR_U_CODE; proc_stack->eflags = (EFLAGS_IOPL_0 | EFLAGS_MBS | EFLAGS_IF_1); proc_stack->esp = (void *)((uint32_t)get_a_page(PF_USER, USER_STACK3_VADDR) + PG_SIZE); proc_stack->ss = SELECTOR_U_DATA; asm volatile("movl %0, %%esp; jmp intr_exit" : : "g"(proc_stack) : "memory");}

激活頁表

/* 激活頁表 */void page_dir_activate(task_struct *p_thread){ /* 若為內核線程,需要重新填充頁表為0x100000 */ uint32_t pagedir_phy_addr = 0x100000; // 默認為內核的頁目錄物理地址,也就是內核線程所用的頁目錄表 if (p_thread->pgdir != NULL) { // 用戶態進程有自己的頁目錄表 pagedir_phy_addr = addr_v2p((uint32_t)p_thread->pgdir); } /* 更新頁目錄寄存器cr3,使新頁表生效 */ asm volatile("movl %0, %%cr3" : : "r"(pagedir_phy_addr) : "memory");}/* 激活線程或進程的頁表,更新tss中的esp0為進程的特權級0的棧 */void process_activate(task_struct *p_thread){ ASSERT(p_thread != NULL); /* 擊活該進程或線程的頁表 */ page_dir_activate(p_thread); /* 內核線程特權級本身就是0,處理器進入中斷時並不會從tss中獲取0特權級棧地址,故不需要更新esp0 */ if (p_thread->pgdir) { /* 更新該進程的esp0,用於此進程被中斷時保留上下文 */ update_tss_esp(p_thread); }}

創建用戶進程的頁目錄表

uint32_t *create_page_dir(void){ /* 用戶進程的頁表不能讓用戶直接訪問到,所以在內核空間來申請 */ uint32_t *page_dir_vaddr = get_kernel_pages(1); if (page_dir_vaddr == NULL) { console_put_str("create_page_dir: get_kernel_page failed!"); return NULL; } /************************** 1 先複製頁表 *************************************/ /* page_dir_vaddr + 0x300*4 是內核頁目錄的第768項 */ memcpy((uint32_t *)((uint32_t)page_dir_vaddr + 0x300 * 4), (uint32_t *)(0xfffff000 + 0x300 * 4), 1024); /*****************************************************************************/ /************************** 2 更新頁目錄地址 **********************************/ uint32_t new_page_dir_phy_addr = addr_v2p((uint32_t)page_dir_vaddr); /* 頁目錄地址是存入在頁目錄的最後一項,更新頁目錄地址為新頁目錄的物理地址 */ page_dir_vaddr[1023] = new_page_dir_phy_addr | PG_US_U | PG_RW_W | PG_P_1; /*****************************************************************************/ return page_dir_vaddr;}/* 創建用戶進程虛擬地址點陣圖 */void create_user_vaddr_bitmap(task_struct *user_prog){ user_prog->userprog_vaddr.vaddr_start = USER_VADDR_START; uint32_t bitmap_pg_cnt = DIV_ROUND_UP((0xc0000000 - USER_VADDR_START) / PG_SIZE / 8, PG_SIZE); user_prog->userprog_vaddr.vaddr_bitmap.bits = get_kernel_pages(bitmap_pg_cnt); user_prog->userprog_vaddr.vaddr_bitmap.btmp_bytes_len = (0xc0000000 - USER_VADDR_START) / PG_SIZE / 8; bitmap_init(&user_prog->userprog_vaddr.vaddr_bitmap);}

創建用戶進程

/* 創建用戶進程 */void process_execute(void *filename, char *name){ /* pcb內核的數據結構,由內核來維護進程信息,因此要在內核內存池中申請 */ task_struct *thread = get_kernel_pages(1); init_thread(thread, name, default_prio); create_user_vaddr_bitmap(thread); thread_create(thread, start_process, filename); thread->pgdir = create_page_dir(); enum intr_status old_status = intr_disable(); 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); intr_set_status(old_status);}

用戶進程一般是由載入器將用戶程序載入到內存中,再根據其文件格式解析裡面的內容,將程序的段載入到相應的內存地址,CS:EIP指向程序的入口地址,程序便執行起來了。由於目前還沒有實現文件系統,只能通過函數來模擬進程的執行,但是產生的效果是一樣的。

進程的創建便完成了。由於目前只能模擬一下進程的運行,所以模擬運行的線程函數任然運行在內核空間中,但是進程該有的屬性都有了,比如3特權級,自己單獨的頁表,3特權級棧等。擁有了這些屬性就可以稱之為一個用戶進程

這是運行了進程函數之後cs的值,其低2位的值為11,也就是rpl=3,目前確實運行在3特權級下.


推薦閱讀:

進程與進程管理 | 進程同步機制
CSAPP Lab -- Cache Lab
操作系統引論 | 操作系統的發展過程
國產操作系統還能怎麼做?
無人駕駛操作系統(OS)

TAG:進程 | 操作系統 | Linux內核 |