目錄 一、前景回顧 二、進程的創建與初始化 三、如何進行進程的切換 四、運行測試 五、原書勘誤 一、前景回顧 在上一回我們大概講述了任務切換的發展,並且知道Linux採用的是一個CPU使用一個TSS的方式,在最後我們成功實現了tss。現在萬事俱備,我們正式來實現用戶進程。 二、進程的創建與初始化 進 ...
目錄
一、前景回顧
二、進程的創建與初始化
三、如何進行進程的切換
四、運行測試
五、原書勘誤
在上一回我們大概講述了任務切換的發展,並且知道Linux採用的是一個CPU使用一個TSS的方式,在最後我們成功實現了tss。現在萬事俱備,我們正式來實現用戶進程。
進程的創建與線程的創建很相似,這裡直接上圖來對比分析:
我們使用process_execute函數來創建初始化進程。
1 /*創建用戶進程*/ 2 void process_execute(void *filename, char *name) 3 { 4 /*pcb內核的數據結構,由內核來維護進程信息,因此要在內核記憶體池中申請*/ 5 struct task_struct *thread = get_kernel_pages(1); 6 init_thread(thread, name, 31); 7 thread_create(thread, start_process, filename); 8 create_user_vaddr_bitmap(thread); //創建虛擬地址的點陣圖 9 thread->pgdir = create_page_dir(); //用戶進程的頁目錄表的物理地址,這裡傳進來的是頁目錄表物理地址所對應的虛擬地址 10 11 enum intr_status old_status = intr_disable(); 12 ASSERT(!elem_find(&thread_ready_list, &thread->general_tag)); 13 list_append(&thread_ready_list, &thread->general_tag); 14 15 ASSERT(!elem_find(&thread_all_list, &thread->all_list_tag)); 16 list_append(&thread_all_list, &thread->all_list_tag); 17 intr_set_status(old_status); 18 }
在該函數中首先使用get_kernel_pages函數在內核物理空間中申請一頁物理記憶體來作為進程的PCB,因為最終調度是由內核來操控的,所以PCB統一都在內核物理空間中申請。隨後依舊調用init_thread()和thread_create()函數來初始化進程的PCB。
下麵開始不一樣了,create_user_vaddr_bitmap()函數的作用是給進程創建初始化點陣圖。這裡科普一下:我們都知道進程有4GB的虛擬空間,其中第1~3GB是分配給用戶空間,第4GB是分配給內核空間,這是Linux下的分配習慣,我們照搬。而用戶空間實際上只用上了0x08048000到0xc0000000這一部分。所以create_user_vaddr_bitmap()函數也就是將這一部分空間劃分到用戶的虛擬地址記憶體池中。
再來看create_page_dir()函數,我們知道操作系統被所有用戶進程所共用,所以我們將用戶進程頁目錄表中的第768~1023個頁目錄項用內核頁目錄表的第768~1023個頁目錄項代替,其實就是將內核所在的頁目錄項複製到進程頁目錄表中同等位置,這樣就能讓用戶進程的高1GB空間指向內核。最後再將進程添加到全部隊列和就緒隊列中供調度。至此,用戶進程就算創建初始化完畢了。
我們現在來看看進程的PCB的內容:
因為我們之前一直都是處於內核態下,也就是0特權級下。現在要切換到用戶進程也就是用戶態,3特權級下運行,和之前的切換不太一樣。還是舉例來說明吧。
假設當前內核線程A時間片用光了,在調度函數schedule()中會從就緒隊列中彈出下一個進程B的PCB,根據PCB我們就知道了進程B的所有信息。不過接下來和之前線程的切換不一樣了,首先調用process_activate()函數激活下一個內核線程或者進程的頁表。對於內核線程來說,內核線程的頁目錄表在之前激活分頁機制的時候就已經設定好了,被存放在0x10000地址處。如果不是內核線程,那麼就需要將進程B的頁目錄表地址賦給CR3寄存器,因為CPU定址是基於CR3寄存器中保存的頁目錄表的地址來定址的。切換到進程B後,需要將進程B的頁目錄表地址賦給了CR3寄存器。
1 /*激活線程或進程的頁表,更新tss中的esp0為進程的特權級0的棧*/
2 void process_activate(struct task_struct *p_thread)
3 {
4 ASSERT(p_thread != NULL);
5 //激活該線程或者進程的頁表
6 page_dir_activate(p_thread);
7
8 if (p_thread->pgdir) { //如果是進程那麼需要在tss中填入0級特權棧的esp0
9 update_tss_esp(p_thread);
10 }
11 }
process_activate
除此之外,還要將tss中的esp0欄位更新為進程B的0級棧。前面已經說過,進程在由例如中斷等操作從3特權級進入0特權級後,也就是進入內核態,使用的會是0特權級下的棧,不再是3特權級的棧。因此在這個地方我們需要給進程B更新0特權級棧。方便以後進程B進入內核態。這裡我們可以看到,進程B的0特權級的棧頂指針指向進程B的PCB最高處。
1 /*更新tss中的esp0欄位的值為pthread的0級棧*/
2 void update_tss_esp(struct task_struct *pthread)
3 {
4 tss.esp0 = (uint32_t *)((uint32_t)pthread + PG_SIZE);
5 }
update_tss_esp
這一系列操作完成後,我們又回到switch_to函數,和前面講線程切換也是一樣,首先通過一系列的push操作,將當前內核線程A的寄存器信息壓入棧中以便下次又被調度上CPU後可以恢復環境。隨後從進程B的PCB中得到新的棧。此時進程B的棧的情況如下:
1 switch_to: 2 push esi ;這裡是根據ABI原則保護四個寄存器 放到棧裡面 3 push edi 4 push ebx 5 push ebp 6 7 mov eax, [esp+20] ;esp+20的位置是cur cur的pcb賦值給eax 8 mov [eax], esp ;[eax]為pcb的內核棧指針變數 把當前環境的esp值記錄下來 9 10 mov eax, [esp+24] 11 mov esp, [eax] 12 13 pop ebp 14 pop ebx 15 pop edi 16 pop esi 17 ret
進程B的還是通過一系列POP操作,最終調用*eip所指向的函數kernel_thread,在該函數中又調用*function所指向的函數start_process(),該函數代碼如下:
1 void start_process(void *filename) 2 { 3 void *function = filename; 4 struct task_struct *cur = running_thread(); 5 cur->self_kstack += sizeof(struct thread_stack); 6 struct intr_stack *proc_stack = (struct intr_stack *)cur->self_kstack; 7 proc_stack->edi = proc_stack->esi = proc_stack->ebp = proc_stack->esp_dummy = 0; 8 proc_stack->ebx = proc_stack->edx = proc_stack->ecx = proc_stack->eax = 0; 9 proc_stack->gs = 0; 10 proc_stack->ds = proc_stack->es = proc_stack->fs = SELECTOR_U_DATA; //數據段選擇子 11 proc_stack->eip = function; //函數地址 ip 12 proc_stack->cs = SELECTOR_U_CODE; //cs ip cs選擇子 13 proc_stack->eflags = (EFLAGS_IOPL_0 | EFLAGS_MBS | EFLAGS_IF_1); //不能夠關閉中斷 ELFAG_IF_1 不然會導致無法調度 14 proc_stack->esp = (void *)((uint32_t)get_a_page(PF_USER, USER_STACK3_VADDR) + PG_SIZE); //棧空間在0xc0000000以下一頁的地方 當然物理記憶體是操作系統來分配 15 proc_stack->ss = SELECTOR_U_DATA; //數據段選擇子 16 asm volatile ("movl %0, %%esp; jmp intr_exit" : : "g" (proc_stack) : "memory"); 17 }
來細品一下這個函數的內容。還記得前面的那個進程的PCB圖嗎?
首先通過running_thread函數獲取到當前進程的PCB的地址。根據圖中我們可以知道self_kstack一開始是被賦值指向棧頂,也就是線程棧的開始位置。經過cur->self_kstack += sizeof(struct thread_stack)後,現在self_kstack指向中斷棧處了,如圖所示。然後定義一個pro_stack指針指向self_kstack。這個先記住,待會兒會用上。
隨後便是對一系列寄存器的初始化,重點關註ds、es、fs、cs、ss和gs這幾個段寄存器的初始化,我們將它們初始化為用戶進程下的3特權級的段選擇子。因為在用戶態下,我們是不能訪問0特權級下的代碼段和數據段的。對於gs寄存器,這裡其實不管是否設置為0都無所謂,因為用戶態下的程式是不能直接訪問顯存的,進程在從內核態進入用戶態時會進行特權檢查,如果gs段寄存器中的段選擇子的特權等級高於進程返回後的特權等級,CPU就會自動將段寄存器gs給置0,如果用戶進程一旦訪問顯存,就會報錯。
再往下就給esp賦值,這個地方是為了當回到用戶態空間後,給用戶程式指定一個棧頂指針。這裡我們將用戶態的棧頂指針設置為用戶態空間下的0xc0000000處。
最後通過內聯彙編:
asm volatile ("movl %0, %%esp; jmp intr_exit" : : "g" (proc_stack) : "memory");
將proc_stack所指向的值賦給當前進程的esp,也就是棧頂指針,前面我們知道proc_stack已經被賦好了值,為self_kstack。最後便是跳轉到intr_exit處執行代碼。
此時棧的情況如下:
然後intr_exit的代碼如下所示:
1 intr_exit: 2 add esp, 4 3 popad 4 pop gs 5 pop fs 6 pop es 7 pop ds 8 add esp, 4 9 iretd
看著代碼就很好理解了,首先add esp, 4跳過棧中的vec_no,隨後popad和pop操作彈出8個32位的通用寄存器和4個段寄存器。又是通過add esp, 4跳過棧中的err_code,最後執行iretd指令,將(*eip)、cs、eflags彈出,而我們事先已經將用戶進程要運行的函數地址存放在eip中。最後,由於我們跳轉後的用戶態,它的特權級不同於當前內核態的特權級,所以需要恢複舊棧,CPU自動將棧中的esp和ss彈出。這些值在我們前面的start_process()函數中已經初始化完畢。至此我們就已經完成了內核態到用戶態的轉換。
這裡我貼上本章所有相關代碼:
1 #include "process.h"
2 #include "thread.h"
3 #include "global.h"
4 #include "memory.h"
5 #include "debug.h"
6 #include "console.h"
7 #include "interrupt.h"
8 #include "tss.h"
9
10 extern void intr_exit(void);
11 extern struct list thread_ready_list; //就緒隊列
12 extern struct list thread_all_list;
13
14 void start_process(void *filename)
15 {
16 void *function = filename;
17 struct task_struct *cur = running_thread();
18 cur->self_kstack += sizeof(struct thread_stack);
19 struct intr_stack *proc_stack = (struct intr_stack *)cur->self_kstack;
20 proc_stack->edi = proc_stack->esi = proc_stack->ebp = proc_stack->esp_dummy = 0;
21 proc_stack->ebx = proc_stack->edx = proc_stack->ecx = proc_stack->eax = 0;
22 proc_stack->gs = 0;
23 proc_stack->ds = proc_stack->es = proc_stack->fs = SELECTOR_U_DATA; //數據段選擇子
24 proc_stack->eip = function; //函數地址 ip
25 proc_stack->cs = SELECTOR_U_CODE; //cs ip cs選擇子
26 proc_stack->eflags = (EFLAGS_IOPL_0 | EFLAGS_MBS | EFLAGS_IF_1); //不能夠關閉中斷 ELFAG_IF_1 不然會導致無法調度
27 proc_stack->esp = (void *)((uint32_t)get_a_page(PF_USER, USER_STACK3_VADDR) + PG_SIZE); //棧空間在0xc0000000以下一頁的地方 當然物理記憶體是操作系統來分配
28 proc_stack->ss = SELECTOR_U_DATA; //數據段選擇子
29 asm volatile ("movl %0, %%esp; jmp intr_exit" : : "g" (proc_stack) : "memory");
30 }
31
32
33 /*激活頁表*/
34 void page_dir_activate(struct task_struct *p_thread)
35 {
36 //內核線程的頁目錄表的物理地址為0x100000
37 uint32_t pagedir_phy_addr = 0x100000;
38 if (p_thread->pgdir != NULL) { //說明下一個調用的是進程,否則是內核線程
39 pagedir_phy_addr = addr_v2p((uint32_t)p_thread->pgdir);
40 }
41
42 /*更新頁目錄寄存器CR3,使新頁表生效*/
43 asm volatile("movl %0, %%cr3" : : "r" (pagedir_phy_addr) : "memory");
44 }
45
46 /*激活線程或進程的頁表,更新tss中的esp0為進程的特權級0的棧*/
47 void process_activate(struct task_struct *p_thread)
48 {
49 ASSERT(p_thread != NULL);
50 //激活該線程或者進程的頁表
51 page_dir_activate(p_thread);
52
53 if (p_thread->pgdir) { //如果是進程那麼需要在tss中填入0級特權棧的esp0
54 update_tss_esp(p_thread);
55 }
56 }
57
58 uint32_t *create_page_dir(void)
59 {
60 //用戶進程的頁表不能讓用戶直接訪問到,所以在內核空間申請
61 uint32_t *page_dir_vaddr = get_kernel_pages(1); //得到記憶體
62 if (page_dir_vaddr == NULL) {
63 console_put_str("create_page_dir: get_kernel_page failed!\n");
64 return NULL;
65 }
66
67 memcpy((uint32_t*)((uint32_t)page_dir_vaddr + 0x300 * 4), (uint32_t*)(0xfffff000 + 0x300 * 4), 1024); // 256項
68 uint32_t new_page_dir_phy_addr = addr_v2p((uint32_t)page_dir_vaddr);
69 page_dir_vaddr[1023] = new_page_dir_phy_addr | PG_US_U | PG_RW_W | PG_P_1; //最後一項是頁目錄項自己的地址
70
71 return page_dir_vaddr;
72 }
73
74
75 /*創建用戶進程虛擬地址點陣圖*/
76 void create_user_vaddr_bitmap(struct task_struct *user_prog)
77 {
78 user_prog->userprog_vaddr.vaddr_start = USER_VADDR_START;
79
80 //計算需要多少物理記憶體頁來記錄點陣圖 USER_VADDR_START為0x08048000
81 uint32_t bitmap_pg_cnt = DIV_ROUND_UP((0xc0000000 - USER_VADDR_START) / PG_SIZE / 8, PG_SIZE);
82 user_prog->userprog_vaddr.vaddr_bitmap.bits = get_kernel_pages(bitmap_pg_cnt);
83
84 user_prog->userprog_vaddr.vaddr_bitmap.btmp_bytes_len = (0xc0000000 - USER_VADDR_START) / PG_SIZE / 8;
85 bitmap_init(&user_prog->userprog_vaddr.vaddr_bitmap);
86 }
87
88 /*創建用戶進程*/
89 void process_execute(void *filename, char *name)
90 {
91 /*pcb內核的數據結構,由內核來維護進程信息,因此要在內核記憶體池中申請*/
92 struct task_struct *thread = get_kernel_pages(1);
93 init_thread(thread, name, 31);
94 thread_create(thread, start_process, filename);
95 create_user_vaddr_bitmap(thread); //創建虛擬地址的點陣圖
96 thread->pgdir = create_page_dir(); //用戶進程的頁目錄表的物理地址,這裡傳進來的是頁目錄表物理地址所對應的虛擬地址
97
98 enum intr_status old_status = intr_disable();
99 ASSERT(!elem_find(&thread_ready_list, &thread->general_tag));
100 list_append(&thread_ready_list, &thread->general_tag);
101
102 ASSERT(!elem_find(&thread_all_list, &thread->all_list_tag));
103 list_append(&thread_all_list, &thread->all_list_tag);
104 intr_set_status(old_status);
105 }
process.c
1 #ifndef __USERPROG_PROCESS_H
2 #define __USERPROG_PROCESS_H
3 #include "stdint.h"
4 #include "thread.h"
5
6 #define USER_STACK3_VADDR (0xc0000000 - 0x1000)
7 #define USER_VADDR_START 0x08048000
8
9
10 void process_execute(void *filename, char *name);
11 void create_user_vaddr_bitmap(struct task_struct *user_prog);
12 uint32_t *create_page_dir(void);
13 void process_activate(struct task_struct *p_thread);
14 void page_dir_activate(struct task_struct *p_thread);
15 void start_process(void *filename);
16
17 #endif
process.h
1 #include "memory.h"
2 #include "print.h"
3 #include "stdio.h"
4 #include "debug.h"
5 #include "string.h"
6 #include "thread.h"
7 #include "sync.h"
8
9 #define PG_SIZE 4096 //頁大小
10
11 /*0xc0000000是內核從虛擬地址3G起,
12 * 0x100000意指低端記憶體1MB,為了使虛擬地址在邏輯上連續
13 * 後面申請的虛擬地址都從0xc0100000開始
14 */
15 #define K_HEAP_START 0xc0100000
16
17 #define PDE_IDX(addr) ((addr & 0xffc00000) >> 22)
18 #define PTE_IDX(addr) ((addr & 0x003ff000) >> 12)
19
20 struct pool {
21 struct bitmap pool_bitmap; //本記憶體池用到的點陣圖結構
22 uint32_t phy_addr_start; //本記憶體池管理的物理記憶體的起始地址
23 uint32_t pool_size; //記憶體池的容量
24 struct lock lock;
25 };
26
27 struct pool kernel_pool, user_pool; //生成內核記憶體池和用戶記憶體池
28 struct virtual_addr kernel_vaddr; //此結構用來給內核分配虛擬地址
29
30
31 /*初始化記憶體池*/
32 static void mem_pool_init(uint32_t all_mem)
33 {
34 put_str("mem_pool_init start\n");
35 /*目前頁表和頁目錄表的占用記憶體
36 * 1頁頁目錄表 + 第0和第768個頁目錄項指向同一個頁表 + 第769~1022個頁目錄項共指向254個頁表 = 256個頁表
37 */
38 lock_init(&kernel_pool.lock);
39 lock_init(&user_pool.lock);
40
41 uint32_t page_table_size = PG_SIZE * 256;
42 uint32_t used_mem = page_table_size + 0x100000; //目前總共用掉的記憶體空間
43 uint32_t free_mem = all_mem - used_mem; //剩餘記憶體為32MB-used_mem
44 uint16_t all_free_pages = free_mem / PG_SIZE; //將剩餘記憶體劃分為頁,餘數捨去,方便計算
45
46 /*內核空間和用戶空間各自分配一半的記憶體頁*/
47 uint16_t kernel_free_pages = all_free_pages / 2;
48 uint16_t user_free_pages = all_free_pages - kernel_free_pages;
49
50 /*為簡化點陣圖操作,餘數不用做處理,壞處是這樣會丟記憶體,不過只要記憶體沒用到極限就不會出現問題*/
51 uint32_t kbm_length = kernel_free_pages / 8; //點陣圖的長度單位是位元組
52 uint32_t ubm_length = user_free_pages / 8;
53
54 uint32_t kp_start = used_mem; //內核記憶體池的起始物理地址
55 uint32_t up_start = kp_start + kernel_free_pages * PG_SIZE; //用戶記憶體池的起始物理地址
56
57 /*初始化內核用戶池和用戶記憶體池*/
58 kernel_pool.phy_addr_start = kp_start;
59 user_pool.phy_addr_start = up_start;
60
61 kernel_pool.pool_size = kernel_free_pages * PG_SIZE;
62 user_pool.pool_size = user_free_pages * PG_SIZE;
63
64 kernel_pool.pool_bitmap.btmp_bytes_len = kbm_length;
65 user_pool.pool_bitmap.btmp_bytes_len = ubm_length;
66
67 /***********內核記憶體池和用戶記憶體池點陣圖************
68 *內核的棧底是0xc009f00,減去4KB的PCB大小,便是0xc009e00
69 *這裡再分配4KB的空間用來存儲點陣圖,那麼點陣圖的起始地址便是
70 *0xc009a00,4KB的空間可以管理4*1024*8*4KB=512MB的物理記憶體
71 *這對於我們的系統來說已經綽綽有餘了。
72 */
73 /*內核記憶體池點陣圖地址*/
74 kernel_pool.pool_bitmap.bits = (void *)MEM_BIT_BASE; //MEM_BIT_BASE(0xc009a00)
75 /*用戶記憶體池點陣圖地址緊跟其後*/
76 user_pool.pool_bitmap.bits = (void *)(MEM_BIT_BASE + kbm_length);
77
78 /*輸出記憶體池信息*/
79 put_str("kernel_pool_bitmap_start:");
80 put_int((int)kernel_pool.pool_bitmap.bits);
81 put_str("\n");
82 put_str("kernel_pool.phy_addr_start:");
83 put_int(kernel_pool.phy_addr_start);
84 put_str("\n");
85
86 put_str("user_pool_bitmap_start:");
87 put_int((int)user_pool.pool_bitmap.bits);
88 put_str("\n");
89 put_str("user_pool.phy_addr_start:");
90 put_int(user_pool.phy_addr_start);
91 put_str(