一、到目前为止的程序流程图
为了让大家清楚目前的程序进度,画了到目前为止的程序流程图,如下。
二、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 是新准备的代码段和数据段。
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\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 }
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 }
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 }
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 }
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 }
这里卡了我好多天,一直就调不通,烦得我连博客都不想继续写了,于是放弃了... 后面还有文件系统这一块,不打算写啦
后面直接读 linux 源码来了解操作系统,敬请期待吧
写在最后:开源项目和课程规划
如果你对自制一个操作系统感兴趣,不妨跟随这个系列课程看下去,甚至加入我们(下方有公众号和小助手微信),一起来开发。
参考书籍
《操作系统真相还原》这本书真的赞!强烈推荐
项目开源
当你看到该文章时,代码可能已经比文章中的又多写了一些部分了。你可以通过提交记录历史来查看历史的代码,我会慢慢梳理提交历史以及项目说明文档,争取给每一课都准备一个可执行的代码。当然文章中的代码也是全的,采用复制粘贴的方式也是完全可以的。
如果你有兴趣加入这个自制操作系统的大军,也可以在留言区留下您的联系方式,或者在 gitee 私信我您的联系方式。
课程规划
本课程打算出系列课程,我写到哪觉得可以写成一篇文章了就写出来分享给大家,最终会完成一个功能全面的操作系统,我觉得这是最好的学习操作系统的方式了。所以中间遇到的各种坎也会写进去,如果你能持续跟进,跟着我一块写,必然会有很好的收货。即使没有,交个朋友也是好的哈哈。
目前的系列包括
- 【自制操作系统01】硬核讲解计算机的启动过程
- 【自制操作系统02】环境准备与启动区实现
- 【自制操作系统03】读取硬盘中的数据
- 【自制操作系统04】从实模式到保护模式
- 【自制操作系统05】开启内存分页机制
- 【自制操作系统06】终于开始用 C 语言了,第一行内核代码!
- 【自制操作系统07】深入浅出特权级
- 【自制操作系统08】中断
- 【自制操作系统09】中断的代码实现
- 【自制操作系统10】内存管理系统
- 【自制操作系统11】中场休息之细节是魔鬼
- 【自制操作系统12】熟悉而陌生的多线程
- 【自制操作系统13】锁
- 【自制操作系统14】实现键盘输入
微信公众号
我要去阿里(woyaoquali)
小助手微信号
Angel(angel19980323)