分类:
2010-06-28 14:01:27
【进程】
一、 多进程运行
何谓进程。进程就是指某个时刻CPU执行的一个任务。该任务有独立的CPU环境、以及资源内存等数据。
在单任务的DOS系统时,所有任务的资源、内存、CPU环境都是共享的,只要其中某个任务改变了这些共享的数据,那么所有任务的环境就都跟着变了。这样的操作实在不好控制,如果想要每个进程都拥有独立的资源、内存、CPU环境。该如何去实现呢?这里就要对进程进行调度,简称进程调度, 这是多任务操作系统的特性!!!
在多大数情况下PC机上只有一个CPU,那么就代表同一时刻只能有一个进程处于正在运行状态。另外一个进程处于等待状态。
假设有进程A(正在运行)、进程B(处于等待)。那么当进程A切换到进程B的时候。这个时候进程B处于正在运行状态,而进程A处于等待状态。
这个时候进程B初始化自己CPU等环境,那么进程A的CPU等环境已经被进程B破坏了。现在如果再想回到进程A该怎么办呢?对于这个问题我需要提前就要解决好!
首先进程A需要拥有自己的代码、数据、堆栈。 进程B同样也需要有自己的代码、数据、堆栈。
假设这个时候处于运行状态的进程A要切换到处于等待状态的进程B。那么首先要对自己的环境进行保护,也就是保存进程A当前的状态挂起进程A, 进行进程调度。然后唤起进程B。这个时候进程B处于正在运行状态,而进程A处于等待状态。 整体来说也就是:
1、进程A处于运行状态,
2、时钟中断发生,从进程A到Ring0,时钟中断处理程序(保存环境到进程表),
3、进程调度,指定下一个将要运行进程B.
4、等待中的进程B被恢复,Ring0->进程B,启动进程B
5、进程B处于运行状态,
如果想要实现这些功能那么必需:
×时钟中断处理程序
×进程调度模块
×两进程
在切换进程时要对当前进程的环境进行保存,保存在进程表结构体中(Process Table).一个进程对应一个进程表。那么多个进程当然就对应一个进程表数组(array)了.
二、内核栈与进程栈
当寄存器的值被保存到进程表后,就要开始执行进程调度了。
但是现在的esp指向进程表的某处。进程调度也要用到堆栈,那么如果现在进程调度引用了esp,那么就会破坏存放好的进程表信息了。为了解决这个问题需要在进程调度前将esp指向专门的内核栈区域。
在进行进程调度之前:esp 首先是指向ring1的堆栈的(也就是进程自己的堆栈),接着跳入Ring0进行中断处理这个时候的esp是指向进程表的。Ring0 与Ring1的堆栈信息在TSS中指定, 保存好进程表信息后。接着进行进程调度。这时应该让esp先指向内核专用的堆栈。不应该让它继续指向进程表。
那么进行进程调度要用到的数据也就是:
×进程栈 -----------------------属于进程的数据段
×进程表栈 -----------------------存储进程状态信息的数据结构
×内核栈。----------------------进程调度模块运行时专用栈
以上三个是在进行进程调度主要用到得数据。
三、进程调度
接下就那实实在在的代码来说进程调度:
首先在Kernel.asm加入如下代码:
;这第一个进程的入口处。
; ====================================================================================
restart: ;中断进来时会push ss esp eflags cs eip,所以中断之前esp一定要对应一个进程表的topStack
mov esp, [p_proc_ready] ;重定向esp到栈尾,准备从进程表弹出信息到CPU.
lldt [esp + P_LDT_SEL] ;加载进程表里指定的LDT
lea eax, [esp + P_STACKTOP]
mov dword [tss + TSS3_S_SP0], eax ;将当前进程表栈顶信息保存在TSS Ring0 esp0 ss0,用于下次中断时能找到此进程表的位置.
;..........从进程表中弹出regs到CPU环境...这里还没有使用内核栈
pop gs
pop fs
pop es
pop ds
popad
add esp, 4 ;跳过retaddr字段
iretd ;正式启动进程表描述的进程
以上代码是对进程表的操作,这只是进程调度的一部分。
下来就一步一步来看看程序的走向,就接着Kernel入口出分析:
从Loader.bin跳到Kernel.bin后,Kernel是被装载在0x30400的地址处.还记得Kernel.asm处的_start:吗?对它就是导出的入口:
_start:
mov esp,TopStack ;重定义栈顶指向属于内核本身的堆栈
mov dword[disp_pos],0 ;初始化显示缓冲位置
sgdt [gdt_ptr] ;这个是Kernel全局变量,在这里是导入进来使用的.目的是将Loader.bin的GDT传给Kernel.bin
call cstart;既然已经传给gdt_ptr了那么当然由start.c代码区处理一下,复制到Kernel.bin专用的地方
lgdt [gdt_ptr] ;这时的gdt_ptr已经被cstart修改过了,是属于Kernel专用的GDT指针了。
lidt [idt_ptr] ;同样这个idt_pt r也是Kernel全局指针变量。初始工作在cstart里做好了。这个时候所有的中断VECTOR都已经对应上了相应的中断处理代码。
jmp SELECTOR_KERNEL_CS:csinit ;第一次使用Kernel专用的GDT进行跳转
csinit:
xor eax,eax
mov ax,SELECTOR_TSS ;这个是Kernel的GDT里的任务堆栈段(TSS),用来控制Ring3-Ring0时esp ss的重定位。
ltr ax ;现在可以定位了。
jmp tinix_main ;这里用jmp 而不是call,就代表从这里跳出去后就不用回来了。直接用IDT控制,tinix_main函数在main.c文件里面。它是启动第一个进程的函数。
以上就是Kernel.asm要运行的代码,在这块代码里有两个重要的跳转,那么就从第一个跳转开始:
call cstart ;
;------------------------------------------start.c------------------------------------------
PUBLIC void cstart(){
cstart()它的主要功能是:
×将Loader.bin的GDT,COPY到Kernel.bin.使gdt_ptr正确指向Kernel里的全局变量GDT结构体.
×.将idt_ptr这个IDT指针初始化。使它正确指向Kernel里的全局变量IDT结构体.
×在init_prot函数里初始 IDT、TSS、LDT。
到这,cstart()的使命也就完成了,
}
接下来看第二个跳转tinix_main;
PUBLIC void tinix_main{
在cstart() 函数已经把进程需要的LDT(进程)、TSS(内核)、给初始化了。
那么接下就剩下进程表的初始化工作了:
PROCESS * p_proc = proc_table; //定义临时指针变量指向进程表数组基址
p_proc->ldt_sel = SELECTOR_LDT_FIRST ; //初始化第一个进程的LDT选择子到进程表.
//初始化第一个进程的LDT局部代码描述符。它也是进程表的一部分。也就是代表进程的cs值了.
memcpy(&p_proc->ldts[0],&gdt[SELECTOR_KERNEL_CS >> 3],sizeof (DESCRIPTOR));
p_proc->ldts[0].attr1 = DA_C | PRIVILEGE_TASK << 5;
//初始化LDT局部数据段..进程ds的值
memcpy(&p_proc->ldts[1],&gdt[SELECTOR_KERNEL_DS >> 3],sizeof (DESCRIPTOR));)
p_proc->ldts[1].attr1 = DA_DRW | PRIVILEGE_TASK << 5;
//以下是给进程对应的段选择子初始化。SA_RPL_MASK 、SA_TI_MASK是掩码。也就是对应的位全是1
p_proc->regs.cs=((8*0)& SA_RPL_MASK &SA_TI_MASK) | SA_TIL | RPL_TASK;
....
p_proc->regs.gs = (SELECTOR_KERNEL_GS)& SA_RPL_MASK |RPL_TASK;
p_proc->regs.eip=(t_32)TestA; eip指向函数偏移。
p_proc->regs.esp =(t_32)task_stack + STACK_SIZE_TOTAL;//属于进程的堆栈偏移
p_proc->eflags= 0x1200 ;IOPL = 1 IF = 1;开中断
p_proc_ready = proc_table;
restart(); //这里跳进Kernel.asm 里的代码.那么这个时候会将进程表的信息读取到CPU环境里。那么当执行到iretd的时候,也就是从进程表读出了.eip即TestA函数的地址到eip这个时候就运行了TestA函数。而现在中断也已经开启了。那么当时钟中断发生后:
CPU首先 push ss,..push esp.push eflags push cs push eip,,这个时候用的esp是tss中指定的。而在调用restart时,就已经将进程表REGS栈顶位置给tss esp0了,所以此时push ss...是存到了进程表里。
然后跳到hwint00:
iretd ;这里直接一个中断返回指令。那么也就是pop eip..... 那么也就相当于push 到进程表。又从进程表pop..
那么也就是说这个进程执行到某一个时刻时,时钟中断发生。将当前的ss、esp、eflags、cs 、eip保存到进程表。因为TSS里面的Ring0 esp0就是为进程表栈准备的。 这里进行中断处理代码时,并没有做太多的事情。而是直接将这些寄存器从进程表里弹出到CPU环境。这样又恢复了进程的执行状态。那么就一直这样随着时钟中断的产生。停止进程,然后再激活进程。
}tinix_main()它的主要功能是:
×初始化进程表的数据:
×调用restart函数将进程表的数据读到CPU
×时钟中断已经打开,这个时候eip指向TestA.那么就开始执行TestA.时钟中断会一直暂停TestA (保存iretd的参数到进程表),然后再恢复TestA(iretd)。
【总结】:
hi.baidu.com里面发个文章怎么限制篇幅啊。。晕。。算了就写到这吧。。在简单说下程序来龙去脉:
一、×从Kernel.asm进入到cstart():
cstart函数对GDT、IDT、TSS、LDT(第一个进程)进程初始化、
二、×从cstart()返回来了、调用tinix_main():
tinix_main()做的工作也就是进程表数据的初始化。。初始化完成后就是一个restart().将进程表数据弹出到CPU.那么最后一个iretd。它是一个特殊指令。因为它可以把esp指向的内容弹出到eflags eip.那么现在eip的值就是TestA()函数的开始。这个eflgas改变了。IF=1了。有时钟中断了。TestA会被不断打扰又不断恢复。。
这里只实现了一个进程的时钟中断。不过如果要实现两个进程原理基本差不多啦。。只要改变进程表栈位置就行。
OK完毕。