上一篇文章记录了进程的说明,完成了第一步从ring0到ring1的转移,下面,接着记录进程的完善,打开进程调度的大门。
第二步——丰富中断处理程序
让时钟中断开始起作用
到现在为止,我们还没有打开时钟中断,现在就在i8259.c的init_8259A()中把它打开。
代码 kernel/i8259.c,打开时钟中断。
/* Master 8259, OCW1. */
out_byte(INT_M_CTLMASK, 0xFE);
/* Slave 8259, OCW1. */
out_byte(INT_S_CTLMASK, 0xFF);
为了让时钟中断可以不停地发生而不是只发生一次,还需要设置EOI。
代码 kernel/kernel1.asm,设置EOI。
hwint00: ; Interrupt routine for irq 0 (the clock).
mov al, EOI ; `. reenable
out INT_M_CTL, al ; / master 8259
iretd
EOI和INT_M_CTL定义在include/sconst.inc中。
INT_M_CTL equ 0x20 ; I/O port for interrupt controller <Master>
INT_M_CTLMASK equ 0x21 ; setting bits in this port disables ints <Master>
INT_S_CTL equ 0xA0 ; I/O port for secton interrupt controller <Slave>
INT_S_CTLMASK equ 0xA1 ; setting bits in this port disables ints <Slave>
EOI equ 0x20
运行后发现结果和原来没有任何区别,这是因为我们只是可以继续接受中断而已,其余并没有做什么。不过心里还是不太放心,因为我们不知道中断处理程序到底有没有在运行。因此,可以在中断例程中再添加些内容,以便看到效果。
代码 kernel/kernel.asm,时钟中断处理程序。
hwint00: ; Interrupt routine for irq 0 (the clock).
inc byte [gs:0] ; 改变屏幕第 0 行,第 0 列的字符
mov al, EOI ; `. reenable
out INT_M_CTL, al ; / master 8259
iretd
新添加的代码作用是通过改变屏幕第0行,第0列字符的方式来表明中断例程正在运行。屏幕上这个位置是Boot的首字母“B”,如果发生中断,它会不断变化。运行,效果如下所示。
通过运行可以看到,黑色屏幕的左上角,字符在不断地发生变化,这说明中断处理程序的确是在运行的。
现场的保护与恢复
现在你可能会有一个疑问,就是我们为什么不使用disp_str这个函数显示字符而是用mov指令直接写显存。回想一下我们为什么要使用进程表吧。使用进程表是为了保存进程状态,以便中断处理程序完成之后需要被恢复的进程能够被顺利地恢复。在进程表中,我们给每一个寄存器预留了位置,以便把它们所有的值都保存下来。这样就可以在进程调度模块中使用这些寄存器,而不必担心对进程产生不良影响。
可是在现在这个很短的中断例程中,我们却在事先没有保存的情况下改变了al这个寄存器的值。al很小,但改变它毕竟是有风险的。之所以没用复杂一点的disp_str函数,是为了不会改变更多寄存器的值而产生更大的风险。从程序运行的情况上来看,对al的改变并没有影响到进程的运行,但它仍让我们感到有些担心,现在我们就来把程序改进一下。
代码 kernel3.asm,时钟中断处理程序。
hwint00: ; Interrupt routine for irq 0 (the clock).
pushad ; `.
push ds ; |
push es ; | 保存原寄存器的值
push fs ; |
push gs ; /
inc byte [gs:0] ; 改变屏幕第 0 行,第 0 列的字符
mov al, EOI ; `. reenable
out INT_M_CTL, al ; / master 8259
pop gs ; `.
pop fs ; |
pop es ; | 恢复原寄存器值
pop ds ; |
popad ; /
iretd
从现在开始,书上建议我们每进行一次代码修改都make运行一下,以便看到效果。更改后运行,你让然看到进程的运行以及跳动的字符,效果是没有变化的。
复制tss.esp0
现在中断例程看上去像样多了,寄存器先是被保存,后又被恢复,进程被很好的保护起来。不过,这里还有一个问题,就是中断现在已经被打开,于是就存在ring0和ring1之间频繁的切换。两个层级之间的切换包含两方面,一是代码的跳转,还有一个就是堆栈也在切换。
由ring0到ring1时,堆栈的切换直接在指令iretd被执行时就完成了,目标代码的cs、eip、ss、esp等都是从堆栈中得到,这很简单。但ring1到ring0切换时就免不了用到TSS了。其实到目前为止,TSS对于我们的用处也只是保存ring0堆栈信息,而堆栈信息也不外乎就是ss、esp两个寄存器。在上一篇文章中,我们已经做了一些TSS的初始化工作,并且已经给TSS中用于ring0的ss赋了值,那么tss.esp0应该在什么时候被赋值呢?其实在上一篇文章中记录restart代码的时候就已经涉及到这个问题了。由于要为下一次ring1->ring0做准备,所以用iretd返回之前要保证tss.esp0是正确的。
我们分析过,当进程被中断切到内核态,当前的各个寄存器应该被立即保存(压栈)。也就是说,每个进程在运行时,tss.esp0应该是当前进程的进程表中保存寄存器值的地方,即struct s_proc中struct s_stackframe的最高地址处。这样,进程被挂起后才恰好保存寄存器到正确的位置。我们假设进程A在运行,那么tss.esp0的值应该是进程表A中regs的最高处,因为我们是不可能在进程A运行时来设置tss.esp0的值的,所以必须在A被恢复运行之前,即iretd执行之前做这件事。换句话说,我们应该在时钟中断处理结束之前做这件事。
以下代码给出了实现方法。代码 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
lea eax, [esp + P_STACKTOP]
mov dword [tss + TSS3_S_SP0], eax
pop gs ; `.
pop fs ; |
pop es ; | 恢复原寄存器值
pop ds ; |
popad ; /
add esp, 4
iretd
你可能注意到,在这里不仅增加了给tss.esp0赋值的语句,而且还额外增加了几句代码。你可以看到,sub/add esp这两句代码实际上是跳过了4个字节,结合进程表的定义知道,被跳过的这4字节实际上是retaddr,我们暂时先不管这个值。另外3行mov是令ds和ss指向与ss相同的段。
现在我们的中断例程变成了这样:在中断发生的开始,esp的值是刚刚从TSS里面读取到的进程表A中regs的最高处地址,然后各个寄存器值被压栈入进程表,最后esp指向regs的最低地址处,然后设置tss.esp0的值,准备下一次进程被中断时使用。
如今我们只有一个进程,第二次时钟中断之后对tss.esp0的赋值其实就是在重复。但以后我们会实现多个进程,在进程B或者C将要获得CPU之前,tss.esp0的值会被修改成进程表B或者C中相应的地址。
看到这里你可以能会想,刚开始添加两行设置EOI位的地址代码时中断就已经打开,从那时起就存在了ring0到ring1的切换,可直到现在我们才把tss.esp0的值补全。也就是说,当前面的程序发生ring1->ring0跳转时,esp一定指向了一个错误而且有风险的地方。实际上,是这样的,因为我们在不知道esp指向何处时就使用了它。
内核栈
我们在上篇文章中说到过内核栈,如今这个问题真的出现了。现在esp指向的是进程表,如果此时我们要执行复杂的进程调度程序呢?最简单的例子,如果我们想调用一个函数,这时一定会用到堆栈操作,那么,我们的进程表立刻会被破坏掉。所以我们需要切换堆栈,将esp指向另外的位置。
在引入内核栈时书上曾经提醒过,在具体编写代码的过程中,一定要清楚当前使用的是哪个堆栈,以免破坏掉不应破坏的数据。现在就到了该用内核栈的时候了。
代码 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
mov esp, StackTop ; 切到内核栈
inc byte [gs:0] ; 改变屏幕第 0 行,第 0 列的字符
mov al, EOI ; `. reenable
out INT_M_CTL, al ; / master 8259
mov esp, [p_proc_ready] ; 离开内核栈
lea eax, [esp + P_STACKTOP]
mov dword [tss + TSS3_S_SP0], eax
pop gs ; `.
pop fs ; |
pop es ; | 恢复原寄存器值
pop ds ; |
popad ; /
add esp, 4
iretd
切到内核栈和重新将esp切到进程表都很简单,一个mov语句就够了,但是它却非常关键。如果没有这个简单的mov,随着中断例程越来越大,出错的时候,你可能都不知道错在哪里。
在这里我们尽可能地把代码放在使用内核栈的过程中来执行,只留下跳回进程所必需的代码。我们不妨在这里试一下,把这段打印字符的代码替换成使用disp_str这个函数。
代码 kernel/kernel.asm,修改时钟中断处理。
extern disp_str
...
[SECTION .data]
clock_init_msg db "^", 0
...
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
mov esp, StackTop ; 切到内核栈
inc byte [gs:0] ; 改变屏幕第 0 行,第 0 列的字符
mov al, EOI ; `. reenable
out INT_M_CTL, al ; / master 8259
push clock_init_msg
call disp_str
add esp, 4
mov esp, [p_proc_ready] ; 离开内核栈
lea eax, [esp + P_STACKTOP]
mov dword [tss + TSS3_S_SP0], eax
pop gs ; `.
pop fs ; |
pop es ; | 恢复原寄存器值
pop ds ; |
popad ; /
add esp, 4
iretd
此时,如果运行的话,就可以看到如下效果。我们看到不断出现的字符“^”,说明函数disp_str运行正常,而且没有影响到中断处理的其它部分已经进程A。之所以在两次字符A的打印中间有多个“^”,是因为我们的进程执行体中加入了delay()函数,在此函数的执行过程中发生了多次中断。
中断重入
从开始只有一句iretd的中断处理程序到现在,我们已经增加了许多内容。而且我们知道,在将寄存器的值保存好以及使用了内核栈之后,可以将更加复杂的内容添加进去。但是,现在又出现了另外的一个问题,由于中断处理程序的内容变得越来越复杂,我们是否应该允许中断嵌套?也就是说,在中断处理过程中,是否应该允许下一个中断发生?不允许肯定是不行的,因为你一定不希望在进程调度时你的按键就不再响应。于是我们必须用合适的机制来应付嵌套的情况。让我们修改一下代码,以便让系统可以在时钟中断的处理过程中接受下一个时钟中断。这听起来不是个很好的注意,但是可以借此来做个实验。
首先,因为CPU在响应中断的过程中会自动关闭中断,我们需要人为地打开中断,加入sti指令;然后,为保证中断处理过程足够长,以至于在它完成之前就会有下一个中断产生,我们在中断处理例程中调用一个延迟函数。具体修改代码如下所示。
代码 kernel/kernel.asm,修改时钟中断处理。
extern delay
...
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
mov esp, StackTop ; 切到内核栈
inc byte [gs:0] ; 改变屏幕第 0 行,第 0 列的字符
mov al, EOI ; `. reenable
out INT_M_CTL, al ; / master 8259
sti
push clock_init_msg
call disp_str
add esp, 4
push 1
call delay
add esp, 4
cli
mov esp, [p_proc_ready] ; 离开内核栈
lea eax, [esp + P_STACKTOP]
mov dword [tss + TSS3_S_SP0], eax
pop gs ; `.
pop fs ; |
pop es ; | 恢复原寄存器值
pop ds ; |
popad ; /
add esp, 4
iretd
运行,你会看到,在打印了一个A0x0之后就不停打印“^”,再也进不到进程里面,运行效果如下所示。
之所以会产生这种情况,是因为在一次中断还未处理完成时,又一次中断发生了。这时程序又跳到中断处理程序的开头,如此反复,永远也执行不到中断处理程序的结尾——跳回进程继续执行。而且,由于压栈操作多而出栈操作少,随着时间的继续,当堆栈溢出的时候,意料不到的事情就可能发生了。
中断处理程序是被动的,它只知道中断发生时执行代码,完全不理会中断在何时发生。可是,为了避免这种嵌套现象的发生,我们必须想一个办法让中断处理程序知道自己是不是在嵌套执行。
这个问题并不难解决,只要设置一个全局变量就可以了。这个全局变量有一个初值-1,当中断处理程序开始执行时它自加,结束时自减。在处理程序开头处这个变量需要被检查一下,如果值不是0(0=-1+1),则说明在一次中断未处理完之前就又发生了一次中断,这时直接跳到最后,结束中断处理程序的执行。当然,武断地结束新的中断并不是一个好的办法,但在这里,我们姑且这样来做。
好了,按照这个思路把程序修改一下。
代码 kernel/main.c,k_reenter。
PUBLIC int kernel_main()
{
...
k_reenter = -1;
...
}
然后在中断例程中加入k_reenter自加以及判断是否为0的代码。
代码 kernel/kernel.asm,修改时钟中断处理。
extern k_reenter
...
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 .re_enter
mov esp, StackTop ; 切到内核栈
sti
push clock_init_msg
call disp_str
add esp, 4
push 1
call delay
add esp, 4
cli
mov esp, [p_proc_ready] ; 离开内核栈
lea eax, [esp + P_STACKTOP]
mov dword [tss + TSS3_S_SP0], eax
.re_enter: ; 如果(k_reenter != 0),会跳到这里
dec dword [k_reenter]
pop gs ; `.
pop fs ; |
pop es ; | 恢复原寄存器值
pop ds ; |
popad ; /
add esp, 4
iretd
我们再make一下,运行,效果如下所示。
可以看到,字符A和相应的数字又在不停出现了,这说明我们的修改生效了。而且,你可以发现,屏幕左上角的字母跳动块而字符“^”打印速度慢,说明又很多时候程序在执行了inc byte [gs:0]之后并没有执行disp_str,这也说明了中断重入的确发生了。
好了,我们已经有了一个办法来解决中断重入这个问题,那么让我们注释掉刚才的打印字符已经delay等语句。
代码 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 .re_enter
mov esp, StackTop ; 切到内核栈
sti
push clock_init_msg
call disp_str
add esp, 4
; push 1
; call delay
; add esp, 4
cli
mov esp, [p_proc_ready] ; 离开内核栈
lea eax, [esp + P_STACKTOP]
mov dword [tss + TSS3_S_SP0], eax
.re_enter: ; 如果(k_reenter != 0),会跳到这里
dec dword [k_reenter]
pop gs ; `.
pop fs ; |
pop es ; | 恢复原寄存器值
pop ds ; |
popad ; /
add esp, 4
iretd
再次运行,效果如下所示。
公众号