中断处理
每个cpu有一张中断表,简称IDT。
IDT的整体布局:【异常->空白->5系->硬】(推荐采用7字口诀的方式重点记忆)
异常:前20个表项存放着各个异常的描述符(IDT表不仅可以放中断描述符,还放置了所有异常的异常处理描述符,0x00-0x13)
保留:0x14-0x1F,忽略这块号段
空白:接下来存放一组空闲的保留项(0x20-0x29),供系统和程序员自己分配注册使用
5系:然后是系统自己注册的5个预定义的软中断向量(软中断指手动的INT指令)
(0x2A-0x2E5个系统预注册的中断向量,0x2A:KiGetTickCount,0x2B:KiCallbaclReturn
0x2C:KiRaiseAssertion,0x2D:KiDebugService,0x2E:KiSystemService)
硬:最后的表项供驱动程序注册硬件中断使用和自定义注册其他软中断使用(0x30-0xFF)
下面是中断号的具体的分配情况:
0x00-0x13固定分配给异常:
0x14-0x1f:Intel保留给他公司将来自己使用(OS和用户都不要试图去使用这个号段,不安全)
----------------------以下的号段可用于*分配给OS、硬件、用户使用-----------------------
linux等其他系统是怎么划分这块号段的,不管,我们只看Windows的情况
0x20-0x29:Windows没占用,因此这块号段我们也可以*使用
0x2A-0x2E:Windows自己本身使用的5个中断号
0x30-0xFF:Windows决定把这块剩余的号段让给硬件和用户使用
参见《寒江独钓》一书P93页注册键盘中断时,搜索空闲未用表项是从0x20开始,到0x29结束的,就知道为什么寒江独钓是在这段范围内搜索空白表项了(其实我们也完全可以从0x14开始搜索)
Windows系统中,0x30-0xFF这块号段让给了硬件和用户自己使用。事实上,这块号段的开头部分默认都是让给硬件IRQ使用的,也即是分配给硬件IRQ的。IRQN默认映射到中断号0x30+N,如IRQ0用于系统时钟,系统时钟中断号默认对应就是0x30。当然程序员也可以修改APIC(可编程中断控制器)将IRQ映射到自定义的中断号。
IRQ对外部设备分配,但IRQ0,IRQ2,IRQ13必须如下分配:
IRQ0---->间隔定时设备
IRQ2---->8259A芯片
IRQ13---->外部数学协处理器
其余的IRQ可以任意分配给外部设备。
虽然一个IRQ只对应一个中断号,但是由于IRQ数量有限,而设备种类成千上万,因此多个设备可以使用同一个IRQ,进而,多个设备可以分配同一个中断号。因此,一个中断号可以共享给多个设备同时使用。
Pnp设备在插入系统后,相应的总线驱动会自动为其创建一个用作栈底基石的pdo,然后给这个pdo发出一个IRP_MN_QUERY_RESOURCE_REQUIREMENTS,查询得到初步的资源需求。然后,pnp管理器会找到相应的硬件端口驱动,调用其AddDevice函数,当这个函数返回后,该硬件设备的设备栈已经建立起立了,pnp管理器就给栈顶设备发出一个IRP_MN_FILTER_RESOURCE_REQUIREMENTS再次询问该硬件需要的资源(功能驱动此时可以拦截处理这个irp,修改资源需求),当确定好最终的资源需求后,系统就协调分配端口号、中断号、DIRQL等硬件资源给它。分配完后,就发出一个IRP_MN_START_DEVICE给栈顶设备请求启动该硬件设备。当该irp下发来到端口驱动(指真正的硬件驱动)时,端口驱动这时就需要在分配的中断号上注册一个中断服务例程,以处理硬件中断,与设备进行交互。下面的函数就是用来注册中断服务例程的(准确的说法叫‘挂接中断’)
NTSTATUS
IoConnectInterrupt(OUTPKINTERRUPT*InterruptObject,//返回创建的中断对象(一般是一个数组)
INPKSERVICE_ROUTINEServiceRoutine,//我们的isr(ourisr)
INPVOIDServiceContext,//isr的参数
INPKSPIN_LOCKSpinLock,//我们isr的自旋锁,用于多cpu互斥(一般传NULL即可)
INULONGVector,//分配到的中断号
INKIRQLIrql,//isr对应的irql
INKIRQLSynchronizeIrql,//必须>=Irql,一般=Irql即可(isr实际运行在这个irql)
INKINTERRUPT_MODEInterruptMode,//表示是否允许执行本中断的下一个中断
INBOOLEANShareVector,//表示本中断对象是否想要共享中断号以及是否允许共享
INKAFFINITYProcessorEnableMask,//本isr的cpu亲缘性,一般全部cpu都亲缘。
INBOOLEANFloatingSave)//一般为FALSE
{
PKINTERRUPTInterrupt;
PKINTERRUPTInterruptUsed;//当前的中断对象
PIO_INTERRUPTIoInterrupt;//中断对象数组的头部
PKSPIN_LOCKSpinLockUsed;//实际使用的自旋锁
BOOLEANFirstRun;
CCHARCount=0;//cpu号
KAFFINITYAffinity;//cpu亲缘性掩码
PAGED_CODE();
*InterruptObject=NULL;
Affinity=ProcessorEnableMask&KeActiveProcessors;//本isr的cpu亲缘性与实有cpu的交集
while(Affinity)
{
if(Affinity&1)Count++;
Affinity>>=1;
}
//上面的循环根据本isr可以在哪些cpu上运行,得出可运行的cpu个数
if(!Count)returnSTATUS_INVALID_PARAMETER;
//分配一个中断对象数组
IoInterrupt=ExAllocatePoolWithTag(NonPagedPool,(Count-1)*sizeof(KINTERRUPT)+
sizeof(IO_INTERRUPT),TAG_KINTERRUPT);
if(!IoInterrupt)returnSTATUS_INSUFFICIENT_RESOURCES;
*InterruptObject=&IoInterrupt->FirstInterrupt;
//if用户没提供自旋锁,就使用内置的自旋锁。一般用户不需自己提供自旋锁
SpinLockUsed=SpinLock?SpinLock:&IoInterrupt->SpinLock;
Interrupt=(PKINTERRUPT)(IoInterrupt+1);//后面的中断对象数组地址
FirstRun=TRUE;
RtlZeroMemory(IoInterrupt,sizeof(IO_INTERRUPT));
Affinity=ProcessorEnableMask&KeActiveProcessors;
for(Count=0;Affinity;Count++,Affinity>>=1)//Count其实表示cpu号
{
if(Affinity&1)
{
//第一次使用头部中的那个内置中断对象
InterruptUsed=FirstRun?&IoInterrupt->FirstInterrupt:Interrupt;
//构造一个中断对象
KeInitializeInterrupt(InterruptUsed,ServiceRoutine,ServiceContext,
SpinLockUsed,Vector,Irql,SynchronizeIrql,
InterruptMode,ShareVector,Count,FloatingSave);
if(!KeConnectInterrupt(InterruptUsed))//关键,挂接中断对象到目标cpu的指定中断号
{
//if挂接失败
if(FirstRun)
ExFreePool(IoInterrupt);
else
IoDisconnectInterrupt(&IoInterrupt->FirstInterrupt);
returnSTATUS_INVALID_PARAMETER;
}
if(FirstRun)
FirstRun=FALSE;
Else//记录各cpu的那个中断号上挂接的中断对象地址
IoInterrupt->Interrupt[(UCHAR)Count]=Interrupt++;
}
}
returnSTATUS_SUCCESS;
}
如上,这个函数用来将指定isr挂接到各个cpu的指定中断号上。因为在多cpu系统中,一个设备可以向每个cpu都发出中断,因此,必须在每个cpu的IDT中都要挂接登记那个中断的isr。具体是怎么挂接的呢?这个函数会创建一个中断对象数组,然后将各个中断对象对应挂接到各cpu的同一中断号上。由于老式机器是单cpu的,因此,早期的中断对象结构IO_INTERRUPT就包含一个中断对象任意,后来的机器对其进行了扩展,在这个结构后面是一个中断对象数组,用来挂接到其他cpu上。
另外,由于多个设备可以共用同一中断号,所以每个中断号需要一个自己的链表来记录所有挂接在此中断号上的所有中断对象。
typedefstruct_IO_INTERRUPT
{
KINTERRUPTFirstInterrupt;//内置的中断对象
PKINTERRUPTInterrupt[MAXIMUM_PROCESSORS];//记录各cpu上挂接的中断对象地址
KSPIN_LOCKSpinLock;//内置的isr自旋锁,如果用户没提供,就默认使用这个公共的自旋锁。
}IO_INTERRUPT,*PIO_INTERRUPT;
该结构体后面紧跟一个INTERRUPT结构体数组
typedefstruct_KINTERRUPT//中断对象
{
CSHORTType;
CSHORTSize;
LIST_ENTRYInterruptListEntry;//用来挂入中断对象链表
PKSERVICE_ROUTINEServiceRoutine;//我们的isr(用户的isr)
PVOIDServiceContext;//isr参数
KSPIN_LOCKSpinLock;//一般无用
ULONGTickCount;//没用
PKSPIN_LOCKActualLock;//我们isr实际使用的自旋锁
PKINTERRUPT_ROUTINEDispatchAddress;//中间的Dispatchisr函数地址
CCHARNumber;//要挂往的目标cpu
ULONGVector;//要挂往的目标中断号
KIRQLIrql;//isr对应的isr
KIRQLSynchronizeIrql;//isr实际运行在的irql
BOOLEANFloatingSave;//一般为FALSE
BOOLEANConnected;//表示本中断对象是否挂上去了
BOOLEANShareVector;//是否想要共享中断号,以及是否允许后来的中断对象共享
KINTERRUPT_MODEMode;//是否允许继续执行本中断对象后面的中断对象的isr
ULONGServiceCount;//没用
ULONGDispatchCount;//没用
ULONGDispatchCode[DISPATCH_LENGTH];//这不是数组,而是一段代码,表示本中断对象的模板isr
}KINTERRUPT;
下面的函数用来构造、初始化一个中断对象
VOID
KeInitializeInterrupt(INPKINTERRUPTInterrupt,
INPKSERVICE_ROUTINEServiceRoutine,INPVOIDServiceContext,
INPKSPIN_LOCKSpinLock,INULONGVector,INKIRQLIrql,
INKIRQLSynchronizeIrql,INKINTERRUPT_MODEInterruptMode,
INBOOLEANShareVector,INCHARProcessorNumber,INBOOLEANFloatingSave)
{
ULONGi;
PULONGDispatchCode=&Interrupt->DispatchCode[0],Patch=DispatchCode;//patch表示补丁处
Interrupt->Type=InterruptObject;
Interrupt->Size=sizeof(KINTERRUPT);
if(SpinLock)//由于这个函数未导出,由系统内部调用,传的SpinLock参数很少为NULL
Interrupt->ActualLock=SpinLock;//使用头部中公共的自旋锁或者我们提供的自旋锁
else
{
KeInitializeSpinLock(&Interrupt->SpinLock);
Interrupt->ActualLock=&Interrupt->SpinLock;
}
Interrupt->ServiceRoutine=ServiceRoutine;
Interrupt->ServiceContext=ServiceContext;
Interrupt->Vector=Vector;
Interrupt->Irql=Irql;
Interrupt->SynchronizeIrql=SynchronizeIrql;
Interrupt->Mode=InterruptMode;
Interrupt->ShareVector=ShareVector;
Interrupt->Number=ProcessorNumber;
Interrupt->FloatingSave=FloatingSave;
Interrupt->TickCount=MAXULONG;
Interrupt->DispatchCount=MAXULONG;
//先拷贝模板isr的字节码到中断对象内部
for(i=0;i<DISPATCH_LENGTH;i++)
*DispatchCode++=((PULONG)KiInterruptTemplate)[i];
//patch指向模板isr中的movedx,0指令的操作数部分
Patch=(PULONG)((ULONG)Patch+
((ULONG)&KiInterruptTemplateObject-4-(ULONG)KiInterruptTemplate));
*Patch=PtrToUlong(Interrupt);//也即将原movedx,0改为movedx,本中断对象的地址
Interrupt->Connected=FALSE;//尚未挂入
}
下面是系统的模板isr:
_KiInterruptTemplate:
KiEnterTrapKI_PUSH_FAKE_ERROR_CODE
_KiInterruptTemplate2ndDispatch:
movedx,0//这条指令的操作数0将被动态修改成具体中断对象的地址
_KiInterruptTemplateObject:
moveax,offset@KiInterruptTemplateHandler@8//KiInterruptTemplateHandler函数
jmpeax
_KiInterruptTemplateDispatch:
上面就是系统的模板isr,每个中断对象的模板isr就是从系统的模板isr复制过来的,然后稍作修改。
当构造好中断对象后,就需要把它挂接到目标cpu的目标中断号上。下面的函数就这个用途
BOOLEAN//返回值表示是否挂接成功
KeConnectInterrupt(INPKINTERRUPTInterrupt)
{
BOOLEANConnected,Error,Status;
KIRQLIrql,OldIrql;
UCHARNumber;
ULONGVector;
DISPATCH_INFODispatch;
Number=Interrupt->Number;//目标cpu
Vector=Interrupt->Vector;//目标中断号
Irql=Interrupt->Irql;
//SynchronizeIrql必须>=Irql
if((Irql>HIGH_LEVEL)||(Number>=KeNumberProcessors)||
(Interrupt->SynchronizeIrql<Irql)||(Interrupt->FloatingSave))
{
returnFALSE;
}
Connected=FALSE;
Error=FALSE;
KeSetSystemAffinityThread(1<<Number);//改变当前线程的cpu亲缘性先,挪到目标cpu上去运行
------------------------------------华丽的分割线--------------------------------------
//下面的这些代码已经处在目标cpu上运行了
OldIrql=KiAcquireDispatcherLock();
if(!Interrupt->Connected)//if尚未挂接
{
//查询当前cpu这个中断号上的最近一次(也即上一次)的挂接情况
KiGetVectorDispatch(Vector,&Dispatch);
if(Dispatch.Type==NoConnect)//如果这个中断号尚未挂接有任何中断对象
{
Interrupt->Connected=Connected=TRUE;
InitializeListHead(&Interrupt->InterruptListEntry);//独立
//NormalConnect表示以普通方式挂上去(非链接方式),相当于覆盖方式
KiConnectVectorToInterrupt(Interrupt,NormalConnect);
Status=HalEnableSystemInterrupt(Vector,Irql,Interrupt->Mode);//APIC相关
if(!Status)Error=TRUE;
}
elseif((Dispatch.Type!=UnknownConnect)&&//已挂接有中断对象
(Interrupt->ShareVector)&&//本中断对象想要共享这个中断号
(Dispatch.Interrupt->ShareVector)&&//并且上次挂接的那个中断对象允许共享
(Dispatch.Interrupt->Mode==Interrupt->Mode))
{
Interrupt->Connected=Connected=TRUE;
//if上一个中断对象不是以链接方式挂上去的,就改为链接方式挂上去
if(Dispatch.Type!=ChainConnect)
{
ASSERT(Dispatch.Interrupt->Mode!=Latched);
KiConnectVectorToInterrupt(Dispatch.Interrupt,ChainConnect);
}
//关键。挂在上一个中断对象的后面
InsertTailList(&Dispatch.Interrupt->InterruptListEntry,
&Interrupt->InterruptListEntry);
}
}
KiReleaseDispatcherLock(OldIrql);
KeRevertToUserAffinityThread();
if((Connected)&&(Error))
{
KeDisconnectInterrupt(Interrupt);
Connected=FALSE;
}
returnConnected;
}
下面的函数用于查询当前cpu指定中断号上的最近一次挂接情况(查询最近一次挂上去的中断对象,以及它当时是怎么挂上去的)
VOIDKiGetVectorDispatch(INULONGVector,INPDISPATCH_INFODispatch)
{
PKINTERRUPT_ROUTINEHandler;
PVOIDCurrent;
UCHARType;
UCHAREntry;
Entry=HalVectorToIDTEntry(Vector);//这个宏将中断向量号转换为IDT表项索引(一般相同)
//固定为KiUnexpectedInterruptN函数的地址,N表示IRQ
Dispatch->NoDispatch=(PVOID)(((ULONG_PTR)&KiStartUnexpectedRange)+
(Entry–0x30)*KiUnexpectedEntrySize);
Dispatch->InterruptDispatch=(PVOID)KiInterruptDispatch;//这个字段固定
Dispatch->FloatingDispatch=NULL;//尚不支持
Dispatch->ChainedDispatch=(PVOID)KiChainedDispatch;//这个字段固定
Dispatch->FlatDispatch=NULL;
Current=KeQueryInterruptHandler(Vector);//获得这个中断向量处当前存放的isr函数地址
if((PKINTERRUPT_ROUTINE)Current==Dispatch->NoDispatch)
{
Dispatch->Interrupt=NULL;//表示尚未挂接有任何中断对象
Dispatch->Type=NoConnect;//表示尚未挂接有任何中断对象
}
else
{
//关键,可有isr(其实是个模板isr)反推出当前的中断对象(即最近一次挂上去的对象)
Dispatch->Interrupt=CONTAINING_RECORD(Current,KINTERRUPT,DispatchCode);
Handler=Dispatch->Interrupt->DispatchAddress;
if(Handler==Dispatch->ChainedDispatch)
Dispatch->Type=ChainConnect;//上次的中断对象是以链接方式挂上去的
elseif((Handler==Dispatch->InterruptDispatch)||
(Handler==Dispatch->FloatingDispatch))
{
Dispatch->Type=NormalConnect;//上次的中断对象是以普通方式挂上去的
}
else
Dispatch->Type=UnknownConnect;//不确定上次的中断对象是怎么挂上去的
}
}
下面这个函数返回当前cpu上指定中断向量处的isr。注意:任一时刻,每个isr可能是个模板isr,可能是个用户自定义的isr,也可能没有isr(即以KiUnexpectedInterruptN函数占位)。
PVOID
KeQueryInterruptHandler(INULONGVector)
{
PKIPCRPcr=(PKIPCR)KeGetPcr();
UCHAREntry;
Entry=HalVectorToIDTEntry(Vector);
return(PVOID)(((Pcr->IDT[Entry].ExtendedOffset<<16)&0xFFFF0000)|
(Pcr->IDT[Entry].Offset&0xFFFF));
}
真正的挂接操作是调用下面的函数完成的
VOIDKiConnectVectorToInterrupt(INPKINTERRUPTInterrupt,INCONNECT_TYPEType)//挂接类型
{
DISPATCH_INFODispatch;
PKINTERRUPT_ROUTINEHandler;//将要填到IDT表项中的最直接isr
KiGetVectorDispatch(Interrupt->Vector,&Dispatch);
if(Type==NoConnect)//if用户要撤销挂接
Handler=Dispatch.NoDispatch;
else
{
//填好本中断对象的dispatchisr
Interrupt->DispatchAddress=(Type==NormalConnect)?Dispatch.InterruptDispatch:
Dispatch.ChainedDispatch;
Handler=(PVOID)&Interrupt->DispatchCode;//本中断对象的模板isr
}
//将本中断对象的模板isr或者KiUnexpectedInterruptN填写到IDT的对应表项处。
//可以看出,通过IoConnectInterrupt函数,IDT中的每个isr都是最后一次挂接的中断对象的模板isr
KeRegisterInterruptHandler(Interrupt->Vector,Handler);
}
VOIDKeRegisterInterruptHandler(INULONGVector,INPVOIDHandler)
{
UCHAREntry;
ULONG_PTRAddress;
PKIPCRPcr=(PKIPCR)KeGetPcr();
Entry=HalVectorToIDTEntry(Vector);
Address=PtrToUlong(Handler);
//将isr填写到相应的表项中
Pcr->IDT[Entry].ExtendedOffset=(USHORT)(Address>>16);
Pcr->IDT[Entry].Offset=(USHORT)Address;
}
通过IoConnectInterrupt函数挂接的中断对象,都是将其模板isr填写到IDT表项中,这样,谁最后挂接,谁的模板isr就会最后覆写到那个表项处。如果使用了同一中断号的各个中断对象都是以链接方式挂接上去的,那么这些中断对象将组成一个链表。这样,当cpu收到对应的中断号时,会找到IDT中对应表项的isr给予执行。而那个isr就是最后挂接的中断对象的模板isr,这个模板isr的代码前面已看过,它将跳转进入下面的函数
VOIDFASTCALL
KiInterruptTemplateHandler(INPKTRAP_FRAMETrapFrame,//ecx
INPKINTERRUPTInterrupt)//edx
{
KiEnterInterruptTrap(TrapFrame);
((PKI_INTERRUPT_DISPATCH)Interrupt->DispatchAddress)(TrapFrame,Interrupt);//关键
}
看到没,每个中断对象的模板isr,会调用它的dispatchisr。以链接方式挂上去的中断对象的dispatchisr都是KiChainedDispatch,反之则是KiInterruptDispatch。我们看:
VOIDFASTCALL
KiChainedDispatch(INPKTRAP_FRAMETrapFrame,INPKINTERRUPTInterrupt)
{
KIRQLOldIrql;
BOOLEANHandled;
PLIST_ENTRYNextEntry,ListHead;
KeGetCurrentPrcb()->InterruptCount++;//递增中断计数
//HalBeginSystemInterrupt会提升irql至指定irql,以准备执行我们的isr
if(HalBeginSystemInterrupt(Interrupt->Irql,Interrupt->Vector,&OldIrql))//APIC相关
{
ListHead=&Interrupt->InterruptListEntry;
NextEntry=ListHead;
while(TRUE)//遍历中断对象链表,直至找到一个能处理这个中断的中断对象为止
{
if(Interrupt->SynchronizeIrql>Interrupt->Irql)//再次提升irql
OldIrql=KfRaiseIrql(Interrupt->SynchronizeIrql);
KxAcquireSpinLock(Interrupt->ActualLock);//加锁,保障多cpu互斥
//执行我们的isr(即用户自己提供的isr),注意返回值
Handled=Interrupt->ServiceRoutine(Interrupt,Interrupt->ServiceContext);
KxReleaseSpinLock(Interrupt->ActualLock);
if(Interrupt->SynchronizeIrql>Interrupt->Irql)
KfLowerIrql(OldIrql);
//if本中断对象认领了,且不许执行下一个中断对象就退出查找循环。
//(LevelSensitive即FALSE,一般的中断对象都这样)
if((Handled)&&(Interrupt->Mode==LevelSensitive))break;
NextEntry=NextEntry->Flink;
if(NextEntry==ListHead)//if链表中的最后一个中断对象
{
if(Interrupt->Mode==LevelSensitive)break;
if(!Handled)break;//if没能处理这个中断,退出循环
}
Interrupt=CONTAINING_RECORD(NextEntry,KINTERRUPT,InterruptListEntry);
}
KiExitInterrupt(TrapFrame,OldIrql,FALSE);
}
Else//清理中断Trap帧,恢复中断现场,回到原断点处继续执行
KiExitInterrupt(TrapFrame,OldIrql,TRUE);
}
用户自己的isr的原型是:
BOOLEANInterruptService(__instruct_KINTERRUPT*Interrupt,__inPVOIDServiceContex);
我们的这个isr应该根据ServiceContex判断这个中断是不是我们驱动中的设备发出的,若是,才能处理,返回TRUE。否则应返回FALSE,让系统继续寻找中断对象链表中的下一个中断对象去认领。
而对于以普通覆写方式挂上去的中断对象,它的dispatchisr是KiInterruptDispatch,我们看:
VOIDFASTCALL
KiInterruptDispatch(INPKTRAP_FRAMETrapFrame,INPKINTERRUPTInterrupt)
{
KIRQLOldIrql;
KeGetCurrentPrcb()->InterruptCount++;//递增中断计数
if(HalBeginSystemInterrupt(Interrupt->SynchronizeIrql,Interrupt->Vector,&OldIrql))
{
KxAcquireSpinLock(Interrupt->ActualLock);
//调用其自己的isr
Interrupt->ServiceRoutine(Interrupt,Interrupt->ServiceContext);
KxReleaseSpinLock(Interrupt->ActualLock);
KiExitInterrupt(TrapFrame,OldIrql,FALSE);
}
else
KiExitInterrupt(TrapFrame,OldIrql,TRUE);
}
看到没,普通方式挂上去的中断对象,它独*断号,当发生相应中断时,系统简单执行一下它自己的isr后,就返回了,不会有在链表中查找的过程。
底层驱动在卸载时,往往要撤销挂接的那些中断,我们看下中断对象如如何撤销挂接的。
VOIDIoDisconnectInterrupt(PKINTERRUPTInterruptObject)
{
LONGi;
PIO_INTERRUPTIoInterrupt;
PAGED_CODE();
IoInterrupt=CONTAINING_RECORD(InterruptObject,IO_INTERRUPT,FirstInterrupt);
KeDisconnectInterrupt(&IoInterrupt->FirstInterrupt);//撤销第一个中断对象的挂接
for(i=0;i<KeNumberProcessors;i++)//撤销其它中断对象的挂接
{
if(IoInterrupt->Interrupt[i])
KeDisconnectInterrupt(&InterruptObject[i]);
}
ExFreePool(IoInterrupt);//释放整个中断对象数组加头部占用的那块内存
}
BOOLEANKeDisconnectInterrupt(INPKINTERRUPTInterrupt)
{
KIRQLOldIrql,Irql;
ULONGVector;
DISPATCH_INFODispatch;
PKINTERRUPTNextInterrupt;
BOOLEANState;
KeSetSystemAffinityThread(1<<Interrupt->Number);
OldIrql=KiAcquireDispatcherLock();
State=Interrupt->Connected;
if(State)
{
Irql=Interrupt->Irql;
Vector=Interrupt->Vector;
KiGetVectorDispatch(Vector,&Dispatch);//获取上次的挂接信息
if(Dispatch.Type==ChainConnect)
{
ASSERT(Irql<=SYNCH_LEVEL);
if(Interrupt==Dispatch.Interrupt)//if要撤销挂接的中断对象就是最近挂接的那个
{
Dispatch.Interrupt=CONTAINING_RECORD(Dispatch.Interrupt->
InterruptListEntry.Flink,KINTERRUPT,InterruptListEntry);
KiConnectVectorToInterrupt(Dispatch.Interrupt,ChainConnect);
}
//关键。脱出链表
RemoveEntryList(&Interrupt->InterruptListEntry);
NextInterrupt=CONTAINING_RECORD(Dispatch.Interrupt->InterruptListEntry.Flink,
KINTERRUPT,InterruptListEntry);
if(Dispatch.Interrupt==NextInterrupt)//也即if链表中就剩下一个中断对象了
{
KiConnectVectorToInterrupt(Dispatch.Interrupt,NormalConnect);//改为普通方式
}
}
Else//if原来本身就只挂着一个中断对象
{
HalDisableSystemInterrupt(Interrupt->Vector,Irql);
KiConnectVectorToInterrupt(Interrupt,NoConnect);//改为KiUnexpectedInterruptN
}
Interrupt->Connected=FALSE;
}
KiReleaseDispatcherLock(OldIrql);
KeRevertToUserAffinityThread();
returnState;
}
通过IoConnectInterrupt函数挂接注册中断,确实为程序员减轻了大量负担。通过这种方式注册的isr,分三层。第一层是中断对象的模板isr,第二层是中断对象的dispatchisr,第三层才是用户自己提供的isr。每当发生中断时,系统逐层调用这三层isr。因此,也可以说,我们提供的那个isr被系统托管了,IDT表项中的isr是系统的托管isr。
当然,程序员,也可以直接修改IDT中的表项,改成自己的isr,这就是所谓的isrhook(注意要进行isrhook的话,必须每个cpu都要hook)
最后我们看一下典型的系统时钟中断是怎么处理的。系统每隔10ms产生一次时钟中断,时钟中断的IRQ固定是0,中断号默认映射到0x30,时钟中断的isr最终进入下面的函数。
VOIDFASTCALL
HalpClockInterruptHandler(INPKTRAP_FRAMETrapFrame)
{
KIRQLIrql;
KiEnterInterruptTrap(TrapFrame);
//提升irql至CLOCK2_LEVEL
if(HalBeginSystemInterrupt(CLOCK2_LEVEL,0x30,&Irql))//0x30就是时钟中断的中断号
{
/*Updatetheperformancecounter*/
HalpPerfCounter.QuadPart+=HalpCurrentRollOver;
HalpPerfCounterCutoff=KiEnableTimerWatchdog;
KeUpdateSystemTime(TrapFrame,HalpCurrentTimeIncrement,Irql);//关键函数
}
KiEoiHelper(TrapFrame);
}
VOIDFASTCALL
KeUpdateSystemTime(INPKTRAP_FRAMETrapFrame,INULONGIncrement,INKIRQLIrql)
{
PKPRCBPrcb=KeGetCurrentPrcb();
ULARGE_INTEGERCurrentTime,InterruptTime;
ULONGHand,OldTickCount;
//更新启动以来的运行时间计数
InterruptTime.HighPart=SharedUserData->InterruptTime.High1Time;
InterruptTime.LowPart=SharedUserData->InterruptTime.LowPart;
InterruptTime.QuadPart+=Increment;//Increment一般为10ms
SharedUserData->InterruptTime.High1Time=InterruptTime.HighPart;
SharedUserData->InterruptTime.LowPart=InterruptTime.LowPart;
SharedUserData->InterruptTime.High2Time=InterruptTime.HighPart;
OldTickCount=KeTickCount.LowPart;
InterlockedExchangeAdd(&KiTickOffset,-(LONG)Increment);//递减
/*Checkforincompletetick*/
if(KiTickOffset<=0)
{
//更新系统时间
CurrentTime.HighPart=SharedUserData->SystemTime.High1Time;
CurrentTime.LowPart=SharedUserData->SystemTime.LowPart;
CurrentTime.QuadPart+=KeTimeAdjustment;
SharedUserData->SystemTime.High2Time=CurrentTime.HighPart;
SharedUserData->SystemTime.LowPart=CurrentTime.LowPart;
SharedUserData->SystemTime.High1Time=CurrentTime.HighPart;
//更新Tick计数
CurrentTime.HighPart=KeTickCount.High1Time;
CurrentTime.LowPart=OldTickCount;
CurrentTime.QuadPart+=1;//递增Tick计数
KeTickCount.High2Time=CurrentTime.HighPart;
KeTickCount.LowPart=CurrentTime.LowPart;
KeTickCount.High1Time=CurrentTime.HighPart;
//更新Tick计数(供应用程序GetTickCount访问)
SharedUserData->TickCount.High2Time=CurrentTime.HighPart;
SharedUserData->TickCount.LowPart=CurrentTime.LowPart;
SharedUserData->TickCount.High1Time=CurrentTime.HighPart;
Hand=OldTickCount&(TIMER_TABLE_SIZE-1);
if(KiTimerTableListHead[Hand].Time.QuadPart<=InterruptTime.QuadPart)
{
if(!Prcb->TimerRequest)
{
Prcb->TimerRequest=(ULONG_PTR)TrapFrame;
Prcb->TimerHand=Hand;
HalRequestSoftwareInterrupt(DISPATCH_LEVEL);
}
}
OldTickCount++;
}
//下面是关键。检查是否有到点的定时器
Hand=OldTickCount&(TIMER_TABLE_SIZE-1);
//if有到点的定时器
if(KiTimerTableListHead[Hand].Time.QuadPart<=InterruptTime.QuadPart)
{
if(!Prcb->TimerRequest)
{
Prcb->TimerRequest=(ULONG_PTR)TrapFrame;//关键。插入一个定时器dpc
Prcb->TimerHand=Hand;
HalRequestSoftwareInterrupt(DISPATCH_LEVEL);//发出一个DPC中断请求
}
}
if(KiTickOffset<=0)
{
KiTickOffset+=KeMaximumIncrement;
KeUpdateRunTime(TrapFrame,Irql);
}
else
{
Prcb->InterruptCount++;
}
KiEndInterrupt(Irql,TrapFrame);
}
上面的函数在发现有定时器到点后,就会发出一个DPC,然后在系统定时器中断退出后,就会扫描执行DPC队列,调用KiTimerExpiration函数扫描所有到点的定时器,然后唤醒在那些定时器上等待的线程。
KiTimerExpiration函数的代码,有兴趣的读者自己看。