WindowsLinux中断异常处理机制分析Word文档格式.docx
《WindowsLinux中断异常处理机制分析Word文档格式.docx》由会员分享,可在线阅读,更多相关《WindowsLinux中断异常处理机制分析Word文档格式.docx(10页珍藏版)》请在冰豆网上搜索。
下面我们来讨论一下Windows系统的硬件终端处理机制。
在Windows系统下,处理器可以在不同的状态(V86、实模式和保护模式)下运行。
当DOS运行时,则处理器运行V86模式。
当Windows执行时或当DOSVM已经切换进保护模式时,处理器则运行Ring3保护模式。
当VMM(虚拟机管理器)或VXD(虚拟设备驱动程序)执行时,处理器则运行在Ring0保护模式。
在Windows系统环境中,Windows将所有的IDT入口指向VMM中的一个函数。
VMM会判断出来自IDT入口项的调用是作为异常被调用还是作为中断被调用。
VMM本身负责处理异常而将所有硬件中断交给一个名为VPICD(虚拟可编程中断控制器设备)的VXD。
如果某个VXD已经为某个硬件中断注册了中断处理函数,那么VPICD就将中断传递给该VXD。
如果没有,VPICD将把某个中断传递给某个VM,这一过程被称作“中断反射”。
VXD通过调用VPICD的VPICD_Viutualize_IRQL服务函数为特定的硬件中断注册并将回调函数传递给VPICD。
一旦VXD已经为中断注册,它将作为一个真正的中断处理器,为中断设备进行中断服务,同时VXD可能使用另一个VPICD服务函数VPICD_Set_Int_Request来把中断映射到VM,让VM的中断处理函数来提供服务。
从IDT到VXD中断处理函数的途径
在使用中断服务时,我们也要对下列情况进行注意。
1、中断响应时间:
为了实现实时操作,一般要求中断响应时间尽可能的短。
由于硬件中断的响应过程比较复杂,中断响应时间通常在1ms以上。
为了使中断响应时间最短,硬件中断的处理应在VXD中进行。
但即使在VXD中进行,VXD也不能保证对硬件中断的实时响应。
原因在于ring转换以及VMM和VPICD之间存在多个层次的联系。
2、中断结束处理(EOI):
在编写中断处理函数时,常见的错误是忘记EOI,导致一个硬件中断仅被调用一次。
虽然设备本身能产生更多的中断,但是PIC(可编程中断控制器)不让这些中断到达处理器,并会一直持续到PIC接受到EOI为止。
Windows使用与DOS不同的中断控制器的EOI机制。
VPICD是被中断通知的第一个VXD,然后VPICD立即发送“特定EOI”到控制器。
然后VPICD屏蔽控制器上的中断级别。
这两个操作使得其他的中断级别可以被识别,包括那些比正在中断的级别低的优先权。
当VXD在退出中断处理函数前调用VPICD_Phys_EOI服务函数时,VPICD将清除相同级别上的中断的屏蔽。
3、存管理:
处理硬件中断的驱动程序对所有分配的存有严格的要求。
所有在中断期间要访问的代码和数据都必须是固定的、页码锁定的和不可废弃的。
这包括中断处理函数本身的代码以及在中断处理过程中要用到的驱动程序代码段、任何动态分配的缓冲区和应用程序分配的要传递给驱动程序的缓冲区。
存要求必须是固定的。
因为存管理器把中断处理函数要用到的数据段移动了,在一些字节被拷贝后,硬件中断发生,这时硬件中断处理函数被执行。
硬件中断处理函数会更新正在移动的数据段中的一些容,当处理函数结束运行之后,存管理器接着移动数据段,而这时的数据段已经被处理函数更改过了。
存管理器并不知道处理函数已经改变了数据。
而使用该数据段的应用程序就得不到希望得到的数据。
同时存又要求必须是页面锁定的。
假设硬件中断发生了,而存管理器是在可废弃的代码中,那么就会发生段不在存中的警告,在此种情况下就有可能发生重入DOS的情况,而DOS代码是不可重入的,所以代码段是不可废弃的。
Ⅱ、LINUX系统的中断处理方式
当一个中断发生时,并不是所有的操作都具有相同的急迫性。
事实上,把所有的操作都放进中断处理程序本身并不合适。
需要时间长的、非重要的操作应该推后,因为当一个中断处理程序正在运行时,相应的IRQ中断线上再发出的信号就会被忽略。
另外中断处理程序不能执行任何阻塞过程,如I/O设备操作。
因此,Linux把一个中断要执行的操作分为下面的三类:
1、紧急的(Critical)
这样的操作诸如:
中断到来时中断控制器做出应答,对中断控制器或设备控制器重新编程,或者对设备和处理器同时访问的数据结构进行修改。
这些操作都是紧急的,应该被很快地执行,也就是说,紧急操作应该在一个中断处理程序立即执行,而且是在禁用中断的状态下。
2、非紧急的(Noncritical)
修改那些只有处理器才会访问的数据结构(例如,按下一个键后,读扫描码)。
这些操作也要很快地完成,因此,它们由中断处理程序立即执行,但在启用中断的状态下。
3、非紧急可延迟的(Noncriticaldeferrable)
把一个缓冲区的容拷贝到一些进程的地址空间(例如,把键盘行缓冲区的容发送到终端处理程序的进程)。
这些操作可能被延迟较长的时间间隔而不影响核操作,有兴趣的进程会等待需要的数据。
尽管分为上述的三种,但所有的中断处理程序都执行四个基本的操作:
1、在核栈中保存IRQ的值和寄存器的容;
2、给与IRQ中断线相连的中断控制器发送一个应答,这将允许在这条中断线上进一步发出中断请求;
3、执行共享这个IRQ的所有设备的中断服务例程(ISR);
4、跳到ret_to_usr()的地址后终止。
Linux系统的中断主要有3种组织形式:
1、IRQ描述符irq_desc:
对于每个IRQ中断线,Linux都用一个irq_desc_t数据结构来描述,我们把它叫做IRQ描述符,NR_IRQS个IRQ形成一个全局数组irq_desc[],其定义在/include/linux/irq.h中。
2、中断控制器描述符irq_chip:
由于CPU不同,故每个处理器对于中断的处理方式不一样。
Linux为了实现统一的中断处理,提供了底层的中断处理抽象接口,对于每个平台都需要实现底层的接口函数。
这样对于上层的中断通用处理程序就无需任何改动。
3、中断服务例程描述符irqaction:
在IRQ描述符中我们看到指针action的结构为irqaction,它是为多个设备能共享一条中断线而设置的一个数据结构,代表了每个注册中断对应的信息,其在include/linux/interrupt.h中定义。
下面我们来具体讨论一下中断过程。
当外设的驱动程序都已完成了初始化工作,并且已把相应的中断服务例程挂入到特定的中断请求队列,同时当前进程正在用户空间运行(随时可以接受中断),且外设已产生了一次中断请求,CPU就在执行完当前指令后来响应该中断。
中断处理系统在Linux中的实现是非常依赖于体系结构的,实现依赖于处理器、所使用的中断控制器的类型、体系结构的设计及机器本身。
设备产生中断,通过总线把电信号发送给中断控制器。
如果中断线是激活的,那么中断控制器就会把中断发往处理器。
在大多数体系结构中,这个工作就是通过电信号给处理器的特定管脚发送一个信号。
除非在处理器上禁止该中断,否则,处理器会立即停止它正在做的事,关闭中断系统,然后跳到存中预定义的位置开始执行那里的代码。
这个预定义的位置是由核设置的,是中断处理程序的入口点。
对于ARM系统来说,有个专用的IRQ运行模式,有一个统一的入口地址。
假定中断发生时CPU运行在用户空间,而中断处理程序属于核空间,因此,要进行堆栈的切换。
也就是说,CPU从TSS中取出核栈指针,并切换到核栈(此时栈还为空)。
对于在Linux系统,如果当前处于核空间时,对于ARM系统来说是处于SVC模式,此时产生中断,中断处理完毕后,若是可剥夺核,则检查是否需要进行进程调度,否则直接返回到被中断的核空间;
若需要进行进程调度,则svc_preempt,进程切换。
如果当前处于用户空间时,对于ARM系统来说是处于USR模式,此时产生中断,中断处理完毕后,无论是否是可剥夺核,都调转到统一的用户模式出口ret_to_user,其检查是否需要进行进程调度,若需要进行进程调度,则进程切换,否则直接返回到被中断的用户空间。
三、异常处理方式
Ⅰ、WINDOWS系统的异常处理方式
目前Windows平台下实现和使用的异常处理机制主要有4种:
筛选器异常处理,结构化异常处理(StructureExceptionHandler,SEH),向量化异常处理(VectoredExceptionHandler,VEH),C++异常处理(C++ExceptionHandler,C++EH)。
前3种是由Windows操作系统实现的异常处理机制,C++EH是由C++编译器实现的异常处理机制,但它们在其部实现机理和调用底层函数方面是十分相似的。
1、筛选器异常处理
筛选器异常处理方式,它采用的是在进程围注册一个异常处理回调函数,并返回前一个注册的异常处理回调函数的句柄。
注册函数如下:
LPTOP_LEVEL_EXCEPTION_FILTERSetUnhandledExceptionFilter(
LPTOP_LEVEL_EXCEPTION_FILTERlpTopLevelExceptionFilter
);
其中的参数表示回调函数的地址。
当结构化异常SEH无法处理的时候,系统就会调用筛选器异常处理。
当筛选器异常处理也无法搞定的时候,系统会调用系统默认的异常处理程序。
筛选器异常处理是属于整个进程的,即是“全局”的,这一点跟SEH不同。
2、结构化异常处理(SEH)
下面是出自《Window核心编程》中一小段容的引用:
“微软在Windows中引入SEH的主要动机是为了便于操作系统本身的开发。
操作系统的开发人员使用SEH,使得系统更加强壮。
我们也可以使用SEH,使我们的自己的程序更加强壮。
使用SEH所造成的负担主要由编译程序来承担,而不是由操作系统承担。
当异常块(exceptionblock)出现时,编译程序要生成特殊的代码。
编译程序必须产生一些表(table)来支持处理SEH的数据结构。
编译程序还必须提供回调(callback)函数,操作系统可以调用这些函数,保证异常块被处理。
编译程序还要负责准备栈结构和其他部信息,供操作系统使用和参考。
在编译程序中增加SEH支持不是一件容易的事。
不同的编译程序厂商会以不同的方式实现SEH,这一点并不让人感到奇怪。
”
可见,SEH是Windows平台下使用最早和最广泛的异常处理机制。
它最基本的特点是在堆栈中实现并且分为进程相关和线程相关种类型。
下面分别对用户模式下的原始型SEH和封装型SEH进行讨论。
①、原始型SEH
SEH的进程相关类型是整个进程作用围的异常处理函数,通过WIN32的API函数SetUnhandledExceptionFilter进行注册,而操作系统部使用一个全局变量来记录这个顶层的处理函数,因此只能有一个全局性的异常处理函数。
而线程相关类型的作用围是本线程,并且可注册多个,甚至可以嵌套注册。
两者相比,线程相关类型在实际应用中使用较为广泛。
当线程初始化时,会自动向栈中安装一个异常处理结构,作为线程默认的异常处理。
SEH最基本的数据结构是保存在堆栈中的称为EXCEPTION_REGISTRATION的结构体,结构体包括2个元素:
第1个元素是指向下一个EXCEPTION_REGISTRATION结构的指针(prev),第2个元素是指向异常处理程序的指针(handler)。
这样一来,基于堆栈的异常处理程序就相互连接成一个链表。
异常处理结构在堆栈中的典型分布如图所示。
最顶端的异常处理结构通过线程控制块(TEB)0Byte偏移处指针标识,即FS:
[0]处地址。
用于进行实际异常处理的函数原型可表示如下:
EXCEPTION_DISPOSITION__cdecl_except_handler(
struct_EXCEPTION_RECORD*ExceptionRecord,
void*EstablisherFrame,
struct_CONTEXT*ContextRecord,
void*DispatcherContext)
该函数的最重要的2个参数是指向_EXCEPTION_RECORD结构的ExceptionRecord参数和指向_CONTEXT结构的ContextRecord参数,前者主要包括异常类别编码、异常发生地址等重要信息;
后者主要包括异常发生时的通用寄存器、调试寄存器和指令寄存器的值等重要的线程执行环境。
而用于注册异常处理函数的典型汇编代码可表示如下:
PUSHhandler;
PUSHFS:
[0];
MOVFS:
[0],ESP;
当异常发生时,操作系统的异常分发函数在进行初始处理后,如果异常没有被处理就会开始在上图所示的线程堆栈上遍历异常处理链,直到异常被处理,如果仍没有注册函数处理异常,则将异常交给缺省处理函数或直接结束产生异常的进程。
②、封装型SEH
通过使用_try{}/_except(){}/_finally{}等关键字,使开发人员更方便地在软件中使用SEH是封装型SEH的主要特点。
该机制的异常处理数据结构定义如下:
structVC_EXCEPTION_REGISTRATION
{
VC_EXCEPTION_REGISTRATION*prev;
FARPROChandler;
scopetable_entry*scopetable;
int_index;
DWORD_ebp;
}
其中,结构体中后3个成员是新增的。
而scopetable_entry的结构如下所示:
structscopetable_entry
DWORDprev_entryindex;
FARPROClpfnFilter;
FARPROClpfnHandler;
封装型SEH的基本思想是为每个函数的_try{}块建立一scopetable表,每个_try{}块对应于scopetable中的一项,该项指向_try{}块对应的scopetable_entry结构,该结构含有与_except(){}/_finally{}对应的过滤函数和处理函数。
若有_try{}块嵌套,则在scopetable_entry结构的prev_entryindex成员中指明,多层嵌套形成单向链表。
而每个函数只注册一个VC_EXCEPTION_REGISTRATION结构,该结构中的handler成员是一个重要的运行时库函数_except_handler3。
该异常处理回调函数负责对结构中的成员进行设置,查找处理函数并根据处理结果决定是继续执行还是让系统继续遍历外层SEH链。
说到底,封装型SHE的部机理,它只是扩展了原始型SEH的功能,使用它可以简化软件开发人员的工作。
3、向量化异常处理(VEH)
向量化异常处理(VEH)是在WindowsXP以后版本中新增的一种异常处理机制。
通过使用Win32API函数AddVectoredExceptionHandler就可以注册新的异常处理函数,函数的参数就是指向EXCEPTION_POINTERS结构的指针。
而VEH用到的数据结构可以构成一个双向链表。
通过对用于注册VEH的函数进行反汇编分析研究可知,VEH数据结构的相关信息存储在堆中。
在用户模式下发生异常时,异常处理分发函数在部会先调用遍历VEH记录链表的函数,如果没有找到可以处理异常的注册函数,再开始遍历SEH注册链表。
通过对VEH的原理和实现的研究,可以总结出VEH具有如下的特点:
(1)VEH是进程相关的,同时可以注册多个,甚至嵌套注册,而且注册的位置可以指定。
(2)VEH保存在堆中,而不像SEH保存在堆栈中。
(3)VEH只能用在用户模式程序中,而SEH还可以用在核模式中。
(4)VEH处理先于SEH处理执行,只有所有VEH全不处理某个异常的时候,异常处理权才会到达SEH。
正因为VEH具有以上特点,所以在实际应用中它的使用更加灵活,甚至可以利用它来实现一个微型调试器。
4、C++异常处理(C++EH)
与SHE,VEH相比,C++异常处理的部实现更加复杂,因为它牵扯到类、对象等相关处理,但C++EH也是建立在基本异常处理机制基础之上的。
另外,编译器提供了类似封装型SEH的关键字try{}/catch(){}/throw帮助开发人员方便地处理C++程序中出现的各种异常。
C++EH的独有特点有:
首先,C++EH扩展了EXCEPTION_REGISTRATION结构,新添加的成员主要作用是当异常发生时用于确定对应的try{}块,功能类似于封装型SEH结构中的_index成员。
当编译器对带有C++EH的函数进行编译时,要添加2个重的数据结构:
一个是异常处理函数_CxxThrowException();
另一个是funcinfo结构,里面包含对应catch{}块的地址、catch{}块所关心的异常类型等重要的信息。
_CxxThrowException()是C++EH的统一的异常处理函数,相当于封装型SEH中的_except_handler3()。
_CxxThrowException()函数部还是调用了Win32API函数RaiseException()来产生异常,并且异常码是固定的0xE06D7363。
当C++异常发生时,发生异常函数的funcinfo结构传给_CxxThrowException(),开始寻找是否有对该异常感兴趣的catch{}块,如果有就调用catch{}块处理异常;
如果没有则沿异常处理链表继续寻找,直到异常被处理或最后抛出错误对话框为止。
C++EH除了增加了funcinfo结构外,还增加了tryblock,catchblock等数据结构,主要的目的就是应对复杂的try{}/catch(){}/throw结构,在异常发生时能正确找到对应的异常处理块,在发生堆栈展开时能够调用对象的析构函数,释放资源等。
Ⅱ、LINUX系统的异常处理方式
Linux系统把所有进程数据结构都放于核,这就增加了一些不必要的切换时间。
Linux可以通过系统调用,安装信号的回调函数,这回调函数指针存放在核的进程数据结构里面。
这点Windows处理得比较好,Windows把进程数据结构分成了两部分,一部分敏感数据放于核的进程数据结构里面,加以保护,另一部分不敏感数据就放于用户空间,这样当访问那些不加保护的数据时,就不用切换到核,节约了时间。
像Windows下异常处理,也是一种回调函数,但因为结构放于用户空间,安装的时候就很方便,也节约切换时间。
除了上述的效率问题,Linux核并不支持类似Windows下的SHE,当Linux发生异常,会自动产生一个异常中断。
在这异常中断处理程序中会判断异常来自用户程序或者核,如果是发生在用户程序,那么会产生一个异常信号,再根据异常信号的回调函数通知用户程序发生异常。
如果发生在核里面,那么就会搜索核模块的异常结构表,找到相应的处理调用地址,修改异常中断的返回地址为异常处理的地址,中断返回的时候程序就跳到异常处理程序处理执行。
LInux有个异常模块表,保存会发生异常时候的eip和异常处理程序指针,发生异常的时候就根据异常时候的eip搜索表里面的eip,发现相等就找到了异常处理指针。
这就意味着编写的核程序必须精确的知道哪条指令可能会发生异常,这样的话要求就会很高。
这就不如Windows下的异常编程那么轻松,程序员只需要知道哪一段程序可能出现异常,就只需要一个括号一个异常语句保护这段程序就可以了。
可见异常表对于Linux是多么重要,虽然对于我们的编程来说Linux的异常处理方式较Windows处于下风,但它还是很实用的,下面我们来讨论一下利用异常表处理Linux核态缺页异常的方式。
在程序的执行过程中,因为遇到某种障碍而使CPU无法最终访问到相应的物理存单元,即无法完成从虚拟地址到物理地址映射的时候,CPU会产生一次缺页异常,从而进行相应的缺页异常处理。
基于CPU的这一特性,Linux采用了请求调页(DemandPaging)和写时复制(CopyOnWrite)的技术。
请求调页是一种动态存分配技术,它把页框的分配推迟到不能再推迟为止。
这种技术的动机是:
进程开始运行的时候并不访问地址空间中的全部容。
事实上,有一部分地址也许永远也不会被进程所使用。
程序的局部性原理也保证了在程序执行的每个阶段,真正使用的进程页只有一小部分,对于临时用不到的页,其所在的页框可以由其它进程使用。
因此,请求分页技术增加了系统中的空闲页框的平均数,使存得到了很好的利用。
从另外一个角度来看,在不改变存大小的情况下,请求分页能够提高系统的吞吐量。
当进程要访问的页不在存中的时候,就通过缺页异常