• [自制操作系统] 第19回 实现用户进程(下)


    目录
    一、前景回顾
    二、进程的创建与初始化
    三、如何进行进程的切换
    四、运行测试
    五、原书勘误

     

    一、前景回顾

      在上一回我们大概讲述了任务切换的发展,并且知道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 #endif
    process.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 #endif
    memory.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 #endif
    thread.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);

      好了,本回合就到此结束了。这一章知识量还是比较多的,代码也是很长的,我也是回味了很久。预知后事如何,请看下回分解。

  • 相关阅读:
    Word自动生成目录的方法
    Windows右键菜单美化(适用版本:Win7-Win11) 奇怪的美化教程 #1
    什么是闭包
    1032 Sharing
    第四章 使用管理门户(四)
    Linux高性能服务器编程——ch5笔记
    关于Android Studio中开发Flutter配置
    第一天:java基础复习(1)
    ViT论文详解
    ROS 学习应用篇(六)参数的使用与编程
  • 原文地址:https://www.cnblogs.com/Lizhixing/p/15984901.html