《ORANGE’S:一个操作系统的实现》读书笔记(十四)进程(二)

上一篇文章记录了进程的说明,完成了第一步从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

再次运行,效果如下所示。

公众号

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值