《操作系统真像还原》操作系统实现——用户进程
硬件生产厂商(Intel)给多进程切换提供了硬件级的解决方案,也就是使用 TSS(Task-Stat Segment),令人遗憾的是由于其效率较低,现代操作系统大多没有使用它来进行进程切换,但是特别的,在特权级转移时的栈切换仍然需要通过它来进行,所以虽然我们不用它来切换进程,也仍然需要设置好它。
由于用不到,所以就不说如何用 TSS 来进行任务切换了
TSS 的设置
将 TSS 抽象为结构体,结构如下(对 32 位 CPU 而言)
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 的函数
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 写一个设置函数
/* 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);
}
然后用一个初始化函数进行设置
/* 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 表就应该是这样的

进入 RING3
一个用户进程和之前已经实现的内核线程的区别主要有两点
- 进程有独立页表
- 用户进程的特权级为 3
首先解决独立页表,页表都是存储在内核内存池的,自然的,也应该由内核来为用户初始化页表,要做的就是为用户从内核内存池中申请数个页框来存放 PDE 和映射到内核空间的 PTE。申请好了之后要把内核的 PDE 值拷贝到用户的 PDE 中,实现内核资源所有进程共享。特别的,PDE 的最后一项要映射到用户的 PDE 上,这样之后才能对 PDE 进行操作。
页表初始化了之后内存管理的一系列结构也需要初始化,即虚拟地址位图。
然后解决退特权级的问题,在 X86 下,要从高特权级转移到低特权级必须使用 iret,所以我们通过中断退出函数 ExitInt 来进入特权级 3,只要提前在用户进程的内核栈中设置好几个寄存器的值,未来调度器扫到用户进程的时候就可以转到用户进程代码上了。
代码
原理说起来比较简单,但是实现起来各种问题不断。这里的代码我调了快 10 个小时才调出来。tag 已打好,具体请看此处。
最后的执行效果大概为
