《操作系统真像还原》操作系统实现——用户进程

Posted on Jun 9, 2021

硬件生产厂商(Intel)给多进程切换提供了硬件级的解决方案,也就是使用 TSSTask-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 已打好,具体请看此处

最后的执行效果大概为