Loading... 硬件生产厂商(Intel)给多进程切换提供了硬件级的解决方案,也就是使用 **TSS**(**Task-Stat Segment**),令人遗憾的是由于其效率较低,现代操作系统大多没有使用它来进行进程切换,但是特别的,在特权级转移时的栈切换仍然需要通过它来进行,所以虽然我们不用它来切换进程,也仍然需要设置好它。 *由于用不到,所以就不说如何用 TSS 来进行任务切换了* ### TSS 的设置 将 TSS 抽象为结构体,结构如下(对 32 位 CPU 而言) ```cpp struct tss { uint32_t backlink; uint32_t* esp0; uint32_t ss0; uint32_t* esp1; uint32_t ss1; uint32_t* esp2; uint32_t ss2; uint32_t cr3; uint32_t (*eip) (void); uint32_t eflags; uint32_t eax; uint32_t ecx; uint32_t edx; uint32_t ebx; uint32_t esp; uint32_t ebp; uint32_t esi; uint32_t edi; uint32_t es; uint32_t cs; uint32_t ss; uint32_t ds; uint32_t fs; uint32_t gs; uint32_t ldt; uint32_t trace; uint32_t io_base; }; ``` 可见这里保存了所有的寄存器,但是我们用不上他们,只需要设置好 esp0 和 ss0 就可以了,前者是从 3 特权级进入 0 特权级时的栈指针,后者是段选择子。由于我们的操作系统只用 RING3 和 RING0,所以 esp1 esp2 也不用管。特别的,io_base 是需要初始化的,这个是以单个端口为粒度进行端口访问权限控制的,初始化为结构体的末尾就可以了。 首先提供一个修改 esp0 的函数 ```cpp static struct tss tss; /* 更新tss中esp0字段的值为pthread的0级线 */ void UpdateTssEsp(struct task_struct* pthread) { tss.esp0 = (uint32_t*)((uint32_t)pthread + PAGE_SIZE); } ``` 这样进程切换优先级时就可以更新栈指针了。 然后需要在 GDT 中设置 TSS 描述符,这个也没什么特别可说的,之前我们在 loader 中设置了内核态使用的代码段和数据段描述符,不过那个是用汇编直接写进去的,这里我们用 C 写一个设置函数 ```cpp /* setup gdt desc */ static void SetUpGDTDesc(struct gdt_desc* desc_ptr, size_t* desc_base_addr, size_t limit, uint8_t attr_low, uint8_t attr_high) { desc_ptr->limit_low_word = limit & 0x0000FFFF; desc_ptr->limit_high_attr_high = (((limit & 0x000f0000) >> 16) + (uint8_t)(attr_high)); desc_ptr->base_low_word = (size_t) desc_base_addr & 0x0000FFFF; desc_ptr->base_mid_byte = (size_t) desc_base_addr & 0x00FF0000; desc_ptr->base_high_byte = (size_t) desc_base_addr >> 24; desc_ptr->attr_low_byte = (uint8_t)(attr_low); } ``` 然后用一个初始化函数进行设置 ```cpp /* create TSS, CODE, DATA desc in GDT, and reload GDT */ void TssInit() { sys_putstr("tss and ltr init.."); size_t tss_size = sizeof(tss); memset(&tss, 0, tss_size); tss.ss0 = SELECTOR_K_STACK; tss.io_base = tss_size; /* gdt_base: 0x600 + 0x10, tss on the 4th, which at 0x600 + 0x10 + 0x20 */ SetUpGDTDesc((struct gdt_desc*) (0xC0000630),\ (size_t *)&tss, tss_size - 1, TSS_ATTR_LOW, GDT_ATTR_HIGH); /* code DESC, dpl = 3 */ SetUpGDTDesc((struct gdt_desc*) (0xC0000638),\ (size_t *) 0, tss_size - 1, GDT_CODE_ATTR_LOW_DPL3, GDT_ATTR_HIGH); /* data, stack DESC, dpl = 3 */ SetUpGDTDesc((struct gdt_desc*) (0xC0000640),\ (size_t *) 0, tss_size - 1, GDT_DATA_ATTR_LOW_DPL3, GDT_ATTR_HIGH); uint64_t gdt_operand = \ ((8 * 7 - 1) | ((uint64_t)((uint32_t)0xC0000610 << 16))); __asm__ volatile ("lgdt %0" : : "m" (gdt_operand)); __asm__ volatile ("ltr %w0" : : "r" (SELECTOR_TSS)); sys_putstr(" done\n"); } ``` 这里将 TSS 段描述符设置到了 GDT[3] 上,用户态代码段(DPL 为 3)设置到了 GDT[4],用户态数据段设置到了 GDT[5] 上。然后通过 lgdt 重新设置了 gdt 表。到这里都是已知的东西,然后的 ltr 就是设置 TSS 的指令。 ltr 将一个段选择子的值写到 TR(Task Register)中,这就是硬件层面的多进程支持所使用的关键寄存器,如果使用 TSS 进行切换,那么将 TR 修改为不同的段选择子,选择不同的 TSS 就可以实现进程切换了,但是由于效率问题以 Linux 为代表的现代操作系统都不使用 TSS 来切换,而是一直都使用同样的 TSS 来*欺骗*处理器。所以这里把写好的 TSS 段描述符的选择子 load 到 TR 里面就可以了。 完成之后 GDT 表就应该是这样的 <div style="text-align:center"><img src="https://www.cjovi.icu/usr/uploads/2021/06/3719226770.png "></div> ### 进入 RING3 一个用户进程和之前已经实现的内核线程的区别主要有两点 * 进程有独立页表 * 用户进程的特权级为 3 首先解决独立页表,页表都是存储在内核内存池的,自然的,也应该由内核来为用户初始化页表,要做的就是为用户从内核内存池中申请数个页框来存放 PDE 和映射到内核空间的 PTE。申请好了之后要把内核的 PDE 值拷贝到用户的 PDE 中,实现内核资源所有进程共享。特别的,PDE 的最后一项要映射到用户的 PDE 上,这样之后才能对 PDE 进行操作。 页表初始化了之后内存管理的一系列结构也需要初始化,即虚拟地址位图。 然后解决退特权级的问题,在 X86 下,要从高特权级转移到低特权级必须使用 iret,所以我们通过中断退出函数 ExitInt 来进入特权级 3,只要提前在用户进程的内核栈中设置好几个寄存器的值,未来调度器扫到用户进程的时候就可以转到用户进程代码上了。 ### 代码 原理说起来比较简单,但是实现起来各种问题不断。这里的代码我调了快 10 个小时才调出来。tag 已打好,具体请看[此处](https://github.com/chujDK/chuj-elephant-os/releases/tag/user_process)。 最后的执行效果大概为 <div style="text-align:center"><img src="https://www.cjovi.icu/usr/uploads/2021/06/3882133818.png "></div> 最后修改:2021 年 06 月 10 日 © 允许规范转载 打赏 赞赏作者 支付宝微信 赞 1 如果觉得我的文章对你有用,那听听上面我喜欢的歌吧