操作系统开发系列—13.a.进程 ●
进程的切换及调度等内容是和保护模式的相关技术紧密相连的,这些代码量可能并不多,但却至关重要。
我们需要一个数据结构记录一个进程的状态,在进程要被挂起的时候,进程信息就被写入这个数据结构,等到进程重新启动的时候,这个信息重新被读出来。
在很多情况下,进程和进程调度是运行在不同的层级上的。这里本着简单的原则,我们让所有任务运行在ring1,而让进程切换运行在ring0.
诱发进程切换的原因不只一种,比较典型的情况是发生了时钟中断。但并非在每一次时钟中断时都一定会发生进程切换,不过这里为了容易理解和实现,每次中断都切换一次进程。
下面介绍一下进程切换时的情形,如下图所示:
1.进程A运行中。
2.时钟中断发生,ring1—>ring0,时钟中断处理程序启动。
3.进程调度,下一个应运行的进程(假设为进程B)被指定。
4.进程B被恢复,ring0—>ring1.
5.进程B运行中。
只有可能被改变的才有保存的必要。所以我们要把寄存器的值统统保存起来,准备进程被恢复执行时使用。
一条pushad指令可以保存许多寄存器值。
进程栈和内核栈如下图:
进程栈——进程运行时自身的堆栈。
进程表——存储进程状态信息的数据结构。
内核栈——进程调度模块运行时使用的堆栈。
对于有特权级变换的转移,如果由外层向内层转移时,需要从TSS中取得从当前TSS中取出内层ss和esp作为目标代码的ss和esp。所以我们必须事先准备好TSS。由于每个进程相对独立,我们把涉及到的描述符放在局部描述符表LDT中,所以我们还需要为每个进程准备LDT。
整个程序的大致流程是:
sgdt [gdt_ptr] ; cstart() 中将会用到 gdt_ptr call cstart ; 在此函数中改变了gdt_ptr,让它指向新的GDT lgdt [gdt_ptr] ; 使用新的GDT lidt [idt_ptr] jmp SELECTOR_KERNEL_CS:csinit
首先走kernel.asm的_start——这里调用start.c的cstart函数
cstart函数——主要是将loader中的GDT复制到新的GDT中
紧接着调用init_prot()函数——init_prot函数在protect.c中,主要是初始化8259A和全部中断门,此函数的最后是填充GDT中TSS这个描述符,紧接着填充GDT中进程的LDT的描述符。
此时cstart结束。
然后执行csinit——先加载ltr(TSS),然后进入kernel_main
kernel_main函数——是在main.c中定义的,此函数首先初始化进程的进程表的各个属性,进程表的定义在proc.h中,然后最后执行restart函数
restart函数在kernel.asm中定义,如下:
restart: mov esp, [p_proc_ready] lldt [esp + P_LDT_SEL] lea eax, [esp + P_STACKTOP] mov dword [tss + TSS3_S_SP0], eax pop gs pop fs pop es pop ds popad add esp, 4 iretd
restart是进程调度的一部分,同时也是我们的操作系统启动第一个进程时的入口。
首先让esp指向将要运行的进程的进程表,然后加载ldt指向的是进程表的ldt_selector,restart最后两行的作用是将s_proc这个结构中第一个结构体成员regs的末地址赋给TSS中ring0堆栈指针域(esp)。我们可以想象,在下一次中断发生时,esp将变成regs的末地址,然后进程ss和esp两个寄存器值,以及eflags还有cs、eip这几个寄存器值将依次被压栈,放到regs这个结构的最后面(不要忘记堆栈是从高地址向低地址生长的),最后通过iretd指令执行并进入进程TestA。
对于p_proc_ready,编译器在编译时会产生一个符号表,记录了符号名和它的地址。对于指针变量,符号表里记录的是指针的地址,通过该地址取到所指变量的真实地址,最后取到的才是所指变量的值。
IRETD 指令先弹出一个32位的EIP值,然后再弹出一个32位值并将最低的2个字节值传入CS寄存器,最后再弹出一个32位的标志寄存器值
lea——
比如: LEA AX,BUF
就是将存储器中BUF所指的地址传送给AX.
区别MOV传送指令:
MOV传送的是地址所指的内容,而LEA只是地址。
还有从低特权级到高特权级转移的时候,需要用到TSS
p_proc_ready应该是一个指向进程表的指针,存放的便是下一个要启动进程的进程表的地址。而且其中的内容必然是以下图所示的顺序进行存放,这样才会使pop和popad指令执行后各寄存器的内容更新一遍。
p_proc_ready是一个结构类型指针:struct s_proc*。s_proc这个结构体的第一个成员也是一个结构s_stackframe,它的内容安排与我们的推断完全一致。
进程的状态统统被存放在s_proc这个结构体中,s_proc这个结构就应该是我们提到过的“进程表”。当要恢复一个进程时,便将esp指向这个结构体的开始处,然后运行一系列的pop命令将寄存器值弹出。进程表的开始位置结构图如下图所示:
接下来lldt这个指令是设置ldtr的。esp + P_LDT_SEL是s_proc中的成员ldt_sel。restart最后两行的作用是将s_proc这个结构中第一个结构体成员regs的末地址赋给TSS中ring0堆栈指针域(esp)。
一个进程开始之前,必须初始化的寄存器列表:cs、ds、es、fs、gs、ss、esp、eip、eflags。
我们在Loader中就把gs对应的描述符DPL设为3,所以进程中的代码是有权限访问显存的。
在第一个进程正式开始之前,其核心内容便是一个进程表以及与之相关的TSS等内容。如下图所示:
这个图看起来有点复杂,但是如果将其化整为零,可以分为4个部分,那就是进程表、进程体、GDT和TSS。它们之间的关系大致分为三个部分:
1.进程表和GDT。进程表内的LDT Selector对应GDT中的一个描述符,而这个描述符所指向的内存空间就存在于进程表内。
2.进程表和进程。进程表是进程的描述,进程运行过程中如果被中断,各个寄存器的值都会被保存进进程表中。但是在我们的第一个进程开始之前,并不需要初始化太多内容,只需要知道进程的入口地址就足够了。另外由于程序免不了用到堆栈,而堆栈是不受程序本身控制的,所以还需要事先指定esp。
3.GDT和TSS。GDT中需要有一个描述符来对应TSS,需要事先初始化这个描述符。
第一步,首先来准备一个小的进程体。
void TestA() { int i = 0; while(1){ disp_str("A"); disp_int(i++); disp_str("."); delay(1); } }
在之前我们调用指令sti打开中断之后就用hlt指令让程序停止以等待中断的发生。但在这里我得把hlt注释掉。还有由于在完成进程的编写之前,要让程序停住,所以我们用一个死循环作为它的结束。
PUBLIC int kernel_main() { ... while(1){} }
第二步,初始化进程表。
要初始化进程表,首先要有进程表结构的定义,proc.h的STACK_FRAME。global.c的NR_TASKS定义了最大允许进程,我们把它设为1.初始化进程表的代码在main.c的kernel_main()函数。
进程表需要初始化的主要有3个部分:寄存器、LDT Selector和LDT。LDT Selector被赋值为SELECTOR_LDT_FIRST,LDT里面共有两个描述符,为简化起见,分别被初始化成内核代码段和内核数据段,只是改变了一下DPL以让其运行在低的特权级下。
要初始化的寄存器比较多,cs指向LDT中第一个描述符,ds、es、fs、ss都设为指向LDT中的第二个描述符,gs仍然指向显存,只是其RPL发生改变。
接下来eip指向TestA,这表明进程将从TestA的入口地址开始运行。另外esp指向了单独的栈,栈的大小为STACK_SIZE_TOTAL。
最后一行是设置eflags,0x1202恰好设置了IF位并把IOPL设为1.这样,进程就可以使用I/O指令,并且中断会在iretd执行时被打开(kernel.asm中的sti指令已经被注释掉了)。
一定要记得LDT跟GDT是联系在一起的,别忘了填充GDT中进程的LDT的描述符。如下:
/* 填充 GDT 中进程的 LDT 的描述符 */ init_descriptor(&gdt[INDEX_LDT_FIRST], vir2phys(seg2phys(SELECTOR_KERNEL_DS), proc_table[0].ldts), LDT_SIZE * sizeof(DESCRIPTOR) - 1, DA_LDT);
最后再初始化填充TSS以及对应的描述符:
/* 填充 GDT 中 TSS 这个描述符 */ memset(&tss, 0, sizeof(tss)); tss.ss0 = SELECTOR_KERNEL_DS; init_descriptor(&gdt[INDEX_TSS], vir2phys(seg2phys(SELECTOR_KERNEL_DS), &tss), sizeof(tss) - 1, DA_386TSS); tss.iobase = sizeof(tss); /* 没有I/O许可位图 */ ------------加载----------------- xor eax, eax mov ax, SELECTOR_TSS ltr ax
由于进程的各寄存器值如今已经在进程表里面保存好了,现在我们只需要让esp指向栈顶,然后将各个值弹出就行了。最后一句iretd执行以后,eflags会被改变成pProc->regs.eflags的值。我们事先置了IF位,所以进程开始运行之时,中断其实也已经被打开了,这对以后的程序很重要。
restart: mov esp, [p_proc_ready] lldt [esp + P_LDT_SEL] lea eax, [esp + P_STACKTOP] mov dword [tss + TSS3_S_SP0], eax pop gs pop fs pop es pop ds popad add esp, 4 iretd
启动进程(main.c),restart实现ring0->ring1的跳转:
p_proc_ready = proc_table; restart();
运行如下:
【源码】