《操作系统真象还原》第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 }
View Code
复制代码

记得再修改它的头文件。

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 }
View Code
复制代码

即可得到结果:

额,好像输出的排版不是很好看,但问题不大。


回顾完线程与锁后紧接着来回顾进程(顺带后面内存管理的完善)。

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中的。注意,进程的页表是指进程自己独立的地址空间;而所有线程与内核共享地址空间,也就是和内核共用同一套页表,线程的页表指的是这个页表。


参考博客:


结束语:

本篇文章距离上一篇文章完结已有相当时间,原因在于最后一步调试了太久,给大家看看:

是的,这个问题困扰了我一天,周六我花了整整一天时间在定位这个问题,我以为问题应该出在了新写的tss和process程序,于是盯着它两看,仔细比对哪一句和标准程序不同,结果硬是找不出毛病。于是我开始怀疑是内存分布的锅,会不会是之前的0xc0000903和某句代码冲突了,于是我又copy了mbr.S和loader.S,重新编译运行,还是不行,说实话那时的我有点心态崩了。。。

于是周日我在寝室休息了一天,回顾了一下之前所写的代码以及整个程序运行的过程。

周一,也就是今天下午,我狠下心来把所有的程序都换成了标准程序,终于跑通了。我又喜又忧,喜的是整个项目是可以进行下去的,至少编译器和虚拟机没问题;忧的是我又要把程序一个一个的还原回去,找出有问题的程序。三个小时后,我发现原来是interrupt.c的一句代码出锅了:

看,那个idt的括号我多打了一对(明明书中如此,真的被害惨了),哎,但我哪里想得到是idt不正确呢,因为如果将它打印出来,你会发现值是一样的,我晕了。。。在你看到这句话的时候,我已经默默把之前有关中断的博客中代码改正确了。而且我也很奇怪,在其它博客的这句代码都是正确的,难道他们都不是按照书上写的代码来的吗,并且也没看到有博客提出这个错误。

由于我把整个代码都重过了一边,所以我的很多程序的结构都略有变化,如果大家在make all后有诸多warning和error,还请耐心调试,这也算是一路上的风景吧。

好了,快快结束本章吧,接下来的产出进度会稍稍变慢,还要备考周末的CSP考试呢,毕竟“我必须考虑这会不会是我此生仅有的机会”。

posted @   Hell0er  阅读(263)  评论(1编辑  收藏  举报
编辑推荐:
· [.NET]调用本地 Deepseek 模型
· 一个费力不讨好的项目,让我损失了近一半的绩效!
· .NET Core 托管堆内存泄露/CPU异常的常见思路
· PostgreSQL 和 SQL Server 在统计信息维护中的关键差异
· C++代码改造为UTF-8编码问题的总结
阅读排行:
· 一个费力不讨好的项目,让我损失了近一半的绩效!
· 清华大学推出第四讲使用 DeepSeek + DeepResearch 让科研像聊天一样简单!
· 实操Deepseek接入个人知识库
· CSnakes vs Python.NET:高效嵌入与灵活互通的跨语言方案对比
· Plotly.NET 一个为 .NET 打造的强大开源交互式图表库
点击右上角即可分享
微信分享提示