进程的实现
一、数据结构的实现(基于内核级线程)
进程和线程的区别:数据结构角度而言
截至到目前,已经实现了内核级线程,数据结构的定义如下:
struct thread{
struct thread_stack * stack_top;
char name [16];
uint_32 id;
enum T_STATUS status;
//还剩的时钟数,每次时钟中断-1
uint_32 tick;
//线程优先级,等于一开始的时钟数
uint_32 priority;
//线程总共的时钟数
uint_32 total_tick;
struct list_node ready_tag;
struct list_node all_tag;
uint_32 stack_magic;
};
thread_stack中保存了线程的上下文,定义如下:
struct thread_stack{
uint_32 gs;
uint_32 fs;
uint_32 es;
uint_32 ds;
uint_32 edi;
uint_32 esi;
uint_32 ebp;
uint_32 esp;
uint_32 ebx;
uint_32 edx;
uint_32 ecx;
uint_32 eax;
//以下参数用于线程第一次调用
void * thread_fun;
uint_32 blank ;//用于占位
thread_fun * function;
void * fun_arg;
};
注意pop和push指令在默认情况下针对的操作数是32位,所以为了防止出错,最好将16位的段寄存器也定义成32位。
对于进程而言,与线程最本质的区别在于是否拥有独立的地址空间,所以两者在数据结构的差别就很明显了,进程需要定义两个指针,一个指向其页目录表,一个指向其管理虚拟内存用的内存池。
这一点也可以用来进行对线程和进程的区分,如果页目录表指针不为空,说明当前进程就是线程。
二、运行环境的准备
CPU眼中进程的身份证 —— TSS字段(Task Status Segment)
上面有提到过,线程与进程最本质的区别在于是否拥有独立的地址空间,这只是从概念上来讲,毕竟不可能让CPU记住上一个cr3寄存器中的值。
首先要明确一点,CPU是有必要知道当前是否发生了进程切换的,或者说我们必须让CPU知道进程发生了切换,原因主要在于:在由低特权级向高特权级转移的过程中,硬件支持的唯一一种获得高特权级栈指针的方式就是CPU根据TSS字段中相应的数据自动获取。
至于高特权级向低特权级转移时,由于当前的高特权级一定是由低特权级转移来的,在进行特权级级转移时自然会将低特权级栈顶以及其他参数压入到高特权级栈顶,最后通过iret指令返回。
那问题来了,一开始开机的时候肯定是运行在内核态,处于最高的特权级,操做系统一开始是如何跳转到用户态执行的?这时的跳转后的栈顶指针又在哪?这是下一个的标题的内容,这里先聚焦于TSS字段上。
TSS字段是CPU原生支持的多道程序设计实现方案,但是由于操作过于繁琐,性能低下,现代操作系统的设计者们并不买Intel的这笔帐,与之处于相同状态的还有LDT(Local Descriptor Table),这部分不细说了,《操作系统真相还原》上面11.1节说的很细了,忘了可以去看。
模仿Linux的做法,可以通过骗过CPU来实现多道程序设计。TSS字段的切换既然很耗时,那就干脆不进行TSS的保存和读写,但是又因为上述原因,必须要进行TSS的填写,既然如此,可以让tr寄存器一直指向一个TSS字段。
ss0和esp0是操作系统负责维护,CPU负责使用的,所以在创建TSS字段时必须要进行填写。
总结,需要做的事情有两件:
- 创建好一个TSS字段,填写好对应的ss0和esp0,在进行任务切换时要进行ss0和esp0的切换(这部分工作在进程切换时完成,即在内核态完成)。
- 将TSS字段加载到tr寄存器。
疑问一: 用户进程执行系统调用(内核级代码)和用户进程切换到内核进程的区别
说到底,两种进程的区别就是一开始创建它的时候给他的段选择子的区别。
为什么这么说?因为从实际来讲,内核进程和用户进程的区别无非就是对内存访问权限的不同而已,这是由特权级的检查来完成的,发生在段寄存器的load时,无非就是CPL、DPL、RPL的比较和判断。
所以说决定了进程一开始的段选择子就决定了它的访问权限,也就区分出了内核进程和用户进程。
回到正题,用户进程执行系统调用和切换到内核进程直接执行,关键的区别在于三个:
- 内核进程和用户进程所对应的页目录表以及页表不同,当然,内核页表都是相同的,至于用户页表,对内核来进程说可以不开辟,反正它也用不到。
- 在执行系统调用时,对数据的访问权限RPL是由操作系统来决定,关于这点具体可看《CPL、RPL和DPL》中的疑问一。
- 当前进程的PCB不同,这个区别其实存在于所有进程间。
说到底,操作系统是服务的集合,所以所谓的内核进程,除了在一开始开机初始化的时候很重要,其余时候除了一些数据的维护外也就啥不干了,所谓的陷入内核和交给内核执行都是通过中断的方式来执行系统调用,真正的执行者还是用户进程,只不过是在0级特权下执行内核代码而已。
疑问二 : 为什么说esp0和ss0只能通过TSS来获取?
到这里会产生疑问,既然esp0和ss0是内核栈,按道理来说thread结构体中stack_top成员就是啊,调用get_running方法就能获取,那为什么说只有通过TSS才能获取呢?
注意,对于用户进程来说,他们是无法访问thread结构体,更无法调用get_running方法的,这些都属于0级特权的数据,那我们怎么让用户进程在被切换到内核态时,让接下来的内核程序知道应该使用哪个栈呢?
很简单,提前把要使用的内核栈顶指针放在某个地方就行了,既然CPU在TSS字段都提供了ss0和esp0的位置,还负责自动切换,那又何必多此一举在其他地方放着呢?如果TSS中的ss0和esp0不对,到了特权级0下还得重新载入正确的,得不偿失。