一、到目前为止的程序流程图
为了让大家清楚目前的程序进度,画了到目前为止的程序流程图,如下。
二、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)
来源:oschina
链接:https://my.oschina.net/u/4271269/blog/4277181