中断嵌套的过程发生时,那个中断先执行? 开始时没有理解清楚,《自己动手写操作系统》作者也没有明说,可能他自己理解了,觉得明白了。仔细看了代码后发现,中断的处理过程代码写的非常好,这个地方的处理方式总结之后,可以在以后的开发过程中借用。
有几点知识需要了解:
a. ret指令段跳转的时候只弹出栈里面的当前一个值,作为ip寄存器的值(CPU做的)。
iretd有5个值,返回多少个值,与跳转的方式有关。段间,段外,不同特权级,这些都不一样,用到的命令也不一样。
b. call指令发生时,cpu会自动将返回值压入到栈当中(cpu做的)。
c. 最重要的一点,要理解汇编语言的精髓,不要把他和C混了。。 最重要的一点区别是: 汇编是单个单个的指令来运行的,没有一定要那两个指令配套使用。eg, 函数当中不一定要ret, 指令来返回,可以用jum指令直接跳到要返回的地方。但是当前栈的内容一定要非常熟悉, 才能这么做,不然就容易出错。而C语言中有很多是需要配套的语句使用的。
1. 将分析,在中断A发生之后,没有退出中断服务时,中断B又来了,这个时候哪个中断先执行,执行后又跳到哪,堆栈怎么变化的。
整个流程相关代码如下(以时钟和键盘中断为例):
; ------------------------------------------------------------------------------------------------------------------------
//下面是中断向量表中指定的中断服务程序,只有两个例子。
ALIGN 16
hwint00: ; Interrupt routine for irq 0 (the clock).
hwint_master 0
ALIGN 16
hwint01: ; Interrupt routine for irq 1 (keyboard)
hwint_master 1
; ------------------------------------------------------------------------------------------------------------------------
//这是上面调用的宏的扩展。
%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 ;根据非重入,重入跳到restart,restart_reenter函数里执行不同的过程
%endmacro
; ------------------------------------------------------------------------------------------------------------------------
//这是上面调用的save函数的扩展。
; ====================================================================================
; save
; ====================================================================================
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; ///跳到上面宏的 in al, INT_M_CTLMASK;这句。
.1: ; } else { 已经在内核栈,不需要再切换,中断重入的时候已经在内核栈了。
push restart_reenter ; push restart_reenter
jmp [eax + RETADR - P_STACKBASE] ; return; ///跳到上面宏的 in al, INT_M_CTLMASK;这句。
; }
; ====================================================================================
; restart
; ====================================================================================
restart:
mov esp, [p_proc_ready]
lldt [esp + P_LDT_SEL]
lea eax, [esp + P_STACKTOP] ;取后者在内存的偏移地址,送到前者
;lea eax, [esp + 0x1000]
mov dword [tss + TSS3_S_SP0], eax ;当着句执行完了后,会继续执行下面的restart_reenter:部分,因为没有指令改变当前ip值。
restart_reenter:
dec dword [k_reenter]
;在执行下面的命令前,esp指向regs的开始处,当pop时会将当前值
;递减的释放到各个相应寄存器,然后iretd之后,系统会开始执行
;TestA()函数,因为iretd会将regs.eip值放入到eip指针中。
pop gs
pop fs
pop es
pop ds
popad
add esp, 4
iretd ;这个指令之后系统就开始执行testA函数了
上面怎么流程稍微有点乱,无非是两条线,一个是没有重入,一个是重入。这两个的最大区别就在于esp的变化。如下图:
1. 没有重入
上图中2应该执行regs的上面的顶部,不是底部,这是退出时,应该pop各个值到寄存器了。麻烦就没有重画了。
2. 发生重入
这就是嵌套的含义,像c语言的嵌套一样,先执行最新发生的中断,然后依次执行前一次发生的中断,直到最后一个中断发生处理完。都在内核栈完成。但是注意,这个时候clock中断是关了的。只有可能发生其它类型的中断重入。也就是说每一个同类型的中断是不可以两次重入的。发送重入只能是不同中断之间。
上面中断处理函数为什么要写这么复杂?
1. 仔细看了之后其实结构还是很清晰的,逻辑调理清楚。不算复杂。
2. 之所以要写成这样,主要是为了迁就,最初的那个宏调用。宏虽然占代码空间,但是相对了函数来说,他节省时间。函数的压栈出栈,是很耗时的。
3. 这个地方写的比较难懂和精彩的地方,是宏当中的ret指令。这个指令是直接在栈中取当前值,为下一个执行的地址。也就是把esp的值,给ip使用。
以及对中断函数处理表(call [irq_table + 4 * %1])的调用思想。
有几点知识需要了解:
a. ret指令段跳转的时候只弹出栈里面的当前一个值,作为ip寄存器的值(CPU做的)。
iretd有5个值,返回多少个值,与跳转的方式有关。段间,段外,不同特权级,这些都不一样,用到的命令也不一样。
b. call指令发生时,cpu会自动将返回值压入到栈当中(cpu做的)。
c. 最重要的一点,要理解汇编语言的精髓,不要把他和C混了。。 最重要的一点区别是: 汇编是单个单个的指令来运行的,没有一定要那两个指令配套使用。eg, 函数当中不一定要ret, 指令来返回,可以用jum指令直接跳到要返回的地方。但是当前栈的内容一定要非常熟悉, 才能这么做,不然就容易出错。而C语言中有很多是需要配套的语句使用的。
1. 将分析,在中断A发生之后,没有退出中断服务时,中断B又来了,这个时候哪个中断先执行,执行后又跳到哪,堆栈怎么变化的。
整个流程相关代码如下(以时钟和键盘中断为例):
; ------------------------------------------------------------------------------------------------------------------------
//下面是中断向量表中指定的中断服务程序,只有两个例子。
ALIGN 16
hwint00: ; Interrupt routine for irq 0 (the clock).
hwint_master 0
ALIGN 16
hwint01: ; Interrupt routine for irq 1 (keyboard)
hwint_master 1
; ------------------------------------------------------------------------------------------------------------------------
//这是上面调用的宏的扩展。
%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 ;根据非重入,重入跳到restart,restart_reenter函数里执行不同的过程
%endmacro
; ------------------------------------------------------------------------------------------------------------------------
//这是上面调用的save函数的扩展。
; ====================================================================================
; save
; ====================================================================================
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; ///跳到上面宏的 in al, INT_M_CTLMASK;这句。
.1: ; } else { 已经在内核栈,不需要再切换,中断重入的时候已经在内核栈了。
push restart_reenter ; push restart_reenter
jmp [eax + RETADR - P_STACKBASE] ; return; ///跳到上面宏的 in al, INT_M_CTLMASK;这句。
; }
; ====================================================================================
; restart
; ====================================================================================
restart:
mov esp, [p_proc_ready]
lldt [esp + P_LDT_SEL]
lea eax, [esp + P_STACKTOP] ;取后者在内存的偏移地址,送到前者
;lea eax, [esp + 0x1000]
mov dword [tss + TSS3_S_SP0], eax ;当着句执行完了后,会继续执行下面的restart_reenter:部分,因为没有指令改变当前ip值。
restart_reenter:
dec dword [k_reenter]
;在执行下面的命令前,esp指向regs的开始处,当pop时会将当前值
;递减的释放到各个相应寄存器,然后iretd之后,系统会开始执行
;TestA()函数,因为iretd会将regs.eip值放入到eip指针中。
pop gs
pop fs
pop es
pop ds
popad
add esp, 4
iretd ;这个指令之后系统就开始执行testA函数了
上面怎么流程稍微有点乱,无非是两条线,一个是没有重入,一个是重入。这两个的最大区别就在于esp的变化。如下图:
1. 没有重入
上图中2应该执行regs的上面的顶部,不是底部,这是退出时,应该pop各个值到寄存器了。麻烦就没有重画了。
2. 发生重入
这就是嵌套的含义,像c语言的嵌套一样,先执行最新发生的中断,然后依次执行前一次发生的中断,直到最后一个中断发生处理完。都在内核栈完成。但是注意,这个时候clock中断是关了的。只有可能发生其它类型的中断重入。也就是说每一个同类型的中断是不可以两次重入的。发送重入只能是不同中断之间。
上面中断处理函数为什么要写这么复杂?
1. 仔细看了之后其实结构还是很清晰的,逻辑调理清楚。不算复杂。
2. 之所以要写成这样,主要是为了迁就,最初的那个宏调用。宏虽然占代码空间,但是相对了函数来说,他节省时间。函数的压栈出栈,是很耗时的。
3. 这个地方写的比较难懂和精彩的地方,是宏当中的ret指令。这个指令是直接在栈中取当前值,为下一个执行的地址。也就是把esp的值,给ip使用。
以及对中断函数处理表(call [irq_table + 4 * %1])的调用思想。