中断是windows中最难的一部分,这是因为中断本身属于操作系统的一部分,理解了中断和内存,对整个系统也就了解了。
中断部分会先从中断优先级、中断处理、中断服务例程入手,大概讲述一下中断的概念;接着从中断的一般实现来讲解;最后我们会实际分析PCI中断相关的部分。
中断优先级
再看中断之前,我们闲了接一下中断优先级的概念,在之前我们也提到过内核代码的执行级别,那个时候只是简单的说一下代码的运行级别,但实际上,这个概念是中断优先级才对。
windows使用0~31的优先级来描述中断,数值越大,中断优先级越高,详细的描述如下:
PASSIVE_LEVEL 0
APC_LEVEL 1
DISPATCH_LEVEL 2 DIRQL: from 3 to 26 for device ISRPROFILE_LEVEL 27
SYNCH_LEVEL 28
CLOCK_LEVEL 28
IPI_LEVEL 29
POWER_LEVEL 30
HIGH_LEVEL 31
0~2级别的中断称为软件中断,它们定义在内核代码中;剩下称为硬件中断,定义在HAL(硬件抽象)层中;硬件中断的3~26号称为设备中断,是给windows设备使用的;27~31号中断是留给处理器使用的中断。
HIGH_LEVEL是最高级别的中断,用于一些不可中断的执行过程,比如说,当发生蓝屏的时候,windows会将优先级提升到HIGH_LEVEL;
POWER_LEVEL是电源中断,但是windows中并没有使用它;
IPI_LEVEL是同步处理器之间通讯的中断;
CLOCK_LEVEL 是时钟中断,CPU利用这个中断来实现定时器;
PROFILE_LEVEL是性能剖析中断,在windows进行性能剖析的时候使用;
Device IRQL则是设备驱动程序使用的,它们可以和设备提供的中断服务例程;
代码运行在某个优先级,是指代码在运行的时候,内核会将优先级提高到某个优先级,事实上,综合前面的部分,我们很容易得到结论,这个说法指的是软件中断,而非硬件中断,不过要注意,内核函数的说明中会明确指出代码运行的优先级,要注意这一点。
3~26的中断等级是设备驱动程序使用的,它们并不是按照加载到内核的顺序确认的,事实上,除了总线驱动,一般也不需要考虑这个问题,不过我们仍然可以在设备管理器中看到一点踪迹:
这是我从设备管理器中的截图,在USB 3.1总线上,会有一个资源选项卡,其中会给出当前总线的IRQ,这个IRQ后面会被映射为某个IRQL。
在上图中已经能够推断出这个USB总线的寄存器范围是84500000~8450FFFF,它的大小就是0xFFFF;其次,这个设备注册的中断服务例程IRQL等级是20;
实际上,现在内部总线几乎都是PCI总线,大部分都在PCI复合设备上,系统中另外一个中断来源则是GPIO。
中断的处理
Intel x86定义了256个中断向量,这些中断向量通过中断描述符来表达,这些描述符存在中断描述符表IDT中,中断描述符寄存器会在启动后,中断向量编程完成后存储中断描述符表的地址,在中断发生时,控制权转移到中断服务例程执行。
注意,虽然只有256个中断,但是处理器可以处理的中断数非常多,因为在中断描述符表中,32!256号中断对应的是一个中断处理链,同级别的中断可能来自于不同的设备,例如USB总线上的N个设备,可能都挂到一个中断处理例程上。
举个例子,当用0来做除数的时候,在译码周期会被检测到,此时CPU就会将根据异常来将中断向量中0号中断地址从IDTR中取出,将寄存器、栈保存下来,然后跳转到中断的入口点,执行中断,执行完中断之后,再跳回原来的指令流。在这种过程中,我们会发现,保存当前寄存器和栈是希望这个中断可以被处理,异常和中断都是用同一套机制来处理的。
先说异常,对于异常,有三种可能: 错误、异常、终止:
对于错误,处理器期待可以恢复,故处理完之后,指令流会回到出现错误的那条指令持续执行,典型的例子是页面错误,中断处理程序会将错误的页面找到并调入系统,然后再执行错误指令即可;
对于异常,处理器也期待可以恢复,故会发挥到下一条指令,典型的例子是调试断点;
至于终止,处理器已经不期待指令流可以执行了,所以指令流不会再回去执行了。
中断和异常不一样的是,中断是异步的,所以当中断会被分为上半部分和下半部分(这个说法是Linux的,但是这个说法很好的诠释了中断处理的本质,上半部分用于处理最紧急的事件,下半部分则处理不那么紧急的事件),上半部分执行完后,代码流可能会返回之前的指令流,此时中断处理可能还没完成,直到下半部分处理完,中断才算完成,不过此时之前的指令流可能已经执行完了,故中断可以是异步的,就是源自于此。
比如说,在上面的USB总线的中断到来时候,windows正在执行某个线程,此时如果处理器可以响应这个USB中断,那么它会将当前线程压入堆栈,然后控制权交给中断服务例程,中断服务例程则会处理最紧急的部分,例如说将总线上的数据取下,然后中断服务例程会插入一个DPC/APC/工作项,将中断的下半部分一些不重要的事情会放在这些处理方式中里面执行,指令流再返回之前的指令流,或者不返回接着执行DPC也是可能的。
中断的异步性是通过DPC/APC/工作项来体现的,总线驱动的中断处理例程往往只会处理非常短的时间,它可能仅仅将数据拷贝一下或者设置一下,然后就马上返回了;处理器不应该在高级别的IRQL下运行太久的时间,这会导致性能急剧下降,对其他中断的响应速度下降。
中断服务例程
生成中断的设备驱动程序必须至少有一个中断服务例程 (ISR) 。ISP例程必须执行适用于设备的任何操作来消除中断,可能包括阻止设备中断;它应仅执行保存状态并将DPC 排队的必要操作,以低于执行ISP例程的优先级(IRQL)完成I/O操作。
驱动程序的ISP例程在中断上下文中执行,由 IoConnectInterruptEx 的 SynchronizeIrql 参数指定为某个系统分配的 DIRQL。
ISP例程是可中断的,具有更高系统分配DIRQL的另一个设备可以随时中断或高IRQL系统中断。
在系统调用ISP例程之前,它会获取中断的自旋锁,因此ISP例程不能在另一个处理器上同时执行。ISP例程返回后,系统会释放旋转锁。
由于ISP例程以相对较高的IRQL运行,因此它使用当前处理器上的等效或更低IRQL屏蔽中断,因此它应尽快返回控制权。 此外,在DIRQL 上运行ISP例程会限制ISP例程可以调用的支持例程集。
通常,ISP例程执行以下常规步骤:
如果导致中断的设备不受ISP例程支持,则ISP例程将立即返回 FALSE。
否则,ISP例程会根据需要清除中断,保存所需的任何设备上下文,并将DPC 排队以在较低的IRQL处完成I/O操作。然后,ISP例程必须返回 TRUE。
具体而言,在不涉及重叠设备I/O操作的驱动程序中,ISP例程应执行以下操作:
1. 确定中断是否为虚假中断。 如果是,请立即返回 FALSE ,以便立即调用中断设备的ISP例程。 否则,请继续中断处理。
2. 如有必要,请停止设备中断。
3. 收集DPCForISP例程(或 CustomDpc) 例程完成当前操作的I/O处理所需的任何上下文信息。
4. 将此上下文存储在DpcForISP例程或CustomDpc例程可访问的区域中,通常存储在处理当前I/O请求导致中断的目标设备对象的设备扩展中。
如果驱动程序与I/O操作重叠,则上下文信息必须包括DPC 例程需要完成的未完成请求计数,以及DPC 例程完成每个请求所需的任何上下文。 如果在DPC 运行之前调用ISP例程来处理另一个中断,则它不得覆盖DPC 尚未完成的请求的已保存上下文。
5. 如果驱动程序具有DPCForISP例程例程,请使用指向当前 IRP、目标设备对象和已保存上下文的指针调用 IoRequestDpc 。 IoRequestDpc 会将DPCForISP例程例程排入队列,只要IRQL低于处理器上的DISPATCH_LEVEL即可运行。
如果驱动程序具有CustomDpc例程,请使用指向与CustomDpc 例程关联的DPC 对象的指针 (调用 KeInsertQueueDpc) 指向CustomDpc例程完成操作所需的任何已保存上下文的指针。
通常,ISP例程还会传递指向当前 IRP 和目标设备对象的指针。 只要IRQL低于处理器上的DISPATCH_LEVEL, 就会运行CustomDpc例程。
6. 返回 TRUE 以指示其设备生成了中断。
通常,ISP例程不会执行实际I/O处理来满足 IRP。 相反,它会停止设备中断,设置必要的状态信息,并将驱动程序的DPCForISP例程或CustomDpc排队,以执行任何必要的I/O处理来满足导致设备中断的当前请求后返回。
ISP例程必须在尽可能短的时间间隔内以DIRQL运行。 遵循此准则会增加计算机中每台设备的I/O吞吐量,因为以DIRQL运行会屏蔽系统为其分配了较小或相等IRQL值的所有中断。