原来进程是这样切换的(转载)
下面这个结构就是最简陋不过的一个进程表(又称进程控制块,我更习惯于它)。
{
STACKFRAME registers;
int16 ldt_selector;
DESCRIPTOR ldt[LDT_SIZE];
int32 pid;
char pname[32];
}PROCESS;
这个结构的开始是另外一个结构STACKFRAME,用于当时钟中断发生时存储当前进程的各segment
Registers和general
Registers,也就是书里面讲的“保护现场”。而DESCRIPTOR是又一个结构,这个结构用于保存本进程自己的局部描述符表的信息。下面我们就看看这两个结构:
{
int32 gs;
int32 fs;
int32 es;
int32 ds;
int32 edi;
int32 esi;
int32 ebp;
int32 kernel_esp; //popad指令会忽略它,详情见intel手册
int32 ebx;
int32 edx;
int32 ecx;
int32 eax;
int32 retaddr;
int32 eip;
int32 cs;
int32 eflags;
int32 esp;
int32 ss;
}STACKFRAME;
typedef struct s_descriptor
{
int8 limit_low1;
int8 limit_low2;
int8 base_low1;
int8 base_low2;
int8 base_high1;
int8 attr1;
int8 attr2_limit_high;
int8 base_high2;
}DESCRIPTOR;
下面说说当时钟中断的时候首先会发生什么:
1、假定在某个时刻,进程1在运行,突然发生了时钟中断,则系统将立马从ringx(x
> 0)切换到ring0态。那么ring0态的堆栈寄存器从哪里得到的呢?
还有另外一个寄存器tr,它的名字叫任务状态段寄存器,它指向的一块内存区(TSS)中有你手工保存的ring0的ss、esp值,硬件就是自动从那里获得的。之后硬件便自动将ss、esp、eflags、cs、eip这五个寄存器的值压入刚获得的ring0的ss:esp指向的堆栈中,而这个堆栈指向的区域正是当前运行的进程的进程控制块的STACKFRAME结构的底部。因此保存完毕esp就指向了STACKFRAME结构的retaddr处。之后便进入中断处理程序。当然这个retaddr完全可以没有,只是在我实现的时候将保存general
registers的过程写成一个函数,因此这个地方保存这个函数的返回地址。
2、下面看一下中断后发生的事情:
hwint00:
hwint_master 0
它是一个宏,再看看这个宏是什么样子的:
call save
in al, INT_M_CTLMASK
or al, (1 << %1)
out INT_M_CTLMASK, al
mov al, EOI
out INT_M_CTL, al
sti
push %1
call [irq_table + 4 * %1]
pop ecx
cli
in al, INT_M_CTLMASK
and al, ~(1 << %1)
out INT_M_CTLMASK, al
ret
%endmacro
多余的不用管,可以看到调用了save()函数,这个函数的样子是这样的:
pushad
push ds
push es
push fs
push gs
mov dx, ss
mov ds, dx
mov es, dx
mov esi, esp
inc dword [k_reenter]
cmp dword [k_reenter], 0
jne .1
mov esp, stacktop
push restart
jmp [esi + RETADDR - P_STACKBASE]
.1:
push restart_reenter
jmp [esi + RETADDR - P_STACKBASE]
这个函数就是保存各个General
Registers寄存器的值,然后mov esp,
stacktop这一句才真是切换到了内核态的堆栈,然后save()函数结束,宏里的这两句:
call [irq_table + 4 * %1]
这才是真正调用相应的中断处理程序,所以说各种类型的中断发生开始时的处理过程是一样的,之后会根据中断号向中断处理函数句柄数组索引相应的处理函数。
3、我们再看在宏内的最后一条ret指令之前,esp指向的是什么地方:
save()函数里最后几条指令有个push
restart(或者是push
restart_reenter,这个是处理中断重入的,比较类似),看来ret指令正是要执行restart()函数,我们再来看一下restart()函数:
mov esp, [ready_process]
lldt [esp + P_LDT_SEL]
lea eax, [esp + P_STACKTOP]
mov dword [tss + TSS3_S_SP0], eax
restart_reenter:
dec dword [k_reenter]
pop gs
pop fs
pop es
pop ds
popad
add esp, 4
iretd
此时的ready_process指向的便是下一个要运行的进程,lldt指令便是加载这个进程的ldt。之后是将本进程的进程控制块中STACKFRAME结构的底部地址保存入tr寄存器指向的TSS结构,以便下一个时钟中断的时候能够顺利保存现场。
4、下面我们就看看真正的时钟中断处理程序做了什么:
void clock_handler(int irq)
{
if (k_reenter != 0)
{
return ;
}
schedule();
}
可以看出除了判断中断重入外,就是调用调度函数:schedule()。不妨让schedule()函数尽量简单:
{
++ready_process;
if (ready_process >= process_table + NR_TASKS)
{
ready_process = process_table;
}
}
这个调度函数(我斗胆称之调度函数,可它实在太简陋了,但依然很可爱-_-)仅仅是简单的执行进程队列中的下一个就绪进程。
接下来可以完善调度函数!不过那不是现在的事情,我的文件系统还搭起来,毕竟那才是迫在眉睫的。