书上作者通过对Minix代码的阅读和理解,觉得先前的中断处理的有些过于粗糙,而Minix这部分代码不但优雅而且思路清晰,所以,决定参照Minix中断处理方式来对代码进行改造。
代码回顾与整理
首先,我们已经有了一个restart,而且与中断例程的最后一部分基本一致,那么我们就先把这两块合二为一。
首先修改中断例程。
代码 kernel/kernel.asm,修改时钟中断处理程序。
hwint00: ; Interrupt routine for irq 0 (the clock).
sub esp, 4
pushad ; `.
push ds ; |
push es ; | 保存原寄存器值
push fs ; |
push gs ; /
mov dx, ss
mov ds, dx
mov es, dx
inc byte [gs:0] ; 改变屏幕第 0 行, 第 0 列的字符
mov al, EOI ; `. reenable
out INT_M_CTL, al ; / master 8259
inc dword [k_reenter]
cmp dword [k_reenter], 0
jne .1 ; 重入时跳到.1,通常情况下顺序执行
mov esp, StackTop ; 切到内核栈
push .restart_v2
jmp .2
.1: ; 中断重入
push .restart_reenter_v2
.2: ; 没有中断重入
sti
push 0
call clock_handler
add esp, 4
cli
ret ; 重入时跳到.restart_reenter_v2,通常情况下到.restart_v2
.restart_v2:
mov esp, [p_proc_ready] ; 离开内核栈
lldt [esp + P_LDT_SEL]
lea eax, [esp + P_STACKTOP]
mov dword [tss + TSS3_S_SP0], eax
.restart_reenter_v2: ; 如果(k_reenter != 0),会跳转到这里
dec dword [k_reenter]
pop gs ; `.
pop fs ; |
pop es ; | 恢复原寄存器值
pop ds ; |
popad ; /
add esp, 4
iretd
需要注意的是,这里不仅仅是形式上的修改,内容也有了一些变化:原先的程序当发生中断重入的时候是不执行clock_handler的,而现在则总是在执行。所以,我们还需要在clock_handler中稍作修改。
代码 kernel/clock.c,时钟中断处理程序。
PUBLIC void clock_handler(int irq)
{
disp_str("#");
if (k_reenter != 0) {
disp_str("!");
return;
}
p_proc_ready++;
if (p_proc_ready >= proc_table + NR_TASKS) {
p_proc_ready = proc_table;
}
}
当发生中断重入的时候,本函数会直接返回,不做任何操作。在返回前加入打印字符“!”的代码,只是为了发生中断重入的时候可以直观得看到而已。
此时make并运行一下,运行结果与之前并无什么不同。
下面再来修改restart。
为了将来合二为一,我们将它修改得几乎与中断例程中的最后一段一模一样,增加了一行代码和一个标号。需要注意的是,既然在进程第一次运行之前执行了dec dword [k_reenter],就必须把k_reenter的初始值修改一下。
代码 kernel/main.c,修改 k_reenter 的初始值。
PUBLIC int kernel_main()
{
...
k_reenter = 0;
...
}
代码 kernel/kernel.asm,修改时钟中断处理程序。
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
现在如果对比代码restart部分和hwint00部分就可以发现,两段代码的最后部分除了标号的名字不同,其余都是相同的,我们完全可以删除掉其中一段,把hwint00中重复的部分删掉,同时修改用到标号.restart_v2和.restart_reenter_v2的地方。
代码 kernel/kernel.asm,修改时钟中断处理程序。
hwint00: ; Interrupt routine for irq 0 (the clock).
sub esp, 4
pushad ; `.
push ds ; |
push es ; | 保存原寄存器值
push fs ; |
push gs ; /
mov dx, ss
mov ds, dx
mov es, dx
inc byte [gs:0] ; 改变屏幕第 0 行, 第 0 列的字符
mov al, EOI ; `. reenable
out INT_M_CTL, al ; / master 8259
inc dword [k_reenter]
cmp dword [k_reenter], 0
jne .1 ; 重入时跳到.1,通常情况下顺序执行
mov esp, StackTop ; 切到内核栈
push restart
jmp .2
.1: ; 中断重入
push restart_reenter
.2: ; 没有中断重入
sti
push 0
call clock_handler
add esp, 4
cli
ret
; .restart_v2:
; mov esp, [p_proc_ready] ; 离开内核栈
; lldt [esp + P_LDT_SEL]
; lea eax, [esp + P_STACKTOP]
; mov dword [tss + TSS3_S_SP0], eax
; .restart_reenter_v2: ; 如果 (k_reenter != 0),会跳转到这里
; dec dword [k_reenter]
; pop gs ; `.
; pop fs ; |
; pop es ; | 恢复原寄存器值
; pop ds ; |
; popad ; /
; add esp, 4
; iretd
现在我们删除了hwint00中的最后一段代码,并修改了涉及删除代码中标号的两句指令。在这里编译并运行,成功,看到的效果与原先并无区别。
原来长长的中断例程,如今已经被分离出了一个restart。现在我们再来分离save。
代码 kernel/kernel.asm,修改时钟中断处理程序。
hwint00: ; Interrupt routine for irq 0 (the clock).
sub esp, 4
call save
mov al, EOI ; `. reenable
out INT_M_CTL, al ; / master 8259
sti
push 0
call clock_handler
add esp, 4
cli
ret
save:
pushad ; `.
push ds ; |
push es ; | 保存原寄存器值
push fs ; |
push gs ; /
mov dx, ss
mov ds, dx
mov es, dx
mov eax, esp ; eax = 进程表起始位置
inc dword [k_reenter] ; k_reenter++;
cmp dword [k_reenter], 0 ; if(k_reenter==0)
jne .1 ; {
mov esp, StackTop ; mov esp, StackTop <--切换到内核栈
push restart ; push restart
jmp [eax + RETADR - P_STACKBASE] ; return;
.1: ; } else { 已经在内核,不需要再切换
push restart_reenter ; push restart_reenter
jmp [eax + RETADR - P_STACKBASE] ; return;
; }
现在你可能会有一个问题,就是为什么这个save函数和之前不一样?之前使用的函数都是ret指令结尾,跳回调用处继续执行,那是因为函数所使用的堆栈最后都被释放了,调用时call指令的下一条指令地址被压栈,最后ret指令将这条指令从堆栈中弹出,函数调用前后esp的值是一样的。可是我们这的save函数则不同,调用前后esp的值是完全不同的,甚至是否发生中断重入也影响着esp的值。所以我们必须事先将返回地址保存起来,最后用jmp指令跳转回去。
save函数准备好之后,我们继续修改中断例程。
代码 kernel/kernel.asm,修改时钟中断处理程序。
hwint00: ; Interrupt routine for irq 0 (the clock).
sub esp, 4
call save
in al, INT_M_CTLMASK ; `.
or al, 1 ; | 不允许再发生时钟中断
out INT_M_CTLMASK, al ; /
mov al, EOI ; `. reenable
out INT_M_CTL, al ; / master 8259
sti
push 0
call clock_handler
add esp, 4
cli
in al, INT_M_CTLMASK ; `.
and al, 0xFE ; | 又允许时钟中断发生
out INT_M_CTLMASK, al ; /
ret
在这里添加了两段代码,在调用clock_handler之前屏蔽掉时钟中断,在调用之后重新打开。这样,在只打开时钟中断的时候不再会发生中断重入的情况,但是可以预料,当其它中断被打开的时候,中断重入的情况仍然可能出现,我们对它的处理仍然有必要。
现在,我们的时钟中断处理程序已经与Minix的hwint_master差不多了,现在,我们也把它改成一个类似的宏,用它替换原有的宏,并且修改中断例程。
代码 kernel/kernel.asm,修改时钟中断处理程序。
hwint00: ; Interrupt routine for irq 0 (the clock).
hwint_master 0
新的宏如下所示,kernel/kernel.asm。
extern irq_table
...
%macro hwint_master 1
call save
in al, INT_M_CTLMASK ; `.
or al, (1 << %1) ; | 屏蔽当前中断
out INT_M_CTLMASK, al ; /
mov al, EOI ; `. 置EOI位
out INT_M_CTL, al ; /
sti ; CPU在响应中断的过程中会自动关中断,这句之后就允许响应新的中断
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
这里引入了一个函数指针数组irq_table,定义在kernel/global.c中。
PUBLIC irq_handler irq_table[NR_IRQ];
在 include/global.h中加入声明。
extern irq_handler irq_table[];
其中,irq_handler在include/type.h中是这样定义的:
typedef void (*irq_handler) (int irq);
这与我们的clock_handler类型是完全一致的。
NR_IRQ的值定义为16,以对应主从两个8259A,定义在include/const.h中:
#define NR_IRQ 16 /* Number of IRQs */
现在,虽然已经定义了irq_table,但是它还没有被赋值,我们需要有16个函数来初始化它,可目前只有一个clock_handler。不要紧,我们把剩余的元素全部赋值为spurious_irq就行了。
好了,我们现在就来初始化irq_table。这项工作分为两部分,首先将所有的元素初始化为spurious_irq,然后再处理需要单独赋值的元素。
代码 kernel/i8259.c,初始化 irq_table。
PUBLIC void init_8259A()
{
...
/* 初始化 irq_table */
int i;
for (i = 0; i < NR_IRQ; i++) {
irq_table[i] = spurious_irq;
}
}
下面到了单独为irq_table[0]赋值,也就是时钟中断赋值的时候了。先写一个函数put_irq_handler来为irq_table赋值。
代码 kernel/i8259.c,put_irq_handler。
PUBLIC void put_irq_handler(int irq, irq_handler handler)
{
disable_irq(irq);
irq_table[irq] = handler;
}
之所以新添加一个函数来做这项工作,一方面是因为这个操作不能用一条语句完成;另一方面如果再为其它中断赋值的时候直接调用函数也方便。
这里,新添加了一个函数disable_irq和我们马上要用到的另一个函数enable_irq都在kliba.asm中。
代码 lib/kliba.asm,disable_irq和enable_irq。
%include "sconst.inc"
...
global enable_irq
global disable_irq
...
; ========================================================================
; void disable_irq(int irq);
; ========================================================================
; Disable an interrupt request line by setting an 8259 bit.
; Equivalent code:
; if(irq < 8)
; out_byte(INT_M_CTLMASK, in_byte(INT_M_CTLMASK) | (1 << irq));
; else
; out_byte(INT_S_CTLMASK, in_byte(INT_S_CTLMASK) | (1 << irq));
disable_irq:
mov ecx, [esp + 4] ; irq
pushf
cli
mov ah, 1
rol ah, cl ; ah = (1 << (irq % 8))
cmp cl, 8
jae disable_8 ; disable irq >= 8 at the slave 8259
disable_0:
in al, INT_M_CTLMASK
test al, ah
jnz dis_already ; already disabled?
or al, ah
out INT_M_CTLMASK, al ; set bit at master 8259
popf
mov eax, 1 ; disabled by this function
ret
disable_8:
in al, INT_S_CTLMASK
test al, ah
jnz dis_already ; already disabled?
or al, ah
out INT_S_CTLMASK, al ; set bit at slave 8259
popf
mov eax, 1 ; disabled by this function
ret
dis_already:
popf
xor eax, eax ; already disabled
ret
; ========================================================================
; void enable_irq(int irq);
; ========================================================================
; Enable an interrupt request line by clearing an 8259 bit.
; Equivalent code:
; if(irq < 8)
; out_byte(INT_M_CTLMASK, in_byte(INT_M_CTLMASK) & ~(1 << irq));
; else
; out_byte(INT_S_CTLMASK, in_byte(INT_S_CTLMASK) & ~(1 << irq));
enable_irq:
mov ecx, [esp + 4] ; irq
pushf
cli
mov ah, ~1
rol ah, cl ; ah = ~(1 << (irq % 8))
cmp cl, 8
jae enable_8 ; enable irq >= 8 at the slave 8259
enable_0:
in al, INT_M_CTLMASK
and al, ah
out INT_M_CTLMASK, al ; clear bit at master 8259
popf
ret
enable_8:
in al, INT_S_CTLMASK
and al, ah
out INT_S_CTLMASK, al ; clear bit at slave 8259
popf
ret
现在在kernel_main()中指定时钟中断处理程序。
代码 kernel/main.c,时钟中断指定。
PUBLIC int kernel_main()
{
...
put_irq_handler(CLOCK_IRQ, clock_handler); /* 设定时钟中断处理程序 */
enable_irq(CLOCK_IRQ); /* 让8259A可以接收时钟中断 */
...
}
这两行不但指定了时钟中断处理程序,而且让8259A可以接收时钟中断。既然我们用disable_irq和enable_irq这两个函数来控制8259A对中断的接收情况了,那就应该在init_8259A()中屏蔽8259A的所有中断:
out_byte(INT_M_CTLMASK, 0xFF);
到此为止,代码的修改就告一段落了。注意添加相应的函数声明,make,运行,效果如下所示。
运行结果虽然跟原先基本相同,但是现在的代码不但更有条理,而且更易于扩展。实际上,现在我们完成的绝不仅仅是一个时钟中断处理程序而已,同时也是一套方便扩展的中断处理的接口。现在,若想添加某个中断处理模块,只需要将完成中断处理函数的入口地址赋给irq_table中相应的元素就可以了。这个函数可能仍然非常底层,但已经可以完全用C语言编写。
应该说,到这里才真正算是里程碑式的成果。现在的Orange’s已经可以随意地添加进程的数目,已经预留了足够方便地中断处理接口。也就是说,虽然它仍算不上是完整意义上的操作系统,但是一个粗糙的框架已经形成了。好了,现在我们回忆一下我们的操作系统是怎样运转起来的吧。
如果顺着这个图表把整个过程重新过一遍的话,你可能发现,涉及的代码并没有感觉上那么多,但是,要彻底把它写出来却并不是一件容易的事情,其中最困难的就是时钟中断处理程序围绕进程表项进行进程切换的过程,这一点我们已经深有体会。不过我们的系统还是非常简陋的,简陋的原因在于我们在进程本身方面考虑的还比较少,比如,未曾考虑进程优先级的问题,未曾考虑进程间通信的问题等。现在让我们对照上面的图表来想像一下, 如果要增加这些内容的话,应该是怎样的情形。
切换到Kernel的GDT的代码通常情况下是不需要改动的,init_prot()中的init_8259A()看上去是比较稳定的代码,kernel_main()结构很简单,但由于在这里初始化了进程表,所以若对进程功能进行扩展的话,会有一些改动。我们在本来就不多的代码中,只想到这一处地方可能会在进程功能扩展时有所改动,这真是一件令人兴奋的事情,因为这意味着,即便目前的工作告一段落,等到下次想要进一步完善它的时候,上手也会比较容易,因为接口已经足够简单。
公众号