目录
一、前景回顾
二、进程的创建与初始化
三、如何进行进程的切换
四、运行测试
五、原书勘误
在上一回我们大概讲述了任务切换的发展,并且知道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 #endifprocess.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("\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 }memory.c
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 #endifmemory.h
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 }thread.c
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 #endifthread.h
修改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 }main.c
运行测试,可以看到基本正常。
五、原书勘误
这个地方我当初做到这里这一章节时,死活调不通。通过打断点,可以看到进入进程后,中断表有明显的异常。
在进程中,中断表的位置位于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);
好了,本回合就到此结束了。这一章知识量还是比较多的,代码也是很长的,我也是回味了很久。预知后事如何,请看下回分解。
标签:操作系统,thread,19,自制,uint32,pool,void,vaddr,struct From: https://www.cnblogs.com/Lizhixing/p/15984901.html