《操作系统真象还原》第11章
引言——因为一个bug本篇文章难产,我一度以为整个项目要夭折了,但功夫不负有心人,还是诞生了它。
“
当加载新任务时,CPU自动把当前任务(旧任务)的状态存入当前任务的TSS,然后将新任务TSS中的数据载入到对应的寄存器中,这就实现了任务切换。TSS就是任务的代表,CPU用不同的TSS区分不同的任务,因此任务切换的本质就是TSS的换来换去。
结论:我们使用TSS唯一的理由就是为0特权级的任务提供栈。
CPU从3特权级的用户态进入0特权级的内核态时(比如从用户进程进入中断),CPU自动从当前任务的TSS获取SS0和esp0字段的值作为0特权级的栈,然后Linux“手动”执行一系列的push指令将任务的状态保存在0特权级栈中,也就是TSS中SS0和esp0所指向的栈。
”
步骤:
1.定义并初始化TSS
2.实现用户进程
1.定义并初始化TSS
重写kernel/global.h,顺便复习一下gdt和selector:
1 #ifndef __KERNEL_GLOBAL_H
2 #define __KERNEL_GLOBAL_H
3 #include "stdint.h"
4
5 #define RPL0 0
6 #define RPL1 1
7 #define RPL2 2
8 #define RPL3 3
9
10 #define TI_GDT 0
11 #define TI_LDT 1
12
13 //---------------- GDT描述符属性 ——————————————————
14 #define DESC_G_4K 1 // 段界限粒度为4K
15 #define DESC_D_32 1 // 有效地址及操作数是32位
16 #define DESC_L 0 // 保留位,32位编程下置0即可
17 #define DESC_AVL 0 // AVaiLable,OS随意用
18 #define DESC_P 1 // 存在位
19 #define DESC_DPL_0 0 // 段特权级
20 #define DESC_DPL_1 1
21 #define DESC_DPL_2 2
22 #define DESC_DPL_3 3
23
24 /*************************************************
25 * 代码段和数据段属于存储段,tss和各种门描述符属于系统段
26 * s为1时表示存储段,为0时表示系统段
27 ************************************************/
28 #define DESC_S_CODE 1
29 #define DESC_S_DATA DESC_S_CODE
30 #define DESC_S_SYS 0
31 #define DESC_TYPE_CODE 8
32 #define DESC_TYPE_DATA 2
33 #define DESC_TYPE_TSS 9
34
35 /* KERNEL段 */
36 #define SELECTOR_K_CODE ((1<<3)+(TI_GDT<<2)+RPL0)
37 #define SELECTOR_K_DATA ((2<<3)+(TI_GDT<<2)+RPL0)
38 #define SELECTOR_K_STACK SELECTOR_K_DATA
39 #define SELECTOR_K_GS ((3<<3)+(TI_GDT<<2)+RPL0)
40 /* 第3个段描述符是显存段,第4个是tss */
41 /* USER段 */
42 #define SELECTOR_U_CODE ((5<<3)+(TI_GDT<<2)+RPL3)
43 #define SELECTOR_U_DATA ((6<<3)+(TI_GDT<<2)+RPL3)
44 #define SELECOTR_U_STACK SELECTOR_U_DATA
45
46 #define GDT_ATTR_HIGH ((DESC_G_4K<<7)+(DESC_D_32<<6)+(DESC_L<<5)+(DESC_AVL<<4))
47 #define GDT_CODE_ATTR_LOW_DPL3 ((DESC_P<<7)+(DESC_DPL_3<<5)+(DESC_S_CODE<<4)+(DESC_TYPE_CODE))
48 #define GDT_DATA_ATTR_LOW_DPL3 ((DESC_P<<7)+(DESC_DPL_3<<5)+(DESC_S_DATA<<4)+(DESC_TYPE_DATA))
49
50 //--------------- IDT描述符属性 ------------------
51 #define IDT_DESC_P 1
52 #define IDT_DESC_DPL0 0
53 #define IDT_DESC_DPL3 3
54 #define IDT_DESC_32_TYPE 0xE
55 #define IDT_DESC_16_TYPE 0x6 // 16位的门,不用,定义它只为和32位门区分
56 #define IDT_DESC_ATTR_DPL0 ((IDT_DESC_P<<7)+(IDT_DESC_DPL0<<5)+IDT_DESC_32_TYPE)
57 #define IDT_DESC_ATTR_DPL3 ((IDT_DESC_P<<7)+(IDT_DESC_DPL3<<5)+IDT_DESC_32_TYPE)
58
59 //--------------- TSS描述符属性 ——————————————————
60 #define TSS_DESC_D 0
61 #define TSS_ATTR_HIGH ((DESC_G_4K<<7)+(TSS_DESC_D<<6)+(DESC_L<<5)+(DESC_AVL<<4)+0x0)
62 #define TSS_ATTR_LOW ((DESC_P<<7)+(DESC_DPL_0<<5)+(DESC_S_SYS<<4)+DESC_TYPE_TSS)
63
64 #define SELECTOR_TSS ((4<<3)+(TI_GDT<<2)+RPL0)
65
66 /* 描述符结构 */
67 struct gdt_desc{
68 uint16_t limit_low_word;
69 uint16_t base_low_word;
70 uint8_t base_mid_byte;
71 uint8_t attr_low_byte;
72 uint8_t limit_high_attr_high;
73 uint8_t base_high_byte;
74 };
75
76 #endif
新建一个文件夹userprog,将用户进程的代码文件都放在这下面。
写个userprog/tss.c:
1 #include "tss.h"
2 #include "global.h"
3 #include "thread.h"
4 #include "print.h"
5 #include "string.h"
6
7 struct tss{
8 uint32_t backlink;
9 uint32_t* esp0;
10 uint32_t ss0;
11 uint32_t esp1;
12 uint32_t ss1;
13 uint32_t esp2;
14 uint32_t ss2;
15 uint32_t cr3;
16 uint32_t (*eip)(void);
17 uint32_t eflags;
18 uint32_t eax;
19 uint32_t ecx;
20 uint32_t edx;
21 uint32_t ebx;
22 uint32_t esp;
23 uint32_t ebp;
24 uint32_t esi;
25 uint32_t edi;
26 uint32_t es;
27 uint32_t cs;
28 uint32_t ss;
29 uint32_t ds;
30 uint32_t fs;
31 uint32_t gs;
32 uint32_t ldt;
33 uint32_t trace;
34 uint32_t io_base;
35 };
36
37 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 /* 更新tss中esp0字段的值为pthread的0级栈 */
45 struct gdt_desc make_gdt_desc(uint32_t* desc_addr,uint32_t limit,uint8_t attr_low,uint8_t attr_high){
46 struct gdt_desc desc;
47 uint32_t desc_base=(uint32_t)desc_addr;
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 void tss_init(){
58 put_str("tss_init start\n");
59 uint32_t tss_size=sizeof(tss);
60 memset(&tss,0,tss_size);
61 tss.ss0=SELECTOR_K_STACK;
62 tss.io_base=tss_size;
63
64 /* gdt段基址为0x903,把tss放到第4个位置,也就是0x903+0x20的位置 */
65
66 /* 在gdt中添加dpl为0的TSS描述符 */
67 *((struct gdt_desc*)0xc0000923)=make_gdt_desc((uint32_t*)&tss,tss_size-1,TSS_ATTR_LOW,TSS_ATTR_HIGH);
68 *((struct gdt_desc*)0xc000092b)=make_gdt_desc((uint32_t*)0,0xfffff,GDT_CODE_ATTR_LOW_DPL3,GDT_ATTR_HIGH);
69 *((struct gdt_desc*)0xc0000933)=make_gdt_desc((uint32_t*)0,0xfffff,GDT_DATA_ATTR_LOW_DPL3,GDT_ATTR_HIGH);
70
71 /* gdt 16位的limit 32位的段基址 */
72 uint64_t gdt_operand=((8*7-1) | ((uint64_t)(uint32_t)0xc0000903<<16)); // 7个描述符大小
73 asm volatile("lgdt %0"::"m"(gdt_operand));
74 asm volatile("ltr %w0"::"r"(SELECTOR_TSS));
75 put_str("tss_init and ltr done\n");
76 }
67~72行和书中有些不同,还记得我们之前的gdt_base在哪儿吗:
和书中的base=0xc0000900不同,因为之前我们把gdt_ptr修改过了对吗。
及其头文件userprog/tss.h:
1 #ifndef __USERPROG_TSS_H
2 #define __USERPROG_TSS_H
3 #include "thread.h"
4
5 void update_tss_esp(struct task_struct* pthread);
6 struct gdt_desc make_gdt_desc(uint32_t* desc_addr,uint32_t limit,uint8_t attr_low,uint8_t attr_high);
7 void tss_init(void);
8
9 #endif
第8行(包括.c文件)不要照书中的使用static定义,否则会warning。
记得修改makefile。
编译运行后可能出现下列warning:
这是因为static函数不能被其它.c文件include,所以要把.h中的声明放进ioqueue.c文件中。
另外,在kernel/init.c中增加一句,很重要:
tss_init();
然后就会:
TSS加载成功。
2.实现用户进程
修改thread/thread.h,增加头文件并在task_struct结构体中增加一个属性:
#include "bitmap.h"
#include "memory.h"
struct virtual_addr userprog_vaddr; // 用户进程的虚拟地址
修改kernel/memory.c为:

1 #include "memory.h"
2 #include "bitmap.h"
3 #include "stdint.h"
4 #include "global.h"
5 #include "debug.h"
6 #include "print.h"
7 #include "string.h"
8 #include "sync.h"
9
10 /************** 位图地址 *****************/
11 #define MEM_BITMAP_BASE 0xc009a000
12
13 /* 0xc0000000是内核从虚拟地址3G起,0x100000意指跨过低端1MB内存,使虚拟地址在逻辑上连续 */
14 #define K_HEAP_START 0xc0100000
15
16 #define PDE_IDX(addr) ((addr & 0xffc00000)>>22) // 得到PDX
17 #define PTE_IDX(addr) ((addr & 0x003ff000)>>12) // 得到PTX
18
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 /* 在pf表示的虚拟内存池中申请pg_cnt个虚拟页
31 * 成功则返回虚拟页的起始地址,失败则返回NULL */
32 static void* vaddr_get(enum pool_flags pf,uint32_t pg_cnt){
33 int vaddr_start=0,bit_idx_start=-1;
34 uint32_t cnt=0;
35 if (pf==PF_KERNEL){ // 内核内存池
36 bit_idx_start=bitmap_scan(&kernel_vaddr.vaddr_bitmap,pg_cnt);
37 if (bit_idx_start==-1){
38 return NULL;
39 }
40 while (cnt<pg_cnt){
41 bitmap_set(&kernel_vaddr.vaddr_bitmap,bit_idx_start+cnt++,1);
42 }
43 vaddr_start=kernel_vaddr.vaddr_start+bit_idx_start*PG_SIZE;
44 }else{ // 用户内存池
45 struct task_struct* cur=running_thread();
46 bit_idx_start=bitmap_scan(&cur->userprog_vaddr.vaddr_bitmap,pg_cnt);
47 if (bit_idx_start==-1){
48 return NULL;
49 }
50 while (cnt<pg_cnt){
51 bitmap_set(&cur->userprog_vaddr.vaddr_bitmap,bit_idx_start+cnt++,1);
52 }
53 vaddr_start=cur->userprog_vaddr.vaddr_start+bit_idx_start*PG_SIZE;
54
55 ASSERT((uint32_t)vaddr_start<(0xc0000000-PG_SIZE));
56 }
57 return (void*)vaddr_start;
58 }
59
60 /* 得到虚拟地址vaddr对应的pte指针 */
61 uint32_t* pte_ptr(uint32_t vaddr){
62 /* 先访问到页表自己 +
63 * 再用页目录项pde作为pte的索引访问到页表 +
64 * 再用页表项pte作为页内偏移 */
65 uint32_t* pte=(uint32_t*)(0xffc00000+\
66 ((vaddr & 0xffc00000)>>10)+\
67 PTE_IDX(vaddr)*4);
68 return pte;
69 }
70
71 /* 得到虚拟地址vaddr对应的pde指针 */
72 uint32_t* pde_ptr(uint32_t vaddr){
73 /* 0xfffff用来访问到页表本身所在的地址 */
74 uint32_t* pde=(uint32_t*)((0xfffff000)+PDE_IDX(vaddr)*4);
75 return pde;
76 }
77
78 /* 在m_pool指向的物理内存池中分配1个物理页,
79 * 成功则返回页框的物理地址,失败则返回NULL */
80 static void* palloc(struct pool* m_pool){
81 /* 扫描或设置位图要保证原子操作 */
82 int bit_idx=bitmap_scan(&m_pool->pool_bitmap,1); // 找一个物理页面
83 if (bit_idx==-1){
84 return NULL;
85 }
86 bitmap_set(&m_pool->pool_bitmap,bit_idx,1); // 将此位bit_idx置为1
87 uint32_t page_phyaddr=((bit_idx*PG_SIZE)+m_pool->phy_addr_start);
88 return (void*)page_phyaddr;
89 }
90
91 /* 页表中添加虚拟地址_vaddr与物理地址_page_phyaddr的映射 */
92 static void page_table_add(void* _vaddr,void* _page_phyaddr){
93 uint32_t vaddr=(uint32_t)_vaddr,page_phyaddr=(uint32_t)_page_phyaddr;
94 uint32_t* pde=pde_ptr(vaddr);
95 uint32_t* pte=pte_ptr(vaddr);
96
97 /***************************** 注意 ****************************
98 * 执行*pte,会访问到空的pde。所以确保pde创建完成后才嫩执行*pte,
99 * 否则会引发page_fault。因此在*pde为0时,pte只能在下面else语句块的*pde后面。
100 **************************************************************/
101 /* 先在页目录内判断目录项的P位,若为1,则表示该表已存在 */
102 if (*pde & 0x00000001){ //P位,此处判断目录项是否存在。若存在
103 ASSERT(!(*pte & 0x00000001));
104
105 if(!(*pte & 0x00000001)){
106 *pte=(page_phyaddr | PG_US_U | PG_RW_W | PG_P_1);
107 }else{ // 理论上不会执行到这里
108 PANIC("pte repeat");
109 *pte=(page_phyaddr | PG_US_U | PG_RW_W | PG_P_1);
110 }
111 }else{ // 页目录项不存在,所以要先创建页目录项再创建页表项
112 /* 页表中用到的页框一律从内核空间分配 */
113 uint32_t pde_phyaddr=(uint32_t)palloc(&kernel_pool);
114
115 *pde=(pde_phyaddr | PG_US_U | PG_RW_W | PG_P_1);
116
117 /* 分配到的物理页地址pde_phyaddr对应的物理内存清0,
118 * 避免里面的陈旧数据变成了页表项,从而让页表混乱。
119 * 访问到pde对应的物理地址,用pte取高20位即可。
120 * 因为pte基于该pde对应的物理地址内再寻址,
121 * 把低12位置0便是该pde对应的物理页的起始 */
122 memset((void*)((int)pte & 0xfffff000),0,PG_SIZE);
123
124 ASSERT(!(*pte & 0x00000001));
125 *pte=(page_phyaddr | PG_US_U | PG_RW_W | PG_P_1);
126 }
127 }
128
129 /* 分配pg_cnt个页空间,成功则返回起始虚拟地址,失败则返回NULL */
130 void* malloc_page(enum pool_flags pf,uint32_t pg_cnt){
131 ASSERT(pg_cnt>0 && pg_cnt<3840);
132 /************ malloc_page 的原理是三个动作的合成:**********
133 * 1.通过vaddr_get在虚拟地址内存池中的申请虚拟地址
134 * 2.通过palloc在物理内存池中申请物理页
135 * 3.通过page_table_add将以上得到的虚拟地址和物理地址在页表中完成映射
136 **********************************************************/
137 void* vaddr_start=vaddr_get(pf,pg_cnt);
138 if (vaddr_start==NULL){
139 return NULL;
140 }
141
142 uint32_t vaddr=(uint32_t)vaddr_start,cnt=pg_cnt;
143 struct pool* mem_pool=(pf & PF_KERNEL)?&kernel_pool:&user_pool;
144
145 /* 因为虚拟地址是连续的,但物理地址可以是不连续的,所以逐个做映射 */
146 while (cnt-->0){
147 void* page_phyaddr=palloc(mem_pool);
148 if (page_phyaddr==NULL){ // 申请物理内存失败,将已申请的虚拟地址和物理页全部回滚,在将来完成内存回收时再补充
149 return NULL;
150 }
151 page_table_add((void*)vaddr,page_phyaddr); // 在页表中做映射
152 vaddr+=PG_SIZE; // 下一个虚拟页
153 }
154 return vaddr_start;
155 }
156
157 /* 在用户空间中申请4k内存,并返回其虚拟地址 */
158 void* get_user_pages(uint32_t pg_cnt){
159 lock_acquire(&user_pool.lock);
160 void* vaddr=malloc_page(PF_USER,pg_cnt);
161 memset(vaddr,0,pg_cnt*PG_SIZE);
162 lock_release(&user_pool.lock);
163 return vaddr;
164 }
165
166 /* 从内核物理内存池中申请1页内存,成功则返回其虚拟地址,失败则返回NULL */
167 void* get_kernel_pages(uint32_t pg_cnt){
168 void* vaddr=malloc_page(PF_KERNEL,pg_cnt);
169 if (vaddr!=NULL){ // 若分配的地址不为空,将页框清0
170 memset(vaddr,0,pg_cnt*PG_SIZE);
171 }
172 return vaddr;
173 }
174
175 /* 将地址vaddr与pf池中的物理地址关联,仅支持一页空间分配 */
176 void* get_a_page(enum pool_flags pf,uint32_t vaddr){
177 struct pool* mem_pool=pf & PF_KERNEL?&kernel_pool:&user_pool;
178 lock_acquire(&mem_pool->lock);
179
180 /* 先将虚拟地址对应的位图置1 */
181 struct task_struct* cur=running_thread();
182 int32_t bit_idx=-1;
183
184 /* 若当前是用户进程申请用户内存,就修改用户进程自己的虚拟地址位图 */
185 if (cur->pgdir!=NULL && pf==PF_USER){
186 bit_idx=(vaddr-cur->userprog_vaddr.vaddr_start)/PG_SIZE;
187 ASSERT(bit_idx>0);
188 bitmap_set(&cur->userprog_vaddr.vaddr_bitmap,bit_idx,1);
189
190 }else if (cur->pgdir==NULL && pf==PF_KERNEL){
191 /* 如果是内核线程申请内核内存,就修改kernel_vaddr. */
192 bit_idx=(vaddr-kernel_vaddr.vaddr_start)/PG_SIZE;
193 ASSERT(bit_idx>0);
194 bitmap_set(&kernel_vaddr.vaddr_bitmap,bit_idx,1);
195 }else {
196 PANIC("get_a_page:not allow kernel alloc userspace or user alloc kernelspace by get_a_page");
197 }
198
199 void* page_phyaddr=palloc(mem_pool);
200 if (page_phyaddr==NULL){
201 return NULL;
202 }
203 page_table_add((void*)vaddr,page_phyaddr);
204 lock_release(&mem_pool->lock);
205 return (void*)vaddr;
206 }
207
208 uint32_t addr_v2p(uint32_t vaddr){
209 uint32_t* pte=pte_ptr(vaddr);
210 /* (*pte)的值是页表所在的物理页框地址
211 * 去掉其低12位的页表项属性+虚拟地址vaddr的低12位 */
212 return ((*pte & 0xfffff000)+(vaddr & 0x00000fff));
213 }
214
215 /* 初始化内存池 */
216 static void mem_pool_init(uint32_t all_mem){
217 put_str(" mem_pool_init start\n");
218 uint32_t page_table_size=PG_SIZE*256;
219
220 uint32_t used_mem=page_table_size+0x100000;
221 uint32_t free_mem=all_mem-used_mem;
222 uint16_t all_free_pages=free_mem/PG_SIZE;
223
224 uint16_t kernel_free_pages=all_free_pages/2;
225 uint16_t user_free_pages=all_free_pages-kernel_free_pages;
226
227 /* 为简化位图操作,余数不处理,不用考虑越界检查,但会丢失部分内存 */
228 uint32_t kbm_length=kernel_free_pages/8; // kernel bitmap的长度。1位管理1页,单位为字节
229 uint32_t ubm_length=user_free_pages/8; // user bitmap的长度
230
231 uint32_t kp_start=used_mem; // KernelLPool,内核内存池的起始地址
232 uint32_t up_start=kp_start+kernel_free_pages*PG_SIZE; // UserPool,用户内存池的起始地址
233
234 kernel_pool.phy_addr_start=kp_start;
235 user_pool.phy_addr_start=up_start;
236
237 kernel_pool.pool_size=kernel_free_pages*PG_SIZE;
238 user_pool.pool_size=user_free_pages*PG_SIZE;
239
240 kernel_pool.pool_bitmap.btmp_bytes_len=kbm_length;
241 user_pool.pool_bitmap.btmp_bytes_len=ubm_length;
242
243
244 /************** 内核内存池和用户内存池位图 ***************
245 * 位图是全局的数据,长度不固定。
246 * 全局或静态的数组需要在编译时知道其长度,
247 * 而我们需要根据总内存大小算出需要多少字节,
248 * 所以改为指定一块内存来生成位图。
249 * ******************************************************/
250 // 内核使用的最高地址是0xc009f000,这是主线程的站地址
251 // 32MB内存占用的位图是2KB,内核内存池位图先定在MEM_BITMAP_BASE(0xc009a000)处
252 kernel_pool.pool_bitmap.bits=(void*)MEM_BITMAP_BASE;
253
254 /* 用户内存池的位图紧跟在内核内存池位图之后 */
255 user_pool.pool_bitmap.bits=(void*)(MEM_BITMAP_BASE+kbm_length);
256
257 /***************** 输出内存池信息 ***********************/
258 put_str(" kernel_pool_bitmap_start:");
259 put_int((int)kernel_pool.pool_bitmap.bits);
260 put_str(" kernel_pool_phy_addr_start:");
261 put_int((int)kernel_pool.phy_addr_start);
262 put_str("\n");
263 put_str(" user_pool_bitmap_start:");
264 put_int((int)user_pool.pool_bitmap.bits);
265 put_str(" user_pool_phy_addr_strar:");
266 put_int((int)user_pool.phy_addr_start);
267 put_str("\n");
268
269 /*将位图置0*/
270 bitmap_init(&kernel_pool.pool_bitmap);
271 bitmap_init(&user_pool.pool_bitmap);
272
273 lock_init(&kernel_pool.lock);
274 lock_init(&user_pool.lock);
275
276 /* 下面初始化内核虚拟地址位图,按实际物理内存大小生成数组 */
277 kernel_vaddr.vaddr_bitmap.btmp_bytes_len=kbm_length;
278 // 用于维护内核堆的虚拟地址,所以要和内核内存池大小一致
279
280 /* 位图的数组指向一块未使用的内存,目前定位在内核内存池和用户内存池之外 */
281 kernel_vaddr.vaddr_bitmap.bits=(void*)(MEM_BITMAP_BASE+kbm_length+ubm_length);
282
283 kernel_vaddr.vaddr_start=K_HEAP_START;
284 bitmap_init(&kernel_vaddr.vaddr_bitmap);
285 put_str(" mem_pool_init done\n");
286 }
287
288 /* 内存管理部分初始化入口 */
289 void mem_init(){
290 put_str("mem_init start\n");
291 uint32_t mem_bytes_total=(*(uint32_t*)(0xb00));
292 mem_pool_init(mem_bytes_total); // 初始化内存池
293 put_str("mem_init done\n");
294 }
记得再修改它的头文件。
在kernel/global.h的末尾添加:
1 //-------------- EFLAGS寄存器属性位 ---------------
2 #define EFLAGS_MBS (1<<1)
3 #define EFLAGS_IF_1 (1<<9) // 开中断
4 #define EFLAGS_IF_0 0 // 关中断
5 #define EFLAGS_IOPL_3 (3<<12) // 用于测试用户程序在非系统调用下进行IO
6 #define EFLAGS_IOPL_0 (0<<12)
7
8 #define NULL ((void*)0)
9 #define DIV_ROUND_UP(X,STEP) ((X+STEP-1)/STEP)
10 #define bool int
11 #define true 1
12 #define false 0
13
14 #define PG_SIZE 4096
15
16 #define default_prio 31
17 #define USER_STACK3_VADDR (0xc0000000-0x1000)
编写userprog/process.c:
1 #include "process.h"
2 #include "global.h"
3 #include "debug.h"
4 #include "memory.h"
5 #include "../thread/thread.h"
6 #include "list.h"
7 #include "tss.h"
8 #include "interrupt.h"
9 #include "string.h"
10 #include "console.h"
11
12 extern void intr_exit(void);
13
14 /* 构建用户进程初始上下文信息 */
15 void start_process(void* filename_){
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; // 用户态用不上,直接初始为0
23 proc_stack->ds=proc_stack->es=proc_stack->fs=SELECTOR_U_DATA;
24 proc_stack->eip=function; // 待执行的用户程序地址
25 proc_stack->cs=SELECTOR_U_CODE;
26 proc_stack->eflags=(EFLAGS_IOPL_0 | EFLAGS_MBS | EFLAGS_IF_1);
27 proc_stack->esp=(void*)((uint32_t)get_a_page(PF_USER,USER_STACK3_VADDR)+PG_SIZE);
28 proc_stack->ss=SELECTOR_U_DATA;
29 asm volatile ("movl %0,%%esp;jmp intr_exit"::"g"(proc_stack):"memory");
30 }
31
32 /* 激活页表 */
33 void page_dir_activate(struct task_struct* p_thread){
34 /********************************************************
35 * 执行此函数时,当前任务可能是线程。
36 * 之所以对线程也要重新安装页表, 原因是上一次被调度的可能是进程,
37 * 否则不恢复页表的话,线程就会使用进程的页表了。
38 ********************************************************/
39
40 /* 若为内核线程,需要重新填充页表为0x100000 */
41 uint32_t pagedir_phy_addr=0x100000; // 默认为内核的页目录物理地址,也就是内核线程所用的页目录表
42 if (p_thread->pgdir!=NULL){ // 用户态进程有自己的页目录表
43 pagedir_phy_addr=addr_v2p((uint32_t)p_thread->pgdir);
44 }
45
46 /* 更新页目录寄存器cr3,使新页表生效 */
47 asm volatile("movl %0,%%cr3"::"r"(pagedir_phy_addr):"memory");
48 }
49
50 /* 击活线程或进程的页表,更新tss中的esp0为进程的特权级0的栈 */
51 void process_activate(struct task_struct* p_thread){
52 ASSERT(p_thread!=NULL);
53 /* 击活该进程或线程的页表 */
54 page_dir_activate(p_thread);
55
56 /* 内核线程特权级本身就是0,处理器进入中断时并不会从tss中获取0特权级栈地址,故不需要更新esp0 */
57 if (p_thread->pgdir){
58 /* 更新该进程的esp0,用于此进程被中断时保留上下文 */
59 update_tss_esp(p_thread);
60 }
61 }
62
63 /* 创建页目录表,将当前页表的表示内核空间的pde复制,
64 * 成功则返回页目录的虚拟地址,否则返回-1 */
65 uint32_t* create_page_dir(void){
66
67 /* 用户进程的页表不能让用户直接访问到,所以在内核空间来申请 */
68 uint32_t* page_dir_vaddr=get_kernel_pages(1);
69 if (page_dir_vaddr==NULL){
70 console_put_str("create_page_dir: get_kernel_page failed!");
71 return NULL;
72 }
73
74 /************************** 1 先复制页表 *************************************/
75 /* page_dir_vaddr + 0x300*4 是内核页目录的第768项 */
76 memcpy((uint32_t*)((uint32_t)page_dir_vaddr+0x300*4),(uint32_t*)(0xfffff000+0x300*4),1024);
77 /*****************************************************************************/
78
79 /************************** 2 更新页目录地址 **********************************/
80 uint32_t new_page_dir_phy_addr=addr_v2p((uint32_t)page_dir_vaddr);
81 /* 页目录地址是存入在页目录的最后一项,更新页目录地址为新页目录的物理地址 */
82 page_dir_vaddr[1023]=new_page_dir_phy_addr | PG_US_U | PG_RW_W | PG_P_1;
83 /*****************************************************************************/
84 return page_dir_vaddr;
85 }
86
87 /* 创建用户进程虚拟地址位图 */
88 void create_user_vaddr_bitmap(struct task_struct* user_prog){
89 user_prog->userprog_vaddr.vaddr_start=USER_VADDR_START;
90 uint32_t bitmap_pg_cnt=DIV_ROUND_UP((0xc0000000-USER_VADDR_START)/PG_SIZE/8,PG_SIZE);
91 user_prog->userprog_vaddr.vaddr_bitmap.bits=get_kernel_pages(bitmap_pg_cnt);
92 user_prog->userprog_vaddr.vaddr_bitmap.btmp_bytes_len=(0xc0000000-USER_VADDR_START)/PG_SIZE/8;
93 bitmap_init(&user_prog->userprog_vaddr.vaddr_bitmap);
94 }
95
96 /* 创建用户进程 */
97 void process_execute(void* filename,char* name){
98 /* pcb内核的数据结构,由内核来维护进程信息,因此要在内核内存池中申请 */
99 struct task_struct* thread=get_kernel_pages(1);
100 init_thread(thread,name,default_prio);
101 create_user_vaddr_bitmap(thread);
102 thread_create(thread,start_process,filename);
103 thread->pgdir=create_page_dir();
104
105 enum intr_status old_status=intr_disable();
106 ASSERT(!elem_find(&thread_ready_list,&thread->general_tag));
107 list_append(&thread_ready_list,&thread->general_tag);
108
109 ASSERT(!elem_find(&thread_all_list,&thread->all_list_tag));
110 list_append(&thread_all_list,&thread->all_list_tag);
111 intr_set_status(old_status);
112 }
编写userprog/process.h:
1 #ifndef __USERPROG_PROCESS_H
2 #define __USERPROG_PROCESS_H
3 #include "../thread/thread.h"
4 #include "stdint.h"
5 #define default_prio 31
6 #define USER_STACK3_VADDR (0xc0000000-0x1000)
7 #define USER_VADDR_START 0x8048000
8 void process_execute(void* filename, char* name);
9 void start_process(void* filename_);
10 void process_activate(struct task_struct* p_thread);
11 void page_dir_activate(struct task_struct* p_thread);
12 uint32_t* create_page_dir(void);
13 void create_user_vaddr_bitmap(struct task_struct* user_prog);
14 #endif
修改thread/thread.c,加头文件并在schedule()内加一句代码:
#include "process.h"
/* 激活任务页表等 */
process_activate(next);
最后是kernel/main.c:

1 #include "print.h"
2 #include "init.h"
3 #include "thread.h"
4 #include "interrupt.h"
5 #include "console.h"
6 #include "ioqueue.h"
7 #include "process.h"
8
9 void k_thread_a(void*);
10 void k_thread_b(void*);
11 void u_prog_a(void);
12 void u_prog_b(void);
13 int test_var_a=0,test_var_b=0;
14
15 int main(void){
16 put_str("Welcome,\nI am kernel!\n");
17 init_all();
18
19 thread_start("k_thread_a",31,k_thread_a,"argA ");
20 thread_start("k_thread_b",31,k_thread_b,"argB ");
21 process_execute(u_prog_a,"user_prog_a");
22 process_execute(u_prog_b,"user_prog_b");
23
24 intr_enable();
25 while(1);
26 return 0;
27 }
28
29 /* 在线程中运行的函数 */
30 void k_thread_a(void* arg){
31 while(1){
32 console_put_str(" v_a:0x");
33 console_put_int(test_var_a);
34 }
35 }
36
37 /* 在线程中运行的函数 */
38 void k_thread_b(void* arg){
39 while(1){
40 console_put_str(" v_b:0x");
41 console_put_int(test_var_b);
42 }
43 }
44
45 /* 测试用户进程 */
46 void u_prog_a(void){
47 while(1){
48 test_var_a++;
49 }
50 }
51
52 /* 测试用户进程 */
53 void u_prog_b(void){
54 while(1){
55 test_var_b++;
56 }
57 }
即可得到结果:
额,好像输出的排版不是很好看,但问题不大。
回顾完线程与锁后紧接着来回顾进程(顺带后面内存管理的完善)。
1.定义并初始化TSS——tss_init():
(1)因为Linux只分0和3两种等级,所有我们只会初始ss0和esp0:ss0初始化为内核栈段的选择子,esp0为内核栈栈顶。
(2)在GDT中添加dpl=0的TSS描述符和dpl=3的代码段描述符、数据段描述符。
(3)因为修改了GDT,所以要重新lgdt一下。并且ltr,加载TSS选择子。
2.修改完善内存管理系统:
(1)先说说块描述符block_desc和arena。一个arena占4KB,里面以block为单位存储数据,空闲块用list相连,而struct arena则是arena的元信息,记录块描述符、空闲块的数量等信息,位于arena的头部;而block_desc则用来描述arena中的这些块,包括块尺寸、块数量等。arena与block_desc一一对应。需要注意的是,同一arena内的block的size相同,而不同arena内是不同的,size从16B起以*2增长直至1KB,从而适应不同的内存需求。
(2)实现获取arena中第idx个内存块的地址——arena2block(),相反地还有获取内存块所在的arena地址——block2arena(),都非常简单,一句话的事儿。
(3)然后是非常重要的,实现在堆中申请size字节的内存——sys_malloc():
①判断申请内核内存or用户内存。
②若申请的内存size大于1KB,则直接申请若干个页框吧——malloc_page();若申请的内存size小于1KB,则可以在各种规格的mem_block_desc中适配。但是有可能找到的mem_block_desc显示已经没有空闲的block了,那就只好手动分配1页页框作arena——malloc_page(1)。因为多了一个arena,所以要额外初始化它:清0,将它与一个mem_block_desc关联起来,并拆开成block,将block用list相连。然后才可以分配之。用(2)中的函数获得block的地址并返回,再将arena的空闲块数量-1即可。
(4)当然,有了申请内存,还要有释放内存才行——sys_free():
①判断释放内核内存or用户内存。
②若释放的内存size大于1KB,调用mfree_page()——释放物理页框pfree(),清除页表项,内存池位图中的相应位清0。若size小于1KB,则将释放出来的block连接到list中。
好吧,我承认,memory.c中的函数是在太多太杂了,于是我整理了一下:
3.实现用户进程:
(1)创建用户进程——process_execute():
①由内核来维护进程信息,因此初始化一个内核线程——init_thread()——其中主要就是初始化PCB。
②创建用户进程虚拟地址位图——create_user_vaddr_bitmap()。
③创建线程——thread_create()——其中主要就是初始化线程栈。
④初始化pgdir——create_page_dir(),为用户进程开辟一个页目录(终于要用上PCB中的pgdir了)。
⑤内存块描述符数组初始化——block_desc_init()。(实现于第12章)
⑥将进程加入就绪队列和全队列。
(2)执行进程——start_process():
thread_create()把start_process()和用户程序的函数指针传了进去,也就是准备执行start_process(),其参数为用户程序地址。
①在start_process()中初始化用户进程的上下文信息(中断栈):段寄存器=相应的用户段选择子; eip=待执行的用户程序地址;此外,专门在用户空间的最后一页(3GB-4KB)处开辟了一页的空间,将esp指向页框顶部,作为用户栈空间。此时,我们可以初见线程与进程的一点区别:进程有自己的一页内存。
②通过内联汇编将中断栈中的数据载入CPU的寄存器,使程序“假装”退出中断,进入特权级3,执行用户进程的代码。
(3)额外的,在schedule()中,无论当前是进程or线程,我们都会执行process_activate(),激活任务页表。所谓激活任务页表,就是将CR3寄存器中保存的页表地址替换为当前进程或线程的页表。还记得吗,页目录表的起始地址是记录在CR3中的。注意,进程的页表是指进程自己独立的地址空间;而所有线程与内核共享地址空间,也就是和内核共用同一套页表,线程的页表指的是这个页表。
参考博客:
- 《操作系统真象还原》第十一章 ---- 实现用户进程 欺骗CPU通彻进程原理 眺望终点到达还需砥砺前行_0xc0000603_Love 6的博客-CSDN博客
- 操作系统真象还原实验记录之实验十九:实现用户进程_mxy990811的博客-CSDN博客
- OS_lab/code/c11/b at master · fuujiro/OS_lab (github.com)
结束语:
本篇文章距离上一篇文章完结已有相当时间,原因在于最后一步调试了太久,给大家看看:
是的,这个问题困扰了我一天,周六我花了整整一天时间在定位这个问题,我以为问题应该出在了新写的tss和process程序,于是盯着它两看,仔细比对哪一句和标准程序不同,结果硬是找不出毛病。于是我开始怀疑是内存分布的锅,会不会是之前的0xc0000903和某句代码冲突了,于是我又copy了mbr.S和loader.S,重新编译运行,还是不行,说实话那时的我有点心态崩了。。。
于是周日我在寝室休息了一天,回顾了一下之前所写的代码以及整个程序运行的过程。
周一,也就是今天下午,我狠下心来把所有的程序都换成了标准程序,终于跑通了。我又喜又忧,喜的是整个项目是可以进行下去的,至少编译器和虚拟机没问题;忧的是我又要把程序一个一个的还原回去,找出有问题的程序。三个小时后,我发现原来是interrupt.c的一句代码出锅了:
看,那个idt的括号我多打了一对(明明书中如此,真的被害惨了),哎,但我哪里想得到是idt不正确呢,因为如果将它打印出来,你会发现值是一样的,我晕了。。。在你看到这句话的时候,我已经默默把之前有关中断的博客中代码改正确了。而且我也很奇怪,在其它博客的这句代码都是正确的,难道他们都不是按照书上写的代码来的吗,并且也没看到有博客提出这个错误。
由于我把整个代码都重过了一边,所以我的很多程序的结构都略有变化,如果大家在make all后有诸多warning和error,还请耐心调试,这也算是一路上的风景吧。
好了,快快结束本章吧,接下来的产出进度会稍稍变慢,还要备考周末的CSP考试呢,毕竟“我必须考虑这会不会是我此生仅有的机会”。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· [.NET]调用本地 Deepseek 模型
· 一个费力不讨好的项目,让我损失了近一半的绩效!
· .NET Core 托管堆内存泄露/CPU异常的常见思路
· PostgreSQL 和 SQL Server 在统计信息维护中的关键差异
· C++代码改造为UTF-8编码问题的总结
· 一个费力不讨好的项目,让我损失了近一半的绩效!
· 清华大学推出第四讲使用 DeepSeek + DeepResearch 让科研像聊天一样简单!
· 实操Deepseek接入个人知识库
· CSnakes vs Python.NET:高效嵌入与灵活互通的跨语言方案对比
· Plotly.NET 一个为 .NET 打造的强大开源交互式图表库