【自制操作系统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\n");
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\n");
79 }

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

GDT

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

【自制操作系统15】用户进程_第1张图片

TSS

【自制操作系统15】用户进程_第2张图片

 

四、实现用户进程

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

  还记得之前我们实现多线程的时候,定义的 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\n");
 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("\n");
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("\n");
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 私信我您的联系方式。

课程规划

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

目前的系列包括

  • 【自制操作系统01】硬核讲解计算机的启动过程
  • 【自制操作系统02】环境准备与启动区实现
  • 【自制操作系统03】读取硬盘中的数据
  • 【自制操作系统04】从实模式到保护模式
  • 【自制操作系统05】开启内存分页机制
  • 【自制操作系统06】终于开始用 C 语言了,第一行内核代码!
  • 【自制操作系统07】深入浅出特权级
  • 【自制操作系统08】中断
  • 【自制操作系统09】中断的代码实现
  • 【自制操作系统10】内存管理系统
  • 【自制操作系统11】中场休息之细节是魔鬼
  • 【自制操作系统12】熟悉而陌生的多线程
  • 【自制操作系统13】锁
  • 【自制操作系统14】实现键盘输入

 微信公众号

  我要去阿里(woyaoquali)

 小助手微信号

  Angel(angel19980323)

你可能感兴趣的:(【自制操作系统15】用户进程)