3.3.2 函数库intALib
函数库intALib是个汇编函数库,里面保存了与CPU结构相关的中断处理函数。这里进行简单的分析。
该函数返回刚intLock函数时CPU的EFLAGS寄存器中的中断允许标志位(IF),然后将中断允许位置0,表示禁止可屏蔽中断。如果需要恢复,则调用函数intUnlock(int)将中断允许标志恢复到intLock调用之前的状态。
intLock()函数既可以在中断级又可以在任务级调用,当在任务级调用时,并不能组织任务的调度,如果需要组织任务调用则需要使用函数taskLock和taskUnlock函数。
3.3.3 函数库i8259Intr
函数库i8259Intr中提供了对8259中断控制器的基本操作函数。分析这个函数之前,有几个问题需要注意:
中断号(IntNum)和IRQ号的关系。同一PIC上的IRQ号和中断号是线性关系,如主片上IRQ0对应中断号0x20,主片上的IRQ1对应中断号0x21等;从片上的IRQ0对应与中断号0x28,从片上的IRQ1对应中断号0x29等。
中断号(IntNum)和中断优先级没有必然的联系。这主要体现在有级联的中断上,加入从片的中断输出连接到中断的IRQ2上,那么主片上IRQ0和IRQ1的中断优先级总高于从片上的中断;而主片上IRQ3~IRQ7的中断优先级总低于从片上的中断。
表3.1给出了中断号、IRQ号与优先级的关系。主从片连接图参见图3.1。
表格 3.1 PC机对应的中断号、IRQ号与优先级的关系
中断号 | IRQ | 优先级 |
0x20 | 主片IRQ0 | 0 |
0x21 | 主片IRQ1 | 1 |
0x22 | 从片 | —— |
0x23 | 主片IRQ3 | A |
0x24 | 主片IRQ4 | B |
0x25 | 主片IRQ5 | C |
0x26 | 主片IRQ6 | D |
0x27 | 主片IRQ7 | E |
0x28 | 从片IRQ0 | 2 |
0x29 | 从片IRQ1 | 3 |
0x2A | 从片IRQ2 | 4 |
0x2B | 从片IRQ3 | 5 |
0x2C | 从片IRQ4 | 6 |
0x2D | 从片IRQ5 | 7 |
0x2E | 从片IRQ6 | 8 |
0x2F | 从片IRQ7 | 9 |
中断向量(intVec)说明某一中断号中断号(IntNum)对应的中断处理程序保存在内存中的相对于中断向量基本地址的偏移量。而中断处理程序实际存放的位置可以参见图3.3的说明。中断向量和中断号的关系从文件h\arch\i86\ivI86.h中看出:
#define IVEC_TO_INUM(intVec) ((int) (intVec) >> 3)
#define INUM_TO_IVEC(intNum) ((VOIDFUNCPTR *) ((intNum) << 3))
1. VOID i8259Init()函数
该函数组合要用于初始化8259控制芯片。初始化完毕后禁止中断。
2. VOID i8259IntBoiEem(irqNo)
在Early EOI Mode模式下,向PIC1发送EOI指令,以清除ISR寄存器中优先级最高的位。这个函数主要用于IRQ0也就是时钟中断的处理,该中断的优先级最高,因此EOI指令必然清除的是IRQ0的中断信号。如果想用该函数清除从片级联接入的IRQ2中断,就可能出现问题。
3. VOID i8259IntBoiSmm(irqNo)
该函数主要用于IRQ0时钟中断的处理。由于IRQ0的优先级最高,因此在处理IRQ0中断时,其他中断是不能响应的,这在一定的程度上会影响系统处理的性能,为了解决这个问题,可以采取特殊屏蔽模式。在此模式下,可以屏蔽IRQ0掉中断,这样系统则可以相应比IRQ0优先级低的中断了。当然在IRQ0处理完毕后,还要将此模式恢复为正常状态。
函数i8259IntBoiSmm(irqNo)的作用,就是首先将主PIC设置为特殊屏蔽模式,并屏蔽IRQ0中断,这样系统就可以处理优先级低于IRQ0的中断。待IRQ0的ISR执行完毕后还要重新执行VOID i8259IntEoiSmm(irqNo)关闭特殊屏蔽模式并接触IRQ0的中断屏蔽。
注意:一般屏蔽模式下,更高优先级的中断可以响应,但低优先级的中断不能响应。而在特殊屏蔽模式下,更高或更低优先级的中断都可以响应。
4. VOID i8259IntBoi(irqNo)
一般来说,中断输入IRQn必须保持高电平一直到第一个INTA信号的变低, 如果IRQn在此之前变高, 当CPU确认中断时,会在总线上上将会产生IRQ7(主片)或IRQ15(从片)中断信号,这种中断只需要简单的返回即可而不需要其他的处理,也不需要发送EOI信号。因此在系统检测到总线上发生了IRQ7或IRQ15中断后,并不能确定是真的发生了中断还是无效的中断,因此必须要对ISR寄存器进行访问,如果对应的ISR寄存器确有中断存在,则需要进行相应的中断处理,否则只需要简单返回即可。
这个函数的实现比较巧妙。为了能够对这个函数的代码有更深入的理解,我们先简单分析一下函数中断代码。
LOCAL UCHAR intConnectCode [] = /* intConnect stub */
{
/*
* 00 e8 kk kk kk kk call _intEnt * tell kernel
* 05 50 pushl %eax * save regs
* 06 52 pushl %edx
* 07 51 pushl %ecx
* 08 68 pp pp pp pp pushl $_parameterBoi * push BOI param
* 13 e8 rr rr rr rr call _routineBoi * call BOI routine
* 18 68 pp pp pp pp pushl $_parameter * push param
* 23 e8 rr rr rr rr call _routine * call C routine
* 28 68 pp pp pp pp pushl $_parameterEoi * push EOI param
* 33 e8 rr rr rr rr call _routineEoi * call EOI routine
* 38 83 c4 0c addl $12, %esp * pop param
* 41 59 popl %ecx * restore regs
* 42 5a popl %edx
* 43 58 popl %eax
* 44 e9 kk kk kk kk jmp _intExit * exit via kernel
*/
从上面的中断代码可以看出,正常的中断操作主要做了5件事情,也就是执行了4个函数及一个跳转。
a)_intEnt
b)_routineBoi
c)_routine
d)_routineEoi
e)_intExit(跳转)
而函数i8259IntBoi(IrqNo)则是放在函数routineBoi中执行,如果是有效的中断,则无需做任何操作,如果是无效的中断,则无需再执行后面的c)d),直接进入e),因此函数i8259IntBoi中直接将routineBoi函数的返回地址修改为intBoiExit,也即执行完routineBoi函数之后立即执行函数intBoiExit而不需要再执行c)d)。
在函数的调用过程中,操作系统首先将参数压入堆栈,而后通过call指令调用函数,call指令在执行时首先将当前代码地址压入堆栈,然后转入被调用函数的地址开始执行子程序,待子程序执行完后则直接弹出堆栈内容恢复当前代码地址,因此这里直接修改了堆栈的内容,从而使得子程序执行完毕后弹出堆栈恢复的不是当前代码地址,而是intBoiExit的地址。
从上面的代码中还可以看出,当有新的数据压入堆栈时,则堆栈指针自动减小,弹出堆栈时,堆栈指针自动增加。如果遇到无效的中断请求,则由于返回的地址已经进行了修改,因此函数routineBoi也即i8259IntBoi执行完毕后马上跳转到intBoiExit处开始执行。代码intBoiExit在库文件intALib(汇编文件库)中,其代码如下:
addl $4, %esp * pop param
popl %ecx * restore regs
popl %edx
popl %eax
jmp _intExit * exit via kernel
这样的代码就显得有些混乱,不如注释里面的代码紧凑:
/*
* 00 e8 kk kk kk kk call _intEnt * tell kernel
* 05 50 pushl %eax * save regs
* 06 52 pushl %edx
* 07 51 pushl %ecx
* 08 68 pp pp pp pp pushl $_parameterBoi * push BOI param
* 13 e8 rr rr rr rr call _routineBoi * call BOI routine
* addl $4, %esp
* cmpl $0, %eax
* jne intConnectCode0
* 18 68 pp pp pp pp pushl $_parameter * push param
* 23 e8 rr rr rr rr call _routine * call C routine
* addl $4, %esp
* 28 68 pp pp pp pp pushl $_parameterEoi * push EOI param
* 33 e8 rr rr rr rr call _routineEo * call EOI routine
* 38 83 c4 04 addl $4, %esp * pop param
* intConnectCode0:
* 41 59 popl %ecx * restore regs
* 42 5a popl %edx
* 43 58 popl %eax
* 44 e9 kk kk kk kk jmp _intExit * exit via kernel
*/
这段代码要求在函数routineBoi有返回值,如果是正常中断返回值为0,否则返回非0值。该返回值都存放在eax寄存器。
5. VOID i8259IntEoiSmm(irqNo)
参考i8259IntBoiSmm(irqNo)函数。
6. VOID i8259IntEoiMaster(irqNo)
向主片发送一般中断结束信号EOI。
7. VOID i8259IntEoiSlave(irqNo)
在Fully Nested Mode模式下,对于从片上的中断IRQn,当处理中断IRQn时,优先级等于或低于当前处理的中断优先级都将不被处理,而只能等待,只有等当前中断处理完毕后才会处理低级别中断或后出现的同级别中断。但是如果此时从片IRQi的中断正在处理,而从片IRQi-1的中断来了,理论上优先级更高,可是对主片来说优先级都是一样的,因此将不被处理。而只有当IRQi处理完毕后才会处理IRQi-1中断,这样就造成了中断优先级处理的混乱。
因此如果简单地向从片发送一个非指定的EOI信号,则从片清除的则是优先级最高的中断,而实际上该中断还没来得及处理,从而造成了混乱。函数i8259IntEoiSlave(irqNo)在处理的时候并没有考虑到这个问题,需要特别注意。
解决这个问题的方法就是向从片发送特定EOI信号,即指定当前EOI指令要清除的中断标识号,从而避免上述问题。
另一方面,如果向从片发送EOI信号之后立即向主片发送EOI信号,也存在一种可能,就是从片上还有其他的中断没有处理,这将导致错误,因此正确的处理方法应该是参照函数i8259IntEoiSlaveSfnm(irqNo),检查从片上是否还有其他未处理的中断,如果有则继续处理,否则可向主片发送EOI信号。
以上两个方面则是这个函数需要改进的地方。当然,如果既有主片又有从片一般来说是不使用完全嵌套模式的。
8. VOID i8259IntEoiSlaveNfnm(irqNo)
同函数i8259IntEoiSlave(irqNo)。
9. VOID i8259IntEoiSlaveSfnm(irqNo)
特殊嵌套模式和完全中断模式的区别在于对同级别中断的处理,特殊嵌套模式下主片在处理从片上的中断时,如果从片上来了更高优先级的中断信号则会在主片上形成新的中断信号并通知CPU。从而解决了完全嵌套模式下从片上中断优先级混乱的问题。
在从片上的当前中断处理完毕后,需要向从片发送非指定EOI信号,此时从片自动清除优先级最高的中断标识,这是正确的,因为处理的确实是优先级最高的中断。不过在向主片上发送EOI之前,还要检查从片上是否还有其他中断需要处理,如果有,还需要处理其他中断,如果没有则直接向主片发送EOI信号即可。
10. LOCAL STATUS i8259IntDisable(irqNo)
屏蔽irqNo中断,注意首先要根据irqNo判断是主片上的还是从片上的。
11. LOCAL STATUS i8259IntEnable(irqNo)
允许irqNo中断,注意首先要根据irqNo判断是主片上的还是从片上的,然后才能取消相应PIC的控制功能。
12. LOCAL VOID i8259IntLock()
屏蔽所有中断,注意屏蔽前要先保存之前的屏蔽情况。主从片上的中断屏蔽情况分别保留在变量i8259IntMask1,i8259IntMask1中。
13. LOCAL VOID i8259IntUnlock()
恢复原中断屏蔽CODE。