【进程续】

http://hi.baidu.com/6121017/blog/item/529f3916dae89d54f3de32f8.html

【进程续】

一、进程的启动:

现在我们已经熟悉了启动一个进程的需求和主要步骤,它的动作也就是:

×准备IDT、TSS、LDT(该LDT描述符在GDT中。但是LDT局部的描述符在进程表的ldts[LDT_SIZE]字段中.)

初始化IDT,也就是定义好GATE描述符。将其0门描述符对应IRQ0(VECTOR),也就是按386模式规定的中断异常处理机制。接着自己定义0x20向量对应8259A的主片IRQ0,0x28对应8259A的从片IRQ8.这需要自己对8259A进行初始化。写入ICW1-ICW4,并且设置可屏蔽中断寄存器OCW1,OCW2(是用作EOI表示中断处理完毕)。

初始化TSS比较简单,直接用0填充这个全局TSS结构体。然后设置tss.iobase=sizeof
(tss)表示TSS没有IO许可位图,tss.ss0=SELECTOR_KERNEL_DS(因为堆栈也属性数据段).这是Ring0用的ss选择子。在时钟中断发生后就会用这个。

初始化LDT,首先要初始在GDT当中的LDT描述符。因为进程的LDT描述符是归GDT管理的。它描述着LDT局部描述符的段基地址和段界限。并且属性要加上DA_LDT。至于LDT内部的描述符就等在进程表中实初始化。因为它是属于进程表的一部分。

×准备进程表

当IDT、TSS、以及LDT都准备好的时候,那么就来初始化进程表:

首先初始化进程的ldt_sel=INDEX_LDT_FIRST,它就是LDT在GDT中的选择子。当实现进程的局部代码与数据访问时候需要用到这个选择子。那么接着就来初始LDT内部的段,这里属于进程的句柄段就定义了两个:一个代码段、一个数据段、而且初始化方法是直接从GDT中复制了代码段、数据段描述符到&ldts[0],这就代表了LDT内部的代码段与数据段也是平坦的0-4GB模式。不过这里还要改一下属性,给代码段加上DA_C
| DA_PRIVILEGE_TASK << 5,因为先前的代码段DPL是0。而这是LDT的代码段是属于进程的当然要比内核DPL要低一点了。
同理数据段也修改为DA_PRIVILEGE_TASK << 5.

上面是初始化了LDT内部描述符段。也就进程专用的代码、以及数据段描述符。那么如果想要使用上述的描述符当然少不了要将CPU段选择子对上LDT内部的描述符了。

接下来在进程表里初始化regs的段寄存器字段,

(0*8表示的就是代码段在LDT中的位置,因为LDT不像GDT是以空descriptor开始的)。

//这个SA_RPL_MASK、SA_TI_MASK是掩码。其作用是将选择的子RPL、TI位清除掉。其他位当然全是1.

regs.cs = ((8 * 0) & SA_RPL_MASK & SA_TI_MASK) |SA_TIL | RPL_TASK

//以下也好理解,就是LDT的数据段,ds、es、fs、ss都是用这个平坦的数据段来访问数据。这个也不足为其了吧。那么进程的整个寻址方式除了gs访问显示器是用GDT以外,其他的可就都是LDT模式的了,进程的DPL,RPL都是TASK。要说明的是只有当执行了lldt
ldt_sel指令后进程才会是知道LDT的具体位置。这个指令会在初始化进程表后执行的。

regs.ds = ((8 * 1) & SA_RPL_MASK & SA_TI_MASK) |SA_TIL | RPL_TASK

regs.es = ((8 * 1) & SA_RPL_MASK & SA_TI_MASK) |SA_TIL | RPL_TASK

regs.fs = ((8 * 1) & SA_RPL_MASK & SA_TI_MASK) |SA_TIL | RPL_TASK

regs.ss = ((8 * 1) & SA_RPL_MASK & SA_TI_MASK) |SA_TIL | RPL_TASK

regs.gs = SELECTOR_KERNEL_GS & SA_RPL_MASK | RPL_TASK

//到这里已经把进程的选择子全部搞定了.那么你一定想到了,剩下的就是运行时需要的偏移寄存器了.

regs.eip = TestA          
//这只是一个偏移值。它的CS段选择子对应的描述符有基址;需要注意的是现在CS是LDT代码段。虽然访问时候地址跟内核访问地址是相等的,但是它的DPL、CPL都变成TASK级别的了。

regs.esp = STACKTOP + STACK_SIZE_TOTAL //这个也是LDT的esp访问时对应regs.ss

regs.eflags = 0x1202   
//进程表中eflags的值是:IOPL=MASK(这样进程才能正常使用I/O指令).IF=1(开启时钟中断),

p_proc_ready = proc_table
//proc_table是整个进程表的数组的基址,那么上面的赋值就是第一个进程表的字段,现在将基址给了p_proc_ready.
p_proc_ready是用来在中断时给esp赋值用的。那么它就是代表当前需要激活的进程的进程表栈尾。

×启动第一个进程

restart() ;       
//这个函数就是启动第一个进程的关键。它就是将上面的进程表数据全部写入到CPU。要知道这个时候CPU还是运行在Ring0内核模式的,。因为现在正在启动第一个进程。那么代表处于是Ring0内核代码.经过restart()的最后一条
iretd后。那么就进入了进程任务模式了。这个时候会随着时钟中断的来临 压栈ss ...eip寄存器到Tss
的esp0中.那么我们在restart()函数里就把第一个进程表的regs栈顶付給了Tss 。mov esp,p_proc_ready       
;第一个进程表的栈尾。

mov eax,[esp + P_STACK_TOP]    

mov dword [tss+ TSS3_S_ESP0],eax

那么现在从进程(Ring1)到时钟中断(Ring0),它就可以找到对应的esp0堆栈。并且保存ss...eip到进程表.再从iretd时钟中断返回时,又可以从当前的esp值逆操作回去?OK这是一个进程。其实没多少改变只是进程在运行时,总是有时钟中断来参与。

二、时钟中断处理

进程的重点就在时钟中断,下面就来细细的了解时钟中断:

在上一个程序时钟中断代码处理是:

hwint00:

iretd;         是这么的直接、武断。
可以看到时钟中断的代码就是直接iretd、那么此时已经经过了Ring1->Ring0,esp当然就是Tss 中指定的esp0了,中断发生时push
ss...eip到esp、而现在又直接iretd。也没有发送0x20到8259A的0x20或者0xA0端口结束中断处理让8259A继续接收中断。那么现在的8259A系列的硬件中断就不会再产生了。也就是进程只被打扰了一次又继续执行死循环了。

现在我们就来改善一下时钟中断的处理,让时钟中断一直参与进程的运行。

hwint00:

 inc byte[gs:0]      ;在显示器第0行0列的缓冲区ascii码++

 mov     al,0x20

 out    INT_M_CTL,al    ;
告之8259A中断处理完毕,这个时候IF=1就可以继续接收8259A中断了.

iretd

将中断改成这样后,当那么时钟中断就会参与到进程的运行了。当进程运行到某个时刻时钟中断发生!程序跳到hwint00
然后在第0行0列缓冲区++(ascii),接着告之8259A中断处理完毕,iretd返回到进程。那么这个时候又会发生时钟中断了。进程简单的实现了按时钟中断间隔无限的切换着自己的进程表。

可是到这里大家有没有发现我们改变了al的值,在中断处理代码里面我们还没有保存所有常用寄存器。只是保存了iretd要用到的寄存器。那么这样当然不完善了,毕竟我们还需要在内核进行调度实现多进程!下面就再改下代码:

hwint00:
   sub esp,4      ;esp->retaddr
   pushad         
;保存eax...ebp到进程表里 其中保存了retaddr字段的地址到esp
   push ds          
   push
es            
   push fs
   push gs    ;保存ds...gs到进程表里
   mov
esp,StackTop          
;.bss节的堆栈数据
 
;--------------保存了进程寄存器后就进行内核调度了-------------------  
   mov dx,ds             ;这些寄存器是LDT模式的,可以放心的改变他们了。
   mov es,dx           
;因为已经将他们的值保存在中断发生时的进程的进程表里。
   mov fs,dx

   inc BYTE [gs:0]
   mov al,0x20
   out INT_M_CTL,al          

push    colck_krn_msg    ;注意此时的esp已经是StackTop 内核专业的,这下可以放心的使用了

call disp_str

add esp,4

;-------------恢复到进程表栈-,准备转向下一个进程,初始化tss esp0-----
mov
esp,[p_proc_ready]  
lea eax,[esp + P_STACKTOP]     ;当前进程表的REGS栈顶
mov DWORD [tss + TSS3_S_SP0],eax ;放到Kernel的TSS esp0中,这样在中断发生时就能正确保存到当前进程表.
   pop gs
   pop fs
   pop es
   pop ds
   popad
   add esp,4  
;跳过kernel_esp
   iretd
重要知识点:每个进程在运行时,tss.esp0应该是指向当前进程的进程表regs栈顶的。这样才能在时钟中断发生时,压栈
ss..eip到当前进程的进程表里去。也就是说如果要启动一个进程。那么必需在启动之前指定tss.esp0=当前进程的进程表regs栈顶位置。

×中断重入问题

也就是中断嵌套的处理,这里处理起来比较简单:

先看时钟重入的问题:{

sti                      ;sti和cli包起来的代码就是时钟中断的嵌套,当sti
(IF=1)那么时钟中断被打开,这个时候内核调度的代码在执行时有可能就会被打断.

call delay         
;如果这个时候调用一个延迟的函数,第二次时钟中断发生了,就会循环到sti,那这个时候,就会发生时钟中断的死循环、为了解决这个问题。我们设计一个全局变量来标志时钟中断是否嵌套.

cli

}

解决:{

inc     dword [k_reenter]      ;它的初始值是-1,当第一次发生中断后,就变成了0

cmp dword [k_reenter],0

jne       .re_enter           ;如果不是0表示那么就证明第一次中断还没完成又出现中断了。重入发现!

mov esp,StackTop   ;内核堆栈

sti

cli

.re_enter:       

dec dword [k_reenter]    
;时钟中断嵌套后直接返回到时钟处理的代码。在时钟中断嵌套的时候使用的是内核堆栈,这个已经设计好了。当第一次中断时,sti前面就已经将esp
指向了内核栈。

......

iretd

}

三、添加一个进程:

第一个进程需要的是:进程表、进程体、GDT(LDT)、TSS,那么就可以:

定义一个给进程任务结构体(方便初始化进程表):

typedef struct s_stack{

t_pf_task   initial_eip;     //typedef  
void(*t_pf_task)();如果想给initial_eip赋值,那么必需函数要是t_pf_task类型,

int         stacksize;       //LDT里的堆栈大小

char          name[32]; //进程的名字

}TASK;

初始化这个这个任务表:

EXTERN TASK task_table[NR_TASKS] ={

TestA,STACK_SIZE_TESTA ,"TestA,\

TestB,STACK_SIZE_TESTB ,"TestB",\

};这里定义了两个任务体,其实也就是两个进程的函数入口与堆栈代码加进程名。

那么可以用这个结构体初始化进程表;

p_proc=proc_table;

p_task = task_table; //一个任务结构体对应一个进程表

char* p_task_stack=task_stack + STACK_SIZE_TOTAL; 堆栈自顶向下

t_16 selector_ldt = SELECTOR_LDT_FISR;

for(int i=0;i<NR_TASKS;i++)

{  p_proc->ldt_sel = selector_ldt;

     p_proc->regs.esp = p_task_stack;

     p_task_stack-= p_task->stacksize ;调整进程的堆栈。

      selector_ldt += 1 << 3;

      p_proc++;

      p_task++;

}那么进过了NR_TASKS次循环后。就把所有进程都给初始化了;这个时候再启动第一个进程:

p_proc_raedy = proc_table;

mov dword [k_reenter],-1

restart();

;在时钟中断发生时需要调整p_proc_ready让它指向下一个需要运行的进程的进程表栈顶。因为进程的切换是随着时钟中断发生的、。

p_proc_ready++;

if(p_proc_ready >= proc_table + NR_TASKS){

//如果已经是最后一个进程在运行了,那么让p_proc_ready指向第一;

p_proc_ready =proc_table ;//这样就彻底解决了 进程的切换问题了;

}借用前辈一句话:一个进程到两个进程就是质的飞跃,两个到三个是量的增加而以!!!.

四、认识8253可编程控制器PIT(programmable interval timer):

×IIntel8253时钟中断:

8253芯片有3个计数器(Counter).他们都是16位的.各有不同的作用。时钟中断是由Counter0产生的!

计数器的原理是这样的:它有一个输入率,在PC上是1193180Hz(1193180次/秒),在每一个时钟周期(CLK
cycle),计数器值会减1.,当减到0时就会触发一个输出。由于计数器是16位最大值是65535,因此默认的时钟中断的发生频率是1193180
/65535==18.2Hz.(18.2次/s)。,值越小时间就越快,也就是说最小精确到1/1193180秒每次。(1193180次/秒)。如果想每10ms产生一次中断那么用这个公式n/1193180=10/1000(n就是次数).n=1193180*1/100;=11931次.。

我们可以用8253可编程控制来控制Counter0.需要操作的端口就是0x40,在操作Counter0之前需要先通过0x43端口写8253的模式控制寄存器。

out     0x43,0x34 ;     //设置8253模式

out      0x40,(t_8)1193180/100; //将Counter0赋值 分别是低字节和高字节

out    0x40,(t_8)(1193180/100 >> 8);//当这里后时钟中断就是10ms一次了。

【总结】

×时钟中断与进程:

经过不段改善设计成一套比较泛型的进程调度:

进程调度主要就是在时钟中断这块。就从启动第一个进程的时候开始讲:

以下的代码片段都是Kernel.asm里面的:

hwint00:         ;时钟中断发生的时候通过IDT GATE指定到这

call    save             ;push eip 非重入的时、 regs.retaddr=++eip
; 这个时候的esp是指向内核栈的。并且[esp]是 (save) push的地址。

in      al,INT_M_CTLMASK

or           al,1                    

out         INT_M_CTLMASK,al     
;这3条指令是将时钟中断屏蔽掉,不再接收时钟中断.

mov      al,EOI

 out      INT_M_CTL,al          ;让8259A继续接收其他没有屏蔽的中断,当IF=1时!

 sti

push     0

call       clock_handler    
;这里进行调度。将p_proc_ready指向下一个要运行的进程.

add      esp,4

 cli

;  把屏蔽的IRQ开打,已经处理完毕就不会重复了

 in      al,INT_M_CTLMASK

 and           al,~0x01                    

 out         INT_M_CTLMASK,al     
;这3条指令是将时钟中断屏蔽掉,不再接收时钟中断.

ret               ;返回到相应的函数进行切换进程或者原位返回/

save:        这个函数主要保存寄存器,判断中断重入问题

pushad

push     ds

push     es

push     fs

push     gs

mov    eax,esp                ;临时保存进程表的开始位置。

 inc     dword[k_reenter]

 jnz         .1                ;如果不等于0表示已经出现中断重入现象

 mov    esp,StackTop        ;调度时要转换到内核栈

push         restart

 jmp     [eax + RETADR - P_STACKBASE] ;这个正是字段regs.retaddr

.1:

 push         restart_reenter         ;将重入代码基址压到内核栈

 jmp     [eax + RETADR - P_STACKBASE] ;这个正是内核的regs.retaddr
偏移位置,并不是进程表的字段,不过这样也在内核栈模拟了进程表的内容。效果一样都是跳回call 下一条指令mov dword

restart:                     ;非重入的时候,从这里一直往下走.,进行切换进程

 mov esp,[p_proc_ready] ;指向下一个启动的进程表开始字段

 lldt [esp + P_LDT_SEL] ;从进程表中获取LDT选择子

lea       eax,[esp + P_STACKTOP]   ;得到进程表的栈顶,指向regs.ss字段

mov   [tss + TSS3_S_SP0],eax  
;当中断再次发生时,就会将寄存器环境保存到esp0.

restart_reenter:        ;中断重入的时候,使用的是内核栈,那么现在就返回去。

dec dword [k_reenter]      
;此时IF=1后就可以正常接收当前向量号的中断了。这样避免了重复的中断

pop              ds

 pop              es

 pop              fs

 pop              gs

 add       esp,4

retd                         ;切换到进程。

进程的切换方式也就是(都是在中断下进行的):

保存寄存器信息到进程表或者内核栈(中断重入)-->屏蔽当前处理的中断IRQ-->(sti)接收8259A其他的IRQ-->内核调度-->关闭中断(cli)-->切换进程或者返回继续指向中断(重入时,)这可以看做是一个循环!!!

你可能感兴趣的:(进程)