uint32_t* pgdir; //进程自己页表的虚拟地址
struct virtual_addr userprog_vaddr; //用户进程的虚拟地址池
页表使用虚拟地址是因为页目录表本身也要占用内存来存储,我们在为进程创建页目录表,肯定要为页目录表申请内存,内存管理系统返回的地址肯定都是虚拟地址,不可能返回物理地址。
static void* vaddr_get(enum pool_flags pf, uint32_t pg_cnt) {
int vaddr_start = 0, bit_idx_start = -1;
uint32_t cnt = 0;
if (pf == PF_KERNEL) { // 内核内存池
bit_idx_start = bitmap_scan(&kernel_vaddr.vaddr_bitmap, pg_cnt);
if (bit_idx_start == -1) {
return NULL;
}
while(cnt < pg_cnt) {
bitmap_set(&kernel_vaddr.vaddr_bitmap, bit_idx_start + cnt++, 1);
}
vaddr_start = kernel_vaddr.vaddr_start + bit_idx_start * PG_SIZE;
} else { // 用户内存池
struct task_struct* cur = running_thread();
bit_idx_start = bitmap_scan(&cur->userprog_vaddr.vaddr_bitmap, pg_cnt);
if (bit_idx_start == -1) {
return NULL;
}
while(cnt < pg_cnt) {
bitmap_set(&cur->userprog_vaddr.vaddr_bitmap, bit_idx_start + cnt++, 1);
}
vaddr_start = cur->userprog_vaddr.vaddr_start + bit_idx_start * PG_SIZE;
/* (0xc0000000 - PG_SIZE)做为用户3级栈已经在start_process被分配 */
ASSERT((uint32_t)vaddr_start < (0xc0000000 - PG_SIZE));
}
return (void*)vaddr_start;
}
void* get_a_page(enum pool_flags pf, uint32_t vaddr) {
struct pool* mem_pool = pf & PF_KERNEL ? &kernel_pool : &user_pool;
lock_acquire(&mem_pool->lock);
/* 先将虚拟地址对应的位图置1 */
struct task_struct* cur = running_thread();
int32_t bit_idx = -1;
/* 若当前是用户进程申请用户内存,就修改用户进程自己的虚拟地址位图 */
if (cur->pgdir != NULL && pf == PF_USER) {
bit_idx = (vaddr - cur->userprog_vaddr.vaddr_start) / PG_SIZE;
ASSERT(bit_idx > 0);
bitmap_set(&cur->userprog_vaddr.vaddr_bitmap, bit_idx, 1);
} else if (cur->pgdir == NULL && pf == PF_KERNEL){
/* 如果是内核线程申请内核内存,就修改kernel_vaddr. */
bit_idx = (vaddr - kernel_vaddr.vaddr_start) / PG_SIZE;
ASSERT(bit_idx > 0);
bitmap_set(&kernel_vaddr.vaddr_bitmap, bit_idx, 1);
} else {
PANIC("get_a_page:not allow kernel alloc userspace or user alloc kernelspace by get_a_page");
}
void* page_phyaddr = palloc(mem_pool);
if (page_phyaddr == NULL) {
return NULL;
}
page_table_add((void*)vaddr, page_phyaddr);
lock_release(&mem_pool->lock);
return (void*)vaddr;
}
刚开始我们的系统一直运行在kernel中,特权级一直是0,所以我们要从0特权级进入3特权级。我们的中断从3特权级进入0特权级,然后是通过 iret 返回,所以我们这里也假装通过 iret 返回,所以要经过 intr_exit,即一大堆的 pop 指令,将3特权级的寄存器资源弹出去。从中断返回时候,要弹出之前的中断前的寄存器资源,所以我们要进入3特权级,要提前在 intr_exit中设置好3特权级的资源。CPU通过中断栈中CS的RPL知道了将会返回哪个特权级,所以RPL必须为3。
1,假装从中断返回,用过 iret 指令,就要经过 intr_exit 的一系列 pop
2,要提前在中断栈中设置好3特权级的环境:cs和eip、ss 和 esp 等,以便借着pop可以弹出
3,中断栈中CS中的RPL 必须为3
4,相应的在用户模式下,只能访问特权级为3的代码段和数据段,因此中断栈中的相应的段寄存器中的选择子指向的段描述符的DPL也要为3
5,在进入中断时候,eflags 的 IF 位 为0,不能响应中断,在退出中断后要置1,因此中断栈中的IF要置1
6,中断栈中的 eflags 的IOPL 位为1 ,不允许用户进程直接访问硬件。
进程首先要先创建好,然后才能运行。进程创建由函数 process_execute()来完成:包括创建进程使用的PCB(一页),然后完善这个PCB:初始化好struck task_stack:虚拟地址池和页表、创建好 thread_stack:注意是进程的函数了、创建好 intr_stack:进程的上下文环境、然后将进程加入全部队列和就绪队列
图中的 init_thread() 是创建 task_stack 的,thread_create() 是创建 thread_stack的:首次运行是调用 kernek_thread(start_process,user_prog),然后调用 start_process(user_prog)的。
创建好后开始执行,通过 时钟中断后时间片到了然后调用 schedule()来进行调度就行进程队列,pop 出用户进程开始执行的。首先激活页表,创建时候将页表的内容都已经写好, 但是并没有给CR3赋值,所以。在shedule()中要将页表激活,就是给CR3赋值。然后通过 ret 弹到 kernel_thread(start_process,user_porg)-->start_process(user_prog)。start_process(user_prog)函数功能:用来构建用户进程的上下文,也就是在 intr_stack 附上合适的值,然后调用 intr_eixt,pop 出一系列值,进入到用户进程中CPU开始执行。
注意:可以看出1,进程是在线程的基础上执行的,很多函数都是通用的。2,线程在执行到 kernel_thread( function,arg)--》function(arg),这个函数功能就是直接执行进程代码了。但是进程到这一步后的函数功能是:先初始化 intr_stack,然后调用 intr_exit,pop 出一系列的值,然后执行进程的代码。
操作系统是为用户进程服务的,它提供了各种系统功能供用户进程调用。为了用户进程可以访问到内核服务,必须确保用户进程在自己的地址空间中能够访问到内核才行。虚拟地址空间由页表来控制,页表由操作系统来管理,所以用户空间的虚拟空间是由操作系统分配的。每个用户都有4GB的虚拟空间,操作系统把4GB的用户空间和内核空间,操作系统占了3G-4G,用户空间是0G-3G。用户进程占据了页目录表的 0-767 个页目录项,内核占据页目录表的 768-1023个目录项。
操作系统在物理内存中的位置是固定的,内核进程也只有一套页表,也是固定的。我们要想内核被所有的用户进程都可以访问到,我们只需要把每个用户进程页目录项用内核页目录表的第768-1023个页目录项代替即可。因为我们的内核页表也是3GB-4GB的,这样当我们的用户进程的3GB-4GB的一个地址来访问时候,就会用虚拟地址的10+10+12来访问物理地址。我们只复制页目录项即可,页表项不需要复制,因为1024个页表在内核的页表地址中已经存在了,用户进程中直接转到内核页表项中去找了。
//功能:创建页表,注意页表项都应该填入物理地址,CR3中应该填入的也是物理地址
//实现:在内核物理池中申请一页,4k/4=1024项,我们将后四分之一复制成内核的。
uint32_t* create_page_dir(void)
{
uint32_t* page_dir_vaddr = get_kernel_pages(1);//用户进程的页表不能让用户直接访问到,所以在内核空间来申请
if (page_dir_vaddr == NULL)
{
console_put_str("create_page_dir: get_kernel_page failed!");
return NULL;
}
//1 先复制页表
memcpy((uint32_t*)((uint32_t)page_dir_vaddr + 0x300 * 4), (uint32_t*)(0xfffff000 + 0x300 * 4), 1024);// page_dir_vaddr + 0x300*4 是内核页目录的第768项
//2 更新页目录地址
uint32_t new_page_dir_phy_addr = addr_v2p((uint32_t)page_dir_vaddr);
page_dir_vaddr[1023] = new_page_dir_phy_addr | PG_US_U | PG_RW_W | PG_P_1;
return page_dir_vaddr;
}
用户进程有自己的堆和栈,因此必须要有方法跟踪内存的分配情况。和内核一样,用户进程也是用位图来管理地址分配的,每个进程有自己单独的位图。用户进程被加载到内存后,剩余未用的高地址都被作为堆和栈的共享空间。虚拟地址池结构包括虚拟起始地址和虚拟地址的位图。我们选择 0xc0804_8000 为用户程序的起始虚拟地址,即0x0804_8000 --0xc000_0000为用户进程的虚拟地址。我们还是用位图的一位表示4K,那么我们的虚拟地址需要的位图页数就要从内核物理池中申请。
//功能:创建用户进程虚拟地址位图
//实现:赋值用户进程虚拟地址的起始地址、赋值用户进程虚拟地址位图的起始地址和位图字节长度。
void create_user_vaddr_bitmap(struct task_struct* user_prog) {
user_prog->userprog_vaddr.vaddr_start = USER_VADDR_START;
uint32_t bitmap_pg_cnt = DIV_ROUND_UP((0xc0000000 - USER_VADDR_START) / PG_SIZE / 8 , PG_SIZE);
user_prog->userprog_vaddr.vaddr_bitmap.bits = get_kernel_pages(bitmap_pg_cnt);
user_prog->userprog_vaddr.vaddr_bitmap.btmp_bytes_len = (0xc0000000 - USER_VADDR_START) / PG_SIZE / 8;
bitmap_init(&user_prog->userprog_vaddr.vaddr_bitmap);
}
//功能:创建用户进程:在内核中申请PCB,然后初始化各个栈,主要是为 task_stack 中的页表和虚拟地址池赋值。
void process_execute(void* filename, char* name) {
/* pcb内核的数据结构,由内核来维护进程信息,因此要在内核内存池中申请 */
struct task_struct* thread = get_kernel_pages(1); //为进程申请一个pcb页。
init_thread(thread, name, default_prio); //初始化task_struck
create_user_vaddr_bitmap(thread); //为task_struck中的虚拟地址赋值
thread_create(thread, start_process, filename); //初始化thread_stack,即第一次运行时候的函数
thread->pgdir = create_page_dir(); //页表
enum intr_status old_status = intr_disable();
ASSERT(!elem_find(&thread_ready_list, &thread->general_tag));
list_append(&thread_ready_list, &thread->general_tag);
ASSERT(!elem_find(&thread_all_list, &thread->all_list_tag));
list_append(&thread_all_list, &thread->all_list_tag);
intr_set_status(old_status);
}
//功能:激活进程:包括激活页表和更新tss中的0特权级栈。激活页表是将页表物理地址赋予CR3。更新tss是将
void process_activate(struct task_struct* p_thread) {
ASSERT(p_thread != NULL);
/* 击活该进程或线程的页表 */
page_dir_activate(p_thread);
/* 内核线程特权级本身就是0,处理器进入中断时并不会从tss中获取0特权级栈地址,故不需要更新esp0 */
if (p_thread->pgdir) {
/* 更新该进程的esp0,用于此进程被中断时保留上下文 */
update_tss_esp(p_thread);
}
}
//功能:创建用户进程上下文。当通过switch()函数开始执行用户进程时候首先要先设置用户上下文环境,然后 iret 出去。
void start_process(void* filename_) {
void* function = filename_;
struct task_struct* cur = running_thread();
cur->self_kstack += sizeof(struct thread_stack);
struct intr_stack* proc_stack = (struct intr_stack*)cur->self_kstack;
proc_stack->edi = proc_stack->esi = proc_stack->ebp = proc_stack->esp_dummy = 0;
proc_stack->ebx = proc_stack->edx = proc_stack->ecx = proc_stack->eax = 0;
proc_stack->gs = 0; // 用户态用不上,直接初始为0
proc_stack->ds = proc_stack->es = proc_stack->fs = SELECTOR_U_DATA;
proc_stack->eip = function; // 待执行的用户程序地址
proc_stack->cs = SELECTOR_U_CODE;
proc_stack->eflags = (EFLAGS_IOPL_0 | EFLAGS_MBS | EFLAGS_IF_1);
proc_stack->esp = (void*)((uint32_t)get_a_page(PF_USER, USER_STACK3_VADDR) + PG_SIZE) ;
proc_stack->ss = SELECTOR_U_DATA;
asm volatile ("movl %0, %%esp; jmp intr_exit" : : "g" (proc_stack) : "memory");
}
用户进程的特征为:3特权级、单独的页表和3特权级的栈。无论代码在内核空间还是在用户空间,代码都是一段指令而已,CPU并不能分清这段代码是用户代码还是内核代码。处理器关注的是CPL:当前特权级,即cs中的RPL。