我们知道外设如果想要通知CPU的读取数据的话,都是通过中断来实现的,例如我们使用键盘的时候,键盘按键就会导致中断通知CPU,然后CPU通过中断处理程序读取键盘的状态来判断按键信息。
其实对于Windows驱动来说,中断也是一种资源。我们在IRP_MJ_START
中,可以提取中断资源。
中断资源的提取和配置过程如下:
VOID OnStartDevice(...)
{
ULONG vector; // interrupt vector
KIRQL irql; // interrupt level
KINTERRUPT_MODE mode; // latching mode
KAFFINITY affinity; // processor affinity
BOOLEAN irqshare; // shared interrupt
for (ULONG i = 0; i < nres; ++i, ++resource)
{
switch (resource->Type)
{
case CmResourceTypeInterrupt:
irql = (KIRQL)resource->u.Interrupt.Level;
vector = resource->u.Interrupt.Vector;
affinity = resource->u.Interrupt.Affinity;
mode = (resource->Flags == CM_RESOURCE_INTERRUPT_LATCHED) ? Latched : LevelSensitive;
irqshare = resource->ShareDisposition == CmResourceShareShared;
break;
}
status = IoConnectInterrupt(&pdx->InterruptObject,
(PKSERVICE_ROUTINE)OnInterrupt,
(PVOID)pdx,
NULL,
vector,
irql,
irql,
mode,
irqshare,
affinity,
FALSE);
}
}
BOOLEAN OnInterrupt(...)
{
IoRequestDpc(pdx->DeviceObject, NULL, (PVOID) pdx);
}
VOID DpcForIsr(...)
{
PIRP Irp = fdo->CurrentIrp;
IoStartNextPacket(fdo, TRUE);
IoCompleteRequest(Irp, <boost value>);
}
说明
Level
参数指定这个中断的IRQL。Vector
参数指定这个中断的硬件中断矢量。Affinity
是一个位掩码,它指出该中断应由哪个CPU来处理。IoConnectInterrupt
我们的中断是边缘触发方式还是水平触发方式。如果资源标志Flags
为CM_RESOURCE_INTERRUPT_LATCHED
,则我们的中断是边缘触发方式,否则是水平触发方式。在这段代码的最后是IoConnectInterrupt
调用,在这里我们使用了从中断资源描述符中获得的各个值。
&pdx->InterruptObject
)指出在哪里存放连接操作的结果,即一个指向内核中断对象的指针,该对象描述你的中断。OnInterrupt
)指向你的中断服务例程(ISR);稍后我将讨论ISR。pdx
)是每次当你的设备中断时作为参数传递给ISR的一个上下文值。vector
和irql
)为被连接的中断指定中断矢量号和中断请求级。mode
)应该为Latched
或LevelSensitive
,它指出中断是边缘触发方式还是水平触发方式。TRUE
,则表明这个中断可以被其它设备共享,反之为FALSE
。affinity
)是该中断的处理器亲合掩码。这里有两个IoConnectInterrupt
的参数还没有描述。当设备使用多个中断时,这两个参数就变得特别重要。在这种情况下,你需要为这些中断创建自旋锁,并调用KeInitializeSpinLock
函数初始化这个自旋锁。在连接中断前,你还需要计算出这些中断中的最大IRQL。在每次调用IoConnectInterrupt
时,你应该在第四个参数中指定这个自旋锁的地址,以及在第七个参数中指定最大IRQL。第七个参数指定的IRQL用于同步多个中断,所以你应该使用这些中断中最大的IRQL,这样你一次就只需要响应一个中断。然而,如果设备只使用一个中断,你就不必指定自旋锁(因为I/O管理器自动为你分配一个),并且中断的同步级别将与中断的IRQL相同。
为了防止中断服务例程调用占用时间太长导致其他中断丢失,一般来说,中断服务例程仅仅设置一个DPC(IoRequestDpc
),我们在DPC例程中才完成所有的操作。
IoConnectInterrupt
这个函数是用来注册一个中断并设置中断对象的回调函数的,这里我们来看一下这个函数的基本流程:
NTSTATUS
IoConnectInterrupt(
OUT PKINTERRUPT *InterruptObject,
IN PKSERVICE_ROUTINE ServiceRoutine,
IN PVOID ServiceContext,
IN PKSPIN_LOCK SpinLock OPTIONAL,
IN ULONG Vector,
IN KIRQL Irql,
IN KIRQL SynchronizeIrql,
IN KINTERRUPT_MODE InterruptMode,
IN BOOLEAN ShareVector,
IN KAFFINITY ProcessorEnableMask,
IN BOOLEAN FloatingSave
)
{
CCHAR count;
BOOLEAN builtinUsed;
PKINTERRUPT interruptObject;
KAFFINITY processorMask;
NTSTATUS status;
PIO_INTERRUPT_STRUCTURE interruptStructure;
PKSPIN_LOCK spinLock;
*InterruptObject = (PKINTERRUPT) NULL;
status = STATUS_SUCCESS;
//计算中断向量应该对应多少个CPU
processorMask = ProcessorEnableMask & KeActiveProcessors;
count = 0;
while (processorMask) {
if (processorMask & 1) {
count++;
}
processorMask >>= 1;
}
//创建中断管理的数据结构
if (count) {
interruptStructure = ExAllocatePoolWithTag( NonPagedPool,
((count - 1) * sizeof( KINTERRUPT )) +
sizeof( IO_INTERRUPT_STRUCTURE ),
'nioI' );
if (interruptStructure == NULL) {
return STATUS_INSUFFICIENT_RESOURCES;
}
} else {
return STATUS_INVALID_PARAMETER;
}
//是否提供了自旋锁
if (ARGUMENT_PRESENT( SpinLock )) {
spinLock = SpinLock;
} else {
spinLock = &interruptStructure->SpinLock;
}
//初始化中断管理结构
*InterruptObject = &interruptStructure->InterruptObject;
interruptObject = (PKINTERRUPT) (interruptStructure + 1);
builtinUsed = FALSE;
processorMask = ProcessorEnableMask & KeActiveProcessors;
RtlZeroMemory( interruptStructure, sizeof( IO_INTERRUPT_STRUCTURE ) );
//针对每一个CPU,来注册中断向量
for (count = 0; processorMask; count++, processorMask >>= 1) {
if (processorMask & 1) {
//初始化中断向量
KeInitializeInterrupt( builtinUsed ?
interruptObject :
&interruptStructure->InterruptObject,
ServiceRoutine,
ServiceContext,
spinLock,
Vector,
Irql,
SynchronizeIrql,
InterruptMode,
ShareVector,
count,
FloatingSave );
//连接中断向量
if (!KeConnectInterrupt( builtinUsed ?
interruptObject :
&interruptStructure->InterruptObject )) {
if (builtinUsed) {
IoDisconnectInterrupt( &interruptStructure->InterruptObject );
} else {
ExFreePool( interruptStructure );
}
status = STATUS_INVALID_PARAMETER;
break;
}
//设置中断向量指针
if (builtinUsed) {
interruptStructure->InterruptArray[count] = interruptObject++;
} else {
builtinUsed = TRUE;
}
}
}
if (!NT_SUCCESS( status )) {
*InterruptObject = (PKINTERRUPT) NULL;
}
return status;
}
上面的流程比较简单,主要是三个:
PIO_INTERRUPT_STRUCTURE
来管理中断信息。KeInitializeInterrupt
来初始化一个中断向量。KeConnectInterrupt
来连接一个中断向量。其中中断管理的数据结构如下:
typedef struct _IO_INTERRUPT_STRUCTURE {
KINTERRUPT InterruptObject;
PKINTERRUPT InterruptArray[MAXIMUM_PROCESSORS];
KSPIN_LOCK SpinLock;
} IO_INTERRUPT_STRUCTURE, *PIO_INTERRUPT_STRUCTURE;
typedef struct _KINTERRUPT {
CSHORT Type;
CSHORT Size;
LIST_ENTRY InterruptListEntry;
PKSERVICE_ROUTINE ServiceRoutine;
PVOID ServiceContext;
KSPIN_LOCK SpinLock;
ULONG TickCount;
PKSPIN_LOCK ActualLock;
PKINTERRUPT_ROUTINE DispatchAddress;
ULONG Vector;
KIRQL Irql;
KIRQL SynchronizeIrql;
BOOLEAN FloatingSave;
BOOLEAN Connected;
CCHAR Number;
BOOLEAN ShareVector;
KINTERRUPT_MODE Mode;
ULONG ServiceCount;
ULONG DispatchCount;
#if defined(_AMD64_)
PKTRAP_FRAME TrapFrame;
PVOID Reserved;
ULONG DispatchCode[DISPATCH_LENGTH];
#else
ULONG DispatchCode[DISPATCH_LENGTH];
#endif
} KINTERRUPT;
KeInitializeInterrupt
这个函数用来初始化一个中断向量,流程很简单,就是初始化一个KINTERRUPT
结构,代码如下:
VOID
KeInitializeInterrupt (
__out PKINTERRUPT Interrupt,
__in PKSERVICE_ROUTINE ServiceRoutine,
__in_opt PVOID ServiceContext,
__out_opt PKSPIN_LOCK SpinLock,
__in ULONG Vector,
__in KIRQL Irql,
__in KIRQL SynchronizeIrql,
__in KINTERRUPT_MODE InterruptMode,
__in BOOLEAN ShareVector,
__in CCHAR ProcessorNumber,
__in BOOLEAN FloatingSave
)
{
LONG Index;
PULONG pl;
PULONG NormalDispatchCode;
Interrupt->Type = InterruptObject;
Interrupt->Size = sizeof(KINTERRUPT);
Interrupt->ServiceRoutine = ServiceRoutine;
Interrupt->ServiceContext = ServiceContext;
if (ARGUMENT_PRESENT(SpinLock)) {
Interrupt->ActualLock = SpinLock;
} else {
KeInitializeSpinLock (&Interrupt->SpinLock);
Interrupt->ActualLock = &Interrupt->SpinLock;
}
Interrupt->Vector = Vector;
Interrupt->Irql = Irql;
Interrupt->SynchronizeIrql = SynchronizeIrql;
Interrupt->Mode = InterruptMode;
Interrupt->ShareVector = ShareVector;
Interrupt->Number = ProcessorNumber;
Interrupt->FloatingSave = FloatingSave;
Interrupt->TickCount = (ULONG)-1;
Interrupt->DispatchCount = (ULONG)-1;
NormalDispatchCode = &(Interrupt->DispatchCode[0]);
pl = NormalDispatchCode;
for (Index = 0; Index < NORMAL_DISPATCH_LENGTH; Index += 1) {
*NormalDispatchCode++ = KiInterruptTemplate[Index];
}
pl = (PULONG)((PUCHAR)pl + ((PUCHAR)&KiInterruptTemplateObject -
(PUCHAR)KiInterruptTemplate) -4);
*pl = (ULONG)Interrupt;
KeSweepDcache(FALSE);
Interrupt->Connected = FALSE;
return;
}
KeConnectInterrupt
这个函数是向CPU注册一个中断向量,这个函数逻辑很复杂,但是处理的流程比较简单,先设置线程到指定的中断向量注册的处理器,然后调用KiSetHandlerAddressToIDT
设置中断向量信息。
#define KiSetHandlerAddressToIDT(Vector, HandlerAddress) {\
UCHAR IDTEntry = HalVectorToIDTEntry(Vector); \
ULONG Ha = (ULONG)HandlerAddress; \
KeGetPcr()->IDT[IDTEntry].ExtendedOffset = HIGHWORD(Ha); \
KeGetPcr()->IDT[IDTEntry].Offset = LOWWORD(Ha); \
}
当中断到来的时候,就会调用_KiInterruptDispatch
来分发中断信息,这个函数中就会调用我们中断回调例程。