目錄
一、前景回顧
二、進程的創建與初始化
三、如何進行進程的切換
四、運行測試
五、原書勘誤
在上一回我們大概講述了任務切換的發展,並且知道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 }
除此之外,還要將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 }
這一系列操作完成后,我們又回到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 }

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

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("\n"); 92
93 /*將位圖置0*/
94 bitmap_init(&kernel_pool.pool_bitmap); 95 bitmap_init(&user_pool.pool_bitmap); 96
97 /*初始化內核虛擬地址的位圖,按照實際物理內存大小生成數組*/
98 kernel_vaddr.vaddr_bitmap.btmp_bytes_len = kbm_length; 99 /*內核虛擬地址內存池位圖地址在用戶內存池位圖地址其后*/
100 kernel_vaddr.vaddr_bitmap.bits = (void *)(MEM_BIT_BASE + kbm_length + ubm_length); 101 /*內核虛擬地址內存池的地址以K_HEAP_START為起始地址*/
102 kernel_vaddr.vaddr_start = K_HEAP_START; 103 bitmap_init(&kernel_vaddr.vaddr_bitmap); 104
105 put_str("mem_pool_init done\n"); 106 } 107
108 /*內存管理部分初始化入口*/
109 void mem_init(void) 110 { 111 put_str("mem_init start\n"); 112 uint32_t mem_bytes_total = 33554432; //32MB內存 32*1024*1024=33554432
113 mem_pool_init(mem_bytes_total); 114 put_str("mem_init done\n"); 115 } 116
117
118 /*在pf表示的虛擬內存池中申請pg_cnt個虛擬頁 119 * 成功則返回虛擬地址的起始地址,失敗返回NULL 120 */
121 static void *vaddr_get(enum pool_flags pf, uint32_t pg_cnt) 122 { 123 int vaddr_start = 0; 124 int bit_idx_start = -1; 125 uint32_t cnt = 0; 126 if (pf == PF_KERNEL) { 127 bit_idx_start = bitmap_scan(&kernel_vaddr.vaddr_bitmap, pg_cnt); 128 if (bit_idx_start == -1) { 129 return NULL; 130 } 131 /*在位圖中將申請到的虛擬內存頁所對應的位給置1*/
132 while (cnt < pg_cnt) { 133 bitmap_set(&kernel_vaddr.vaddr_bitmap, bit_idx_start + cnt++, 1); 134 } 135 vaddr_start = kernel_vaddr.vaddr_start + bit_idx_start * PG_SIZE; 136
137 } else { //用戶內存池
138 struct task_struct *cur = running_thread(); 139 bit_idx_start = bitmap_scan(&cur->userprog_vaddr.vaddr_bitmap, pg_cnt); 140 if (bit_idx_start == -1) { 141 return NULL; 142 } 143 while (cnt < pg_cnt) { 144 bitmap_set(&cur->userprog_vaddr.vaddr_bitmap, bit_idx_start + cnt++, 1); 145 } 146 vaddr_start = cur->userprog_vaddr.vaddr_start + bit_idx_start * PG_SIZE; 147 /*0xc00000000 - PG_SIZE作為用戶3級棧已經在start_process被分配*/
148 ASSERT((uint32_t)vaddr_start < (0xc0000000 - PG_SIZE)); 149 } 150 return (void *)vaddr_start; 151 } 152
153 /*得到虛擬地址vaddr所對應的pte指針 154 * 這個指針也是一個虛擬地址,CPU通過這個虛擬地址去尋址會得到一個真實的物理地址 155 * 這個物理地址便是存放虛擬地址vaddr對應的普通物理頁的地址 156 * 假設我們已經知道虛擬地址vaddr對應的普通物理頁地址為0xa 157 * 那么便可以通過如下操作完成虛擬地址和普通物理頁地址的映射 158 * *pte = 0xa 159 */
160 uint32_t *pte_ptr(uint32_t vaddr) 161 { 162 uint32_t *pte = (uint32_t *)(0xffc00000 + \ 163 ((vaddr & 0xffc00000) >> 10) + \ 164 PTE_IDX(vaddr) * 4); 165 return pte; 166 } 167
168 /*得到虛擬地址vaddr所對應的pde指針 169 * 這個指針也是一個虛擬地址,CPU通過這個虛擬地址去尋址會得到一個真實的物理地址 170 * 這個物理地址便是存放虛擬地址vaddr對應的頁表的地址,使用方法同pte_ptr()一樣 171 */
172 uint32_t *pde_ptr(uint32_t vaddr) 173 { 174 uint32_t *pde = (uint32_t *)(0xfffff000 + PDE_IDX(vaddr) * 4); 175 return pde; 176 } 177
178 /*在m_pool指向的物理內存地址中分配一個物理頁 179 * 成功則返回頁框的物理地址,失敗返回NULL 180 */
181 static void *palloc(struct pool *m_pool) 182 { 183 int bit_idx = bitmap_scan(&m_pool->pool_bitmap, 1); 184 if (bit_idx == -1) { 185 return NULL; 186 } 187 /*在位圖中將申請到的物理內存頁所對應的位給置1*/
188 bitmap_set(&m_pool->pool_bitmap, bit_idx, 1); 189 /*得到申請的物理頁所在地址*/
190 uint32_t page_phyaddr = (m_pool->phy_addr_start + bit_idx * PG_SIZE); 191
192 return (void *)page_phyaddr; 193 } 194
195 /*在頁表中添加虛擬地址_vaddr與物理地址_page_phyaddr的映射*/
196 static void page_table_add(void *_vaddr, void *_page_phyaddr) 197 { 198 uint32_t vaddr = (uint32_t)_vaddr; 199 uint32_t page_phyaddr = (uint32_t)_page_phyaddr; 200 uint32_t *pde = pde_ptr(vaddr); 201 uint32_t *pte = pte_ptr(vaddr); 202
203 //先判斷虛擬地址對應的pde是否存在
204 if (*pde & 0x00000001) { 205 ASSERT(!(*pte & 0x00000001)); 206 *pte = (page_phyaddr | PG_US_U | PG_RW_W | PG_P_1); 207 } else { //頁目錄項不存在,需要先創建頁目錄再創建頁表項
208 uint32_t pde_phyaddr = (uint32_t)palloc(&kernel_pool); 209 *pde = (pde_phyaddr | PG_US_U | PG_RW_W | PG_P_1); 210 /* 將分配到的物理頁地址pde_phyaddr對應的物理內存清0 211 * 避免里面的陳舊數據變成頁表項 212 */
213 /* 這個地方不能這樣memset((void *)pde_phyaddr, 0, PG_SIZE); 214 * 因為現在我們所使用的所有地址都是虛擬地址,雖然我們知道pde_phyaddr是真實的物理地址 215 * 可是CPU是不知道的,CPU會把pde_phyaddr當作虛擬地址來使用,這樣就肯定無法清0了 216 * 所以解決問題的思路就是:如何得到pde_phyaddr所對應的虛擬地址。 217 */
218 //為什么不是memset((void *)((int)pde & 0xffc00000), 0, PG_SIZE); 219 //建議好好看看pde_ptr()和pte_ptr()函數的實現
220 memset((void *)((int)pte & 0xfffff000), 0, PG_SIZE); 221 ASSERT(!(*pte & 0x00000001)); 222 *pte = (page_phyaddr | PG_US_U | PG_RW_W | PG_P_1); 223 } 224 } 225
226 /*分配pg_cnt個頁空間,成功則返回起始虛擬地址,失敗返回NULL*/
227 void *malloc_page(enum pool_flags pf, uint32_t pg_cnt) 228 { 229 ASSERT((pg_cnt > 0) && (pg_cnt < 3840)); 230 void *vaddr_start = vaddr_get(pf, pg_cnt); 231 if (vaddr_start == NULL) { 232 return NULL; 233 } 234
235 uint32_t vaddr = (uint32_t)vaddr_start; 236 uint32_t cnt = pg_cnt; 237
238 struct pool *mem_pool = pf & PF_KERNEL ? &kernel_pool : &user_pool; 239
240 /*因為虛擬地址連續,而物理地址不一定連續,所以逐個做映射*/
241 while (cnt-- > 0) { 242 void *page_phyaddr = palloc(mem_pool); 243 if (page_phyaddr == NULL) { 244 return NULL; 245 } 246 page_table_add((void *)vaddr, page_phyaddr); 247 vaddr += PG_SIZE; 248 } 249 return vaddr_start; 250 } 251
252 /*從內核物理內存池中申請pg_cnt頁內存,成功返回其虛擬地址,失敗返回NULL*/
253 void *get_kernel_pages(uint32_t pg_cnt) 254 { 255 void *vaddr = malloc_page(PF_KERNEL, pg_cnt); 256 if (vaddr != NULL) { 257 memset(vaddr, 0, pg_cnt * PG_SIZE); 258 } 259 return vaddr; 260 } 261
262
263 /*在用戶空間中申請4K內存,並返回其虛擬地址*/
264 void *get_user_pages(uint32_t pg_cnt) 265 { 266 lock_acquire(&user_pool.lock); 267 void *vaddr = malloc_page(PF_USER, pg_cnt); 268 memset(vaddr, 0, pg_cnt * PG_SIZE); 269 lock_release(&user_pool.lock); 270 return vaddr; 271 } 272
273 /*將地址vaddr與pf池中的物理地址關聯起來,僅支持一頁內存空間分配*/
274 void *get_a_page(enum pool_flags pf, uint32_t vaddr) 275 { 276 struct pool *mem_pool = pf & PF_KERNEL ? &kernel_pool : &user_pool; 277 lock_acquire(&mem_pool->lock); 278
279 struct task_struct* cur = running_thread(); 280 int32_t bit_idx = -1; 281
282 //虛擬地址位圖置1
283 if (cur->pgdir != NULL && pf == PF_USER) { 284 bit_idx = (vaddr - cur->userprog_vaddr.vaddr_start) / PG_SIZE; 285 ASSERT(bit_idx > 0); 286 bitmap_set(&cur->userprog_vaddr.vaddr_bitmap, bit_idx, 1); 287 } else if(cur->pgdir == NULL && pf == PF_KERNEL) { 288 bit_idx = (vaddr - kernel_vaddr.vaddr_start) / PG_SIZE; 289 ASSERT(bit_idx > 0); 290 bitmap_set(&kernel_vaddr.vaddr_bitmap, bit_idx, 1); 291 } else { 292 PANIC("get_a_page:not allow kernel alloc userspace or user alloc kernelspace by get_a_page"); 293 } 294
295 void* page_phyaddr = palloc(mem_pool); 296 if (page_phyaddr == NULL) 297 return NULL; 298 page_table_add((void *)vaddr, page_phyaddr); 299 lock_release(&mem_pool->lock); 300 return (void *)vaddr; 301 } 302
303 /*得到虛擬地址映射的物理地址*/
304 uint32_t addr_v2p(uint32_t vaddr) 305 { 306 uint32_t *pte = pte_ptr(vaddr); 307 return ((*pte & 0xfffff000) + (vaddr & 0x00000fff)); 308 }

1 #ifndef __KERNEL_MEMORY_H 2 #define __KERNEL_MEMORY_H
3 #include "stdint.h"
4 #include "bitmap.h"
5
6 #define MEM_BIT_BASE 0xc009a000
7
8 /*虛擬地址池,用於虛擬地址管理*/
9 struct virtual_addr { 10 struct bitmap vaddr_bitmap; //虛擬地址用到的位圖結構
11 uint32_t vaddr_start; //虛擬地址起始地址
12 }; 13
14 /*內存池標記,用於判斷用哪個內存池*/
15 enum pool_flags { 16 PF_KERNEL = 1, 17 PF_USER = 2
18 }; 19
20 #define PG_P_1 1 //頁表項或頁目錄項存在屬性位,存在
21 #define PG_P_0 0 //頁表項或頁目錄項存在屬性位,不存在
22 #define PG_RW_R 0 //R/W屬性位值,不可讀/不可寫
23 #define PG_RW_W 2 //R/W屬性位值,可讀/可寫
24 #define PG_US_S 0 //U/S屬性位值,系統級
25 #define PG_US_U 4 //U/S屬性位值,用戶級
26
27 void mem_init(void); 28 void *get_kernel_pages(uint32_t pg_cnt); 29 void *get_a_page(enum pool_flags pf, uint32_t vaddr); 30 void *get_user_pages(uint32_t pg_cnt); 31 uint32_t addr_v2p(uint32_t vaddr); 32 void *get_a_page(enum pool_flags pf, uint32_t vaddr); 33
34 #endif

1 #include "thread.h"
2 #include "string.h"
3 #include "memory.h"
4 #include "list.h"
5 #include "interrupt.h"
6 #include "debug.h"
7 #include "print.h"
8 #include "stddef.h"
9 #include "process.h"
10
11 struct task_struct *main_thread; //主線程PCB
12 struct list thread_ready_list; //就緒隊列
13 struct list thread_all_list; //所有人物隊列
14 static struct list_elem *thread_tag; //用於保存隊列中的線程節點
15 extern void switch_to(struct task_struct* cur, struct task_struct* next); 16
17
18 /*獲取當前線程PCB指針*/
19 struct task_struct *running_thread(void) 20 { 21 uint32_t esp; 22 asm volatile ("mov %%esp, %0" : "=g" (esp)); 23
24 /*取esp整數部分,即PCB起始地址*/
25 return (struct task_struct *)(esp & 0xfffff000); 26 } 27
28 /*由kernel_thread去執行function(func_arg)*/
29 static void kernel_thread(thread_func *function, void *func_arg) 30 { 31 /*執行function前要開中斷,避免后面的時鍾中斷被屏蔽,而無法調度其他線程*/
32 intr_enable(); 33 function(func_arg); 34 } 35
36 /*初始化線程PCB*/
37 void init_thread(struct task_struct *pthread, char *name, int prio) 38 { 39 memset(pthread, 0, sizeof(*pthread)); 40 strcpy(pthread->name, name); 41
42 /*由於main函數也封裝成了一個線程,並且他是一直在運行的,所以將其直接設置為TASK_RUNNING*/
43 if (pthread == main_thread) { 44 pthread->status = TASK_RUNNING; 45 } else { 46 pthread->status = TASK_READY; 47 } 48 //pthread->status = TASK_RUNNING;
49 pthread->priority = prio; 50 pthread->ticks = prio; 51 pthread->elapsed_ticks = 0; 52 pthread->pgdir = NULL; 53 pthread->self_kstack = (uint32_t *)((uint32_t)pthread + PG_SIZE); 54 pthread->stack_magic = 0x19870916; 55 } 56
57 void thread_create(struct task_struct *pthread, thread_func function, void *func_arg) 58 { 59 pthread->self_kstack -= sizeof(struct intr_stack); 60 pthread->self_kstack -= sizeof(struct thread_stack); 61
62 //初始化線程棧
63 struct thread_stack *kthread_stack = (struct thread_stack *)pthread->self_kstack; 64 kthread_stack->eip = kernel_thread; 65 kthread_stack->function = function; 66 kthread_stack->func_arg = func_arg; 67 kthread_stack->ebp = kthread_stack->ebx = kthread_stack->edi = kthread_stack->esi = 0; 68 } 69
70 /*創建一個優先級為prio的線程,線程名字為name,線程所執行的函數為function(func_arg)*/
71 struct task_struct *thread_start(char *name, int prio, thread_func function, void *func_arg) 72 { 73 /*創建線程的pcb,大小為4kb*/
74 struct task_struct *thread = get_kernel_pages(1); 75 init_thread(thread, name, prio); 76 thread_create(thread, function, func_arg); 77
78 /*確保之前不在隊列中*/
79 ASSERT(!elem_find(&thread_ready_list, &thread->general_tag)); 80
81 /*加入就緒線程隊列*/
82 list_append(&thread_ready_list, &thread->general_tag); 83
84 /*確保之前不在隊列*/
85 ASSERT(!elem_find(&thread_all_list, &thread->all_list_tag)); 86
87 /*加入全部線程隊列*/
88 list_append(&thread_all_list, &thread->all_list_tag); 89
90 return thread; 91 } 92
93 static void make_main_thread(void) 94 { 95 main_thread = running_thread(); 96 init_thread(main_thread, "main", 31); 97
98 /*main函數是當前線程,當前線程不在thread_ready_list,所以只能將其加在thread_all_list*/
99 ASSERT(!elem_find(&thread_all_list, &main_thread->all_list_tag)); 100 list_append(&thread_all_list, &main_thread->all_list_tag); 101 } 102
103 /*實現任務調度*/
104 void schedule(void) 105 { 106 ASSERT(intr_get_status() == INTR_OFF); 107 struct task_struct *cur = running_thread(); 108 if (cur->status == TASK_RUNNING) { 109 ASSERT(!elem_find(&thread_ready_list, &cur->general_tag)); 110 list_append(&thread_ready_list, &cur->general_tag); 111 cur->ticks = cur->priority; 112 cur->status = TASK_READY; 113 } else { 114 /*阻塞等其他情況*/
115 } 116
117 ASSERT(!list_empty(&thread_ready_list)); 118 thread_tag = NULL; 119 thread_tag = list_pop(&thread_ready_list); 120
121 struct task_struct *next = elem2entry(struct task_struct, general_tag, thread_tag); 122 next->status = TASK_RUNNING; 123
124 process_activate(next); 125 switch_to(cur, next); 126 } 127
128 /*初始化線程環境*/
129 void thread_init(void) 130 { 131 put_str("thread_init start\n"); 132 list_init(&thread_ready_list); 133 list_init(&thread_all_list); 134 /*將當前main函數創建為線程*/
135 make_main_thread(); 136 put_str("thread_init done\n"); 137 } 138
139 /*當前線程將自己阻塞,標志其狀態為stat*/
140 void thread_block(enum task_status stat) 141 { 142 /*stat取值為TASK_BLOCKED、TASK_WAITING、TASK_HANGING 143 這三種狀態才不會被調度*/
144 ASSERT(((stat == TASK_BLOCKED) || (stat == TASK_WAITING) || (stat == TASK_HANGING))); 145 enum intr_status old_status = intr_disable(); 146 struct task_struct *cur_thread = running_thread(); 147 cur_thread->status = stat; 148 schedule(); 149 intr_set_status(old_status); 150 } 151
152 /*將線程thread解除阻塞*/
153 void thread_unblock(struct task_struct *thread) 154 { 155 enum intr_status old_status = intr_disable(); 156 ASSERT(((thread->status == TASK_BLOCKED) || (thread->status == TASK_WAITING) || (thread->status == TASK_HANGING))); 157 if (thread->status != TASK_READY) { 158 ASSERT(!elem_find(&thread_ready_list, &thread->general_tag)); 159 if (elem_find(&thread_ready_list, &thread->general_tag)) { 160 PANIC("thread_unblock: blocked thread in ready_list!\n"); 161 } 162 list_push(&thread_ready_list, &thread->general_tag); 163 thread->status = TASK_READY; 164 } 165 intr_set_status(old_status); 166 }

1 #ifndef __KERNEL_THREAD_H 2 #define __KERNEL_THREAD_H
3 #include "stdint.h"
4 #include "list.h"
5 #include "memory.h"
6
7 /*自定義通用函數類型,它將在很多線程函數中作為形參類型*/
8 typedef void thread_func (void *); 9 #define PG_SIZE 4096
10 /*進程或線程的狀態*/
11 enum task_status { 12 TASK_RUNNING, 13 TASK_READY, 14 TASK_BLOCKED, 15 TASK_WAITING, 16 TASK_HANGING, 17 TASK_DIED 18 }; 19
20 /****************中斷棧intr_stack****************/
21 struct intr_stack { 22 uint32_t vec_no; 23 uint32_t edi; 24 uint32_t esi; 25 uint32_t ebp; 26 uint32_t esp_dummy; 27 uint32_t ebx; 28 uint32_t edx; 29 uint32_t ecx; 30 uint32_t eax; 31 uint32_t gs; 32 uint32_t fs; 33 uint32_t es; 34 uint32_t ds; 35
36 /*以下由cpu從低特權級進入高特權級時壓入*/
37 uint32_t err_code; 38 void (*eip)(void); 39 uint32_t cs; 40 uint32_t eflags; 41 void *esp; 42 uint32_t ss; 43 }; 44
45 /***************線程棧thread_stack**********/
46 struct thread_stack 47 { 48 uint32_t ebp; 49 uint32_t ebx; 50 uint32_t edi; 51 uint32_t esi; 52
53 void (*eip) (thread_func *func, void *func_arg); 54 void (*unused_retaddr); 55 thread_func *function; 56 void *func_arg; 57 }; 58
59 /************進程或者線程的pcb,程序控制塊**********/
60 struct task_struct 61 { 62 uint32_t *self_kstack; //每個內核線程自己的內核棧
63 enum task_status status; 64 uint8_t priority; 65
66 char name[16]; 67 uint8_t ticks; //每次在處理器上執行的時間滴答數
68
69 /*此任務自從上CPU運行至今占用了多少滴答數,也就是這個任務執行了多久時間*/
70 uint32_t elapsed_ticks; 71
72 /*general_tag的作用是用於線程在一般的隊列中的節點*/
73 struct list_elem general_tag; 74
75 /*all_list_tag的作用是用於線程thread_all_list的節點*/
76 struct list_elem all_list_tag; 77
78 uint32_t *pgdir;//進程自己頁表的虛擬地址
79
80 struct virtual_addr userprog_vaddr; //用戶進程的虛擬地址池
81
82 uint32_t stack_magic; 83 }; 84
85 void schedule(void); 86 struct task_struct *running_thread(void); 87 static void kernel_thread(thread_func *function, void *func_arg); 88 void init_thread(struct task_struct *pthread, char *name, int prio); 89 void thread_create(struct task_struct *pthread, thread_func function, void *func_arg); 90 struct task_struct *thread_start(char *name, int prio, thread_func function, void *func_arg); 91 static void make_main_thread(void); 92 void thread_init(void); 93 void thread_block(enum task_status stat); 94 void thread_unblock(struct task_struct *thread); 95
96
97 #endif
修改main.c文件,本來用戶進程在執行前,是由操作系統的程序加載起將用戶程序從文件系統直接讀取到內存,再根據程序文件的格式解析其內容,將程序中的段展開到相應的內存地址。程序格式會記錄程序的入口地址,CPU把CS:[E]IP指向它,該程序就被執行了,C語言雖然不能直接控制這兩個寄存器,但是函數調用其實就是改變這兩個寄存器的指向,故C語言編寫的操作系統可以像調用函數那樣調用執行用戶程序。因此用戶進程被加載到內存中后如同函數一樣,僅僅是個指令區域,由於我們目前沒有實現文件系統,前期我們用普通函數來代替用戶程序,所以在main函數中我們新建了兩個名為u_prog_a和u_prog_b的兩個函數來作為進程執行的用戶程序。在這兩個程序中分別對test_var_a和test_var_b變量進行加1操作,由於用戶態下的字符串打印函數我們還沒實現,所以又新建兩個內核線程k_thread_a和k_thread_b來打印這兩個變量。

1 #include "print.h"
2 #include "debug.h"
3 #include "init.h"
4 #include "memory.h"
5 #include "thread.h"
6 #include "timer.h"
7 #include "list.h"
8 #include "interrupt.h"
9 #include "console.h"
10 #include "keyboard.h"
11 #include "ioqueue.h"
12 #include "process.h"
13
14 void k_thread_a(void *arg); 15 void k_thread_b(void *arg); 16 void u_prog_a(void); 17 void u_prog_b(void); 18 int test_var_a = 0, test_var_b = 0; 19 int main (void) 20 { 21 put_str("I am Kernel\n"); 22 init_all(); 23
24 thread_start("k_thread_a", 31, k_thread_a, "argA "); 25 thread_start("k_thread_b", 31, k_thread_b, "argB "); 26 process_execute(u_prog_a, "user_prog_a"); 27 process_execute(u_prog_b, "user_prog_b"); 28 intr_enable(); 29
30 while (1); 31 return 0; 32 } 33
34 void u_prog_a(void) 35 { 36 while(1) { 37 test_var_a = *(int *)(0xc0006480); 38 } 39 } 40
41 void u_prog_b(void) 42 { 43 while(1) { 44 test_var_b++; 45 } 46 } 47
48 void k_thread_a(void *arg) 49 { 50 char *para = arg; 51 while (1) { 52 console_put_str("v_a:0x"); 53 console_put_int(test_var_a); 54 console_put_str("\n"); 55 } 56 } 57
58 void k_thread_b(void *arg) 59 { 60 char *para = arg; 61 while (1) { 62 console_put_str("v_b:0x"); 63 console_put_int(test_var_b); 64 console_put_str("\n"); 65 } 66 }
運行測試,可以看到基本正常。
五、原書勘誤
這個地方我當初做到這里這一章節時,死活調不通。通過打斷點,可以看到進入進程后,中斷表有明顯的異常。
在進程中,中斷表的位置位於0x000063c0處,當然每個人的實際情況可能不太一樣。總之明顯不對,因為我們只給進程的頁目錄表映射了內核部分,很明顯這個地址是沒有被添加到頁表中的。所以一旦發生了中斷,CPU拿着這個中斷表的地址去找中斷描述符時就會報錯,因為頁表中沒有記錄這個位置的映射關系。
后面調試的時候發現其實是在實現中斷代碼那一章時,書上給的代碼有誤,原書第330頁,如下:
黃色部分的代碼是罪魁禍首,我測試了一下,在我的系統中idt被存放在虛擬地址0xc00063c0處,對應到物理地址就是0x000063c0處。經過上圖這種移位操作后,最終得到的地址變成了虛擬地址0x000063c0,可以發現高16位被舍掉了。在我們還沒有實現進程的時候,在內核線程的頁表中0x000063c0和0xc00063c0這兩個虛擬地址都是映射到0x000063c0這個物理地址的,所以我們前面並不會報錯。但是到了進程,在我們進程的頁表中,只有0xc00063c0這個虛擬地址映射到0x000063c0這個物理地址,而0x000063c0這個虛擬地址是沒有被添加映射關系的,所以才會一執行就報錯。所以將代碼修改成如下就好了:
uint64_t idt_operand = (sizeof(idt) - 1) | ((uint64_t)(uint32_t)idt << 16);
好了,本回合就到此結束了。這一章知識量還是比較多的,代碼也是很長的,我也是回味了很久。預知后事如何,請看下回分解。