《自己动手写操作系统第六章》引入minix中断处理方式

时间:2022-03-18 14:36:27
摘要:回过头来看,我们发现我们的中断处理程序写的并不够优雅。中断被响应需要三个条件:Eflags中的中断标记是打开状态;中断屏蔽寄存器没有屏蔽对应中断,设置了EOI标志。

1.修改一下时钟中断处理程序:g/kernel/kernel.asm


174     inc dword [k_reenter]
175 cmp dword [k_reenter], 0
176 jne .1 ;重入进入.1
177
178 mov esp, StackTop ; 切到内核栈
179
push .restart_v2
jmp .2
.1:
push .restart_reenter_v2
.2
180 sti
181
182 push 0
183 call clock_handler
184 add esp, 4
185
186 cli
ret;对应前面的push语句

.restart_v2:
188 mov esp, [p_proc_ready] ; 离开内核栈;
189 lldt [esp + P_LDT_SEL]
190 lea eax, [esp + P_STACKTOP]
191 mov dword [tss + TSS3_S_SP0], eax
192
193 .restart_reenter_v2: ; 如果(k_reenter != 0),会跳转到这里
194 dec dword [k_reenter] ; k_reenter--;
195 pop gs ; ┓
196 pop fs ; ┃
197 pop es ; ┣ 恢复原寄存器值
198 pop ds ; ┃
199 popad ; ┛
200 add esp, 4
201

注意:上面的代码从逻辑上也发生了改变:在发生了中断重入的时候,仍然会执行中断服务程序,不过仅仅会打印"!"就返回;没有发生中断切换的时候就会执行进程切换。
    我们来看看对应的中断处理程序:
 21 PUBLIC void clock_handler(int irq) 22 {
23 disp_str("#");
24 ticks++;
25
26 if (k_reenter != 0) {
27 disp_str("!");
28 return;
29 }
30
31 p_proc_ready++;
32
33 if (p_proc_ready >= proc_table + NR_TASKS) {
34 p_proc_ready = proc_table;
35 }
36 }

我们更改代码以后,再来运行一下程序:
思考:上面的代码是否会产生中断重入导致的堆栈溢出问题?

2.修改restart程序

restart是将睡眠的程序重新运行的一段程序,是用汇编在kernel.asm中定义的,由main.c中的tnix_main()调用,然后进入死循环:
 54     k_reenter = -1; 55 
56 p_proc_ready = proc_table;
57
58 restart();
59
60
61 while(1){}

我们来修改restart的代码:
353 restart:354     mov esp, [p_proc_ready]355     lldt    [esp + P_LDT_SEL]356     lea eax, [esp + P_STACKTOP]357     mov dword [tss + TSS3_S_SP0], eax358 restart_reenter: dec dword [k_reenter];由于这一句,我们需要将k_reenter的初始值从-1改成0359     pop gs360     pop fs361     pop es362     pop ds363     popad364     add esp, 4365     iretd

由于时钟中断的后部分和restart是相同的,我们可以将这时钟中断中重合的部分省略。


3修改中断处理程序——save

对比原来的中断处理程序:我们总结一下时钟中断程序的内容:
1)保护上下文
2)判断中断重入与堆栈切换
3)开中断与中断处理核心程序
4)恢复上下文(已经并在了restart过程当中)
现在,我们将1)和2)也独立出来:
193 save:194     sub esp, 4
195 pushad ; ┓
196 push ds ; ┃
197 push es ; ┣ 保存原寄存器值
198 push fs ; ┃
199 push gs ; ┛
200 mov dx, ss
201 mov ds, dx
202 mov es, dx
203
204 mov eax,esp
205
206 inc dword [k_reenter]
207 cmp dword [k_reenter], 0
208 jne .1
209
210 mov esp, StackTop ; 切到内核栈
211 push restart
212 jmp .2
213 .1:
214 push restart_reenter
215 .2:
216 jmp [eax+RETADR-P_STACKBASE]
     中断处理程序的部分内容:
158 hwint00:        ; Interrupt routine for irq 0 (the clock).159 160     ;inc    byte [gs:0] ; 改变屏幕第 0 行, 第 0 列的字符161     call save162     mov al, EOI     ; ┓reenable master 8259163     out INT_M_CTL, al   ; ┛
      这里,我们注意到一个很奇怪的call指令——没有以ret结尾。为什么呢?想一想,ret从堆栈中弹出返回地址赋值给eip;但是由于这里,esp在call指令中发生了变化,所以没法用ret进行返回,需要使用jmp,就是call指令的下一条指令的地址作为call过程的返回地址。

4.修改中断处理程序——时钟中断的可重入问题

        我们在本文第一部分已经分析过,这种情况下的代码仍然可能导致时钟中断的重入问题:所以,我们有必要在重新打开中断之前关闭时钟中断;在关闭中断之后再打开时钟中断。
看代码:
160     call save161     
162 in al,INT_M_CTLMASK
163 or al,1
164 out INT_M_CTLMASK,al
165
166 mov al, EOI ; ┓reenable master 8259
167 out INT_M_CTL, al ; ┛
168
169 sti
170 push 0
171 call clock_handler
172 add esp, 4
173
174 cli
175 in al,INT_M_CTLMASK
176 and al,0xFE
177 out INT_M_CTLMASK
178
179 ret

5.修改中断处理程序——代码模块化:统一的中断处理例程

     在上面的这段代码中,我们注意到,真正与时钟相关的部分是有限的,这很容易扩展到其他中断类型。我们来定义一个宏hwint_master 1来处理硬件中断。
150 ; 中断和异常 -- 硬件中断151 ; ---------------------------------
152 %macro hwint_master 1
153 call save
154 in al, INT_M_CTLMASK ; ┓
155 or al, (1 << %1) ; ┣ 屏蔽当前中断
156 out INT_M_CTLMASK, al ; ┛
157 mov al, EOI ; ┓置EOI位
158 out INT_M_CTL, al ; ┛
159 sti ; CPU在响应中断的过程中会自动关中断,这句之后就允许响应新的中断
160 push %1 ; ┓
161 call [irq_table + 4 * %1] ; ┣ 中断处理程序;很显然,irq_table是一个函数指针组成的数组,我们在global.c中对他加以定义
162 pop ecx ; ┛
163 cli
164 in al, INT_M_CTLMASK ; ┓
165 and al, ~(1 << %1) ; ┣ 恢复接受当前中断
166 out INT_M_CTLMASK, al ; ┛
167 ret
168 %endmacro

     相关:
irq_table在global.c中进行定义:
PUBLIC irq_hander irq_tabel[NR_IRQ];
申明:extern irq_hander irq_table[];在global.h
在type.h中增加对irq_handler的定义:typedef void (*irq_handler) (int irq)
const.h :#define NR_IRQ 16

    接下来,我们来初始化irq_table,先都初始化成spurious_irp:
在init_8259A函数中:
int i;for(i=0;i<NR_IRQ;i++LL)
irq_table[i]=surious_irq;

    现在,我们需要一个函数对irq_table[0]进行赋值:
 
44 PUBLIC void put_irq_handler(int irq, t_pf_irq_handler handler) 45 { 46     disable_irq(irq);这一句是必须的——进行中断处理之前,先关对应中断 47     irq_table[irq] = handler; 48 }

    这里,你看到,我们使用了一个中断使能函数disable_irq,我们接下来看它和另外一个函数enable_irq的结构体。

123 ; ========================================================================
124 ; void disable_irq(int irq);
125 ; ========================================================================
126 ; Disable an interrupt request line by setting an 8259 bit.
127 ; Equivalent code for irq < 8:
128 ; out_byte(INT_CTLMASK, in_byte(INT_CTLMASK) | (1 << irq));
129 ; Returns true iff the interrupt was not already disabled.
130 ;
131 disable_irq:
132 mov ecx, [esp + 4] ; irq
133 pushf
134 cli
135 mov ah, 1
136 rol ah, cl ; ah = (1 << (irq % 8))
137 cmp cl, 8
138 jae disable_8 ; disable irq >= 8 at the slave 8259
139 disable_0:
140 in al, INT_M_CTLMASK
141 test al, ah
142 jnz dis_already ; already disabled?
143 or al, ah
144 out INT_M_CTLMASK, al ; set bit at master 8259
145 popf
146 mov eax, 1 ; disabled by this function
147 ret
148 disable_8:
149 in al, INT_S_CTLMASK
150 test al, ah
151 jnz dis_already ; already disabled?
152 or al, ah
153 out INT_S_CTLMASK, al ; set bit at slave 8259
154 popf
155 mov eax, 1 ; disabled by this function
156 ret
157 dis_already:
158 popf
159 xor eax, eax ; already disabled
160 ret
161
   相应的enable的代码我们省略,请读者自行看书讲解。

6.应用统一的中断处理范式


    在main.c中,绑定中断处理程序到固定的引脚。
 57     p_proc_ready    = proc_table; 58 
59 put_irq_handler(CLOCK_IRQ, clock_handler); /* 设定时钟中断处理程序 */
60 enable_irq(CLOCK_IRQ); /* 让8259A可以接收时钟中断 */
61
62 restart();
在init_8259A中屏蔽所有的中断:out_byte(INT_M_CTLMASK,0xFF).

        回过头来总结一下,我们是如何将中断处理流程范式化的:我们将寄存器恢复、堆栈切换ldt等操作提取出来,归到restart部分;将中断处理核心部分:打印相关信息和进程切换放到时钟中断处理句柄中;将上下文的保存、判断中断重入等放到save函数之中;为了防止时钟中断重入,我们在中断处理流程中加入中断使能控制语句;接下来,将时钟中断处理,推广到一般化的中断处理,将上述中断处理过程 封装成宏定义hwint_master。过程大致如此,可以看出,这是一个从特殊化到一般化的过程,也是我们避免代码臃肿和出错的良好方式。