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)-->切换进程或者返回继续指向中断(重入时,)这可以看做是一个循环!!!