%macro hwint_master 1
call save
in al, INT_M_CTLMASK
or al, (1 << %1)
out INT_M_CTLMASK, al
mov al, 20h
out 20h, al
sti
push %1
call [irq_table+4*%1] ;调用真正的处理函数
add esp, 4
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 + RETADR - P_STACKBASE]
.1:
push restart_reenter
jmp [esi + RETADR - P_STACKBASE]
restart:
mov esp, [p_proc_ready]
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
//时间中断的真正处理函数
PUBLIC void clock_handler(int irq)
{
//disp_str("#");
ticks++;
p_proc_ready->ticks--;
if(k_reenter!=0)
{
//disp_str("!");
return;
}
if (p_proc_ready->ticks > 0) {
return;
}
schedule();
}
分析:Tinix操作系统目前的运行状态分为内核态(ring0)和任务态(ring1),ring1是进程代码,然后这些进程的内核对象即进程表是在ring0下访问的,进程表就是一个结构体数组,每个结构体保存了进程上次运行时各个寄存器的状态,包括gs,fs,es,ds,edi,esi,ebp,ebx,edx,ecx,eax,eip,cs,eflags,esp,ss,这些寄存器在tinix_main(运行在ring0下)中进行了初始化,其中eip指向进程的入口函数,即TestA,TestB,TestC,esp指向各自不同的栈空间,并且p_proc_ready = proc_table,p_proc_ready 即下一个将要运行的进程的内核对象,tinix_main最后会调用restart,restart会将下一个将要运行的进程的内核对象中的regs地址赋给tss的sp0,这样进行运行发生中断时,堆栈要切换到ring0,栈指针sp从tss的sp0读入,这样中断处理程序就可以刚好在内核对象中保存上述寄存器了(严格来说eip,cs,eflags,esp,ss是CPU自动压栈的),restart最后执行iretd指令就恢复进程的运行了。
下面看一下中断处理的流程,进入中断处理程序后,中断屏蔽位是开着的,这是不会发生硬件中断,进入save后通过pushad、push es,push fs,push gs把之前的进入中断之间的寄存器状态保存起来,不过不是中断重入的话保存的位置就是进程内核对象,保存完以后需要将sp指向内核堆栈,以免以后的堆栈操作破坏进程结构;如果是中断重入,保存的位置就是内核栈的某个位置(中断重入的情况不会发生堆栈切换),从call save返回后,中断处理程序首先关闭掉了同类的中断,也就是说在时钟处理过程中是不会发生时钟中断的中断重入的,同理键盘中断的处理过程中叶不会发生键盘中断重入。然后激活EOI,开中断,这时候别的中断可以发生了。接下来调用 call [irq_table+4*%1]来进行实际的中断处理,接下来要关闭中断,通过操作EOI重新打开同类中断,ret指令后,如果是中断重入,程序执行到restart_reenter,直接重中断返回;如果不是中断重入,执行到restart,此时需要将堆栈切换到进程内核对象处,因为进入中断前的寄存器状态保存在内核对象处。通过iretd指令从中断返回后中断又重新打开。
上述流程保证了中断处理函数在执行时同类的中断不会发生,我们来看看时钟中断处理函数clock_handler,只有不是中断重入的时候才会切换进程,否则什么都不做。