• 【自制操作系统15】用户进程


    一、到目前为止的程序流程图

      为了让大家清楚目前的程序进度,画了到目前为止的程序流程图,如下。

    二、CPU 原生支持多任务切换

      没错,本来多任务同分页、中断、段选择子一样,都是软硬件配合的产物,CPU 厂商也在硬件层面用 TSS 结构支持多任务,同中断的逻辑一样,也是有个 TSS 描述符存在 GDT 全局描述符表里,有个 TR 寄存器存储 TSS 的初始内存地址,然后只需要用一个简单的 call 指令,后面地址指向的描述符是一个 TSS 描述符的时候,就会发生任务切换,一条指令,很方便。

      但硬件其实也是通过 很多微指令 实现的任务切换,虽然程序员很方便用了一条指令就切换了任务,但实际上会产生一个很复杂很耗时的一些列操作,具体是啥我也没研究。

      所以现在的操作系统几乎都没有用原生的方式实现多任务,而是用软件方式自己实现,仅仅把 TSS 当作为 0 特权级的任务提供栈,不过那是因为硬件要求必须这么做,不然操作系统可能完全会忽视 TSS 的所有支持。比如 Linux 的做法就是,一次性加载 TSS 到 TR,之后不断修改同一个 TSS 的内容,不再进行重复加载操作。 Linux 在 TSS 中只初始化了 SS0、esp0 和 I/O 位图字段,除此之外 TSS 便没用了,就是个空架子,不再做保存任务状态之用。

    三、为应付 CPU 实现 TSS

     正如上文所说,我们只是应付一下

     userprog/tss.c

     1 #include "tss.h"
     2 #include "stdint.h"
     3 #include "global.h"
     4 #include "string.h"
     5 #include "print.h"
     6 
     7 /* 任务状态段tss结构 */
     8 struct tss {
     9     uint32_t backlink;
    10     uint32_t* esp0;
    11     uint32_t ss0;
    12     uint32_t* esp1;
    13     uint32_t ss1;
    14     uint32_t* esp2;
    15     uint32_t ss2;
    16     uint32_t cr3;
    17     uint32_t (*eip) (void);
    18     uint32_t eflags;
    19     uint32_t eax;
    20     uint32_t ecx;
    21     uint32_t edx;
    22     uint32_t ebx;
    23     uint32_t esp;
    24     uint32_t ebp;
    25     uint32_t esi;
    26     uint32_t edi;
    27     uint32_t es;
    28     uint32_t cs;
    29     uint32_t ss;
    30     uint32_t ds;
    31     uint32_t fs;
    32     uint32_t gs;
    33     uint32_t ldt;
    34     uint32_t trace;
    35     uint32_t io_base;
    36 }; 
    37 static 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 /* 创建gdt描述符 */
    45 static struct gdt_desc make_gdt_desc(uint32_t* desc_addr, uint32_t limit, uint8_t attr_low, uint8_t attr_high) {
    46    uint32_t desc_base = (uint32_t)desc_addr;
    47    struct gdt_desc desc;
    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 /* 在gdt中创建tss并重新加载gdt */
    58 void tss_init() {
    59    put_str("tss_init start
    ");
    60    uint32_t tss_size = sizeof(tss);
    61    memset(&tss, 0, tss_size);
    62    tss.ss0 = SELECTOR_K_STACK;
    63    tss.io_base = tss_size;
    64 
    65 /* gdt段基址为0x900,把tss放到第4个位置,也就是0x900+0x20的位置 */
    66 
    67   /* 在gdt中添加dpl为0的TSS描述符 */
    68   *((struct gdt_desc*)(GDT_BASE_ADDR+(0x8*4)))= make_gdt_desc((uint32_t*)&tss, tss_size - 1, TSS_ATTR_LOW, TSS_ATTR_HIGH);
    69 
    70   /* 在gdt中添加dpl为3的数据段和代码段描述符 */
    71   *((struct gdt_desc*)(GDT_BASE_ADDR+(0x8*5))) = make_gdt_desc((uint32_t*)0, 0xfffff, GDT_CODE_ATTR_LOW_DPL3, GDT_ATTR_HIGH);
    72   *((struct gdt_desc*)(GDT_BASE_ADDR+(0x8*6))) = make_gdt_desc((uint32_t*)0, 0xfffff, GDT_DATA_ATTR_LOW_DPL3, GDT_ATTR_HIGH);
    73    
    74   /* gdt 16位的limit 32位的段基址 */
    75    uint64_t gdt_operand = ((8 * 7 - 1) | ((uint64_t)(uint32_t)GDT_BASE_ADDR << 16));   // 7个描述符大小
    76    asm volatile ("lgdt %0" : : "m" (gdt_operand));
    77    asm volatile ("ltr %w0" : : "r" (SELECTOR_TSS));
    78    put_str("tss_init and ltr done
    ");
    79 }

    上述代码我们在 GDT 里增加了 TSS 描述符,和两个为后续用户进程准备的 代码段数据段,我们分别用 bochs 的 info gdt 和 info tss 看下目前的 GDT 结构,以及我们加载的唯一一个 TSS 的结构

    GDT

    可以看到,序号 0x04 就是 TSS 描述符,05 和 06 是新准备的代码段和数据段。

    TSS

    四、实现用户进程

      铺垫工作都做好了,下面开始最关键的实现用户进程部分

      还记得之前我们实现多线程的时候,定义的 task_struct 么,我们在之前的基础上加了属性 userprog_vaddr 用于指向用户进程的虚拟地址

    thread.h

     1 struct task_struct {
     2    uint32_t* self_kstack;     // 各内核线程都用自己的内核栈
     3    pid_t pid;
     4    enum task_status status;
     5    char name[TASK_NAME_LEN];
     6    uint8_t priority;
     7    uint8_t ticks;       // 每次在处理器上执行的时间嘀嗒数
     8 /* 此任务自上cpu运行后至今占用了多少cpu嘀嗒数,
     9  * 也就是此任务执行了多久*/
    10    uint32_t elapsed_ticks;
    11 /* general_tag的作用是用于线程在一般的队列中的结点 */
    12    struct list_elem general_tag;                    
    13 /* all_list_tag的作用是用于线程队列thread_all_list中的结点 */
    14    struct list_elem all_list_tag;
    15    uint32_t* pgdir;              // 进程自己页表的虚拟地址
    16    struct virtual_addr userprog_vaddr;   // 用户进程的虚拟地址
    17    struct mem_block_desc u_block_desc[DESC_CNT];   // 用户进程内存块描述符
    18    int32_t fd_table[MAX_FILES_OPEN_PER_PROC];    // 已打开文件数组
    19    uint32_t cwd_inode_nr;     // 进程所在的工作目录的inode编号
    20    pid_t parent_pid;         // 父进程pid
    21    int8_t  exit_status;         // 进程结束时自己调用exit传入的参数
    22    uint32_t stack_magic;     // 用这串数字做栈的边界标记,用于检测栈的溢出
    23 };

     之后我们按照代码调用顺序来看

    main.c

     1 ...
     2 int test_var_a = 0, test_var_b = 0;
     3 
     4 int main(void){
     5     put_str("I am kernel
    ");
     6     init_all();
     7     thread_start("threadA", 31, k_thread_a, "AOUT_");
     8     thread_start("threadB", 31, k_thread_b, "BOUT_");
     9     process_execute(u_prog_a, "userProcessA");
    10     process_execute(u_prog_b, "userProcessB");
    11     intr_enable();
    12     while(1);
    13     return 0;
    14 }
    15 
    16 void k_thread_a(void* arg) {
    17     char* para = arg;
    18     while(1) {
    19         console_put_str("threadA:");
    20         console_put_int(test_var_a);
    21         console_put_str("
    ");
    22     }
    23 }
    24 
    25 void k_thread_b(void* arg) {
    26     char* para = arg;
    27     while(1) {
    28         console_put_str("threadB:");
    29         console_put_int(test_var_b);
    30         console_put_str("
    ");
    31     }
    32 }
    33 
    34 void u_prog_a(void) {
    35     while(1) {
    36         test_var_a++;
    37     }
    38 }
    39 
    40 void u_prog_b(void) {
    41     while(1) {
    42         test_var_b++;
    43     }
    44 }

    process.c 中创建进程的主函数

     1 /* 创建用户进程 */
     2 void process_execute(void* filename, char* name) { 
     3    /* pcb内核的数据结构,由内核来维护进程信息,因此要在内核内存池中申请 */
     4    struct task_struct* thread = get_kernel_pages(1);
     5    init_thread(thread, name, default_prio); 
     6    create_user_vaddr_bitmap(thread);
     7    thread_create(thread, start_process, filename);
     8    thread->pgdir = create_page_dir();
     9    
    10    enum intr_status old_status = intr_disable();
    11    list_append(&thread_ready_list, &thread->general_tag);
    12    list_append(&thread_all_list, &thread->all_list_tag);
    13    intr_set_status(old_status);
    14 }

    里面连续调用了 5 个函数(其中黄色的是比创建线程多出来的),再加上两个添加链表函数,完成了创建进程的功能,下面我们看这五个函数都干了什么

    1 // 从内核物理内存池中申请1页内存,成功返回虚拟地址,失败NULL
    2 void* get_kernel_pages(uint32_t pg_cnt) {
    3     void* vaddr = malloc_page(PF_KERNEL, pg_cnt);
    4     if (vaddr != NULL) {
    5         memset(vaddr, 0, pg_cnt * PG_SIZE);
    6     }
    7     return vaddr;
    8 }
    get_kernel_pages
     1 // 初始化线程基本信息
     2 void init_thread(struct task_struct* pthread, char* name, int prio) {
     3     memset(pthread, 0, sizeof(*pthread));
     4     strcpy(pthread->name, name);
     5     
     6     if (pthread == main_thread) {
     7         pthread->status = TASK_RUNNING;
     8     } else {
     9         pthread->status = TASK_READY;
    10     }
    11     pthread->priority = prio;
    12     // 线程自己在内核态下使用的栈顶地址
    13     pthread->self_kstack = (uint32_t*)((uint32_t)pthread + PG_SIZE);
    14     pthread->ticks = prio;
    15     pthread->elapsed_ticks = 0;
    16     pthread->pgdir = NULL;
    17     pthread->stack_magic = 0x19870916; // 自定义魔数
    18 }
    init_thread
    1 /* 创建用户进程虚拟地址位图 */
    2 void create_user_vaddr_bitmap(struct task_struct* user_prog) {
    3    user_prog->userprog_vaddr.vaddr_start = USER_VADDR_START;
    4    uint32_t bitmap_pg_cnt = DIV_ROUND_UP((0xc0000000 - USER_VADDR_START) / PG_SIZE / 8 , PG_SIZE);
    5    user_prog->userprog_vaddr.vaddr_bitmap.bits = get_kernel_pages(bitmap_pg_cnt);
    6    user_prog->userprog_vaddr.vaddr_bitmap.btmp_bytes_len = (0xc0000000 - USER_VADDR_START) / PG_SIZE / 8;
    7    bitmap_init(&user_prog->userprog_vaddr.vaddr_bitmap);
    8 }
    create_user_vaddr_bitmap
     1 // 初始化线程栈 thread_stack
     2 void thread_create(struct task_struct* pthread, thread_func function, void* func_arg) {
     3     // 先预留中断使用栈的空间
     4     pthread->self_kstack -= sizeof(struct intr_stack);
     5     // 再留出线程栈空间
     6     pthread->self_kstack -= sizeof(struct thread_stack);
     7     struct thread_stack* kthread_stack = (struct thread_stack*)pthread->self_kstack;
     8     kthread_stack->eip = kernel_thread;
     9     kthread_stack->function = function;
    10     kthread_stack->func_arg = func_arg;
    11     kthread_stack->ebp = kthread_stack->ebx = kthread_stack->esi = kthread_stack->edi = 0;
    12 }
    thread_create
     1 // 创建页目录表,将当前页表的表示内核空间的pde复制
     2 uint32_t* create_page_dir(void) {
     3 
     4    /* 用户进程的页表不能让用户直接访问到,所以在内核空间来申请 */
     5    uint32_t* page_dir_vaddr = get_kernel_pages(1);
     6    if (page_dir_vaddr == NULL) {
     7       console_put_str("create_page_dir: get_kernel_page failed!");
     8       return NULL;
     9    }
    10 
    11 /************************** 1  先复制页表  *************************************/
    12    /*  page_dir_vaddr + 0x300*4 是内核页目录的第768项 */
    13    memcpy((uint32_t*)((uint32_t)page_dir_vaddr + 0x300*4), (uint32_t*)(0xfffff000+0x300*4), 1024);
    14 /*****************************************************************************/
    15 
    16 /************************** 2  更新页目录地址 **********************************/
    17    uint32_t new_page_dir_phy_addr = addr_v2p((uint32_t)page_dir_vaddr);
    18    /* 页目录地址是存入在页目录的最后一项,更新页目录地址为新页目录的物理地址 */
    19    page_dir_vaddr[1023] = new_page_dir_phy_addr | PG_US_U | PG_RW_W | PG_P_1;
    20 /*****************************************************************************/
    21    return page_dir_vaddr;
    22 }
    create_page_dir

    这里卡了我好多天,一直就调不通,烦得我连博客都不想继续写了,于是放弃了... 后面还有文件系统这一块,不打算写啦

    后面直接读 linux 源码来了解操作系统,敬请期待吧

    写在最后:开源项目和课程规划

    如果你对自制一个操作系统感兴趣,不妨跟随这个系列课程看下去,甚至加入我们(下方有公众号和小助手微信),一起来开发。

    参考书籍

    《操作系统真相还原》这本书真的赞!强烈推荐

    项目开源

    项目开源地址:https://gitee.com/sunym1993/flashos

    当你看到该文章时,代码可能已经比文章中的又多写了一些部分了。你可以通过提交记录历史来查看历史的代码,我会慢慢梳理提交历史以及项目说明文档,争取给每一课都准备一个可执行的代码。当然文章中的代码也是全的,采用复制粘贴的方式也是完全可以的。

    如果你有兴趣加入这个自制操作系统的大军,也可以在留言区留下您的联系方式,或者在 gitee 私信我您的联系方式。

    课程规划

    本课程打算出系列课程,我写到哪觉得可以写成一篇文章了就写出来分享给大家,最终会完成一个功能全面的操作系统,我觉得这是最好的学习操作系统的方式了。所以中间遇到的各种坎也会写进去,如果你能持续跟进,跟着我一块写,必然会有很好的收货。即使没有,交个朋友也是好的哈哈。

    目前的系列包括

     微信公众号

      我要去阿里(woyaoquali)

     小助手微信号

      Angel(angel19980323)

  • 相关阅读:
    OS程序开发引用的第三方库之间出现冲突的处理方法
    ios的指令集(转)
    查看lib库支持的IOS指令集
    Audio Session Programming Guide
    Swift中文教程
    NSString 与 char * 互转
    id 与void *类型的转换(转)
    versions使用(转)
    superview透明问题
    Python 头部 #!/usr/bin/python 和 #!/usr/bin/env 的区别
  • 原文地址:https://www.cnblogs.com/flashsun/p/12546934.html
Copyright © 2020-2023  润新知