windows内核原理分析之DPC函数的执行(1)
当发生中断时,有些操作本来应该在中断服务程序中完成,但是实际上却是在退出中断服务程序之后在一个DPC函数中完成的。DPC是”Deferred Procedure Call”的缩写,意为推迟了的过程(函数)调用。这是因为,逻辑上应该放在中断服务程序中完成的操作并非都是那么紧迫,其中有一部分可能相对而言不那么紧迫,而又比较费时间,实际上可以放在开中断的条件下执行。如果把这些操作都放在中断服务程序中,就会使关闭中断的时间太长而引起中断请求的丢失,因为整个中断服务程序通常都是在关中断的条件下执行的。为此,把中断服务程序中不那么紧迫却比较费时,而又不必在关中断条件下执行的操作分割出来,放在另一个函数中,在开中断的条件下加以执行,就可以缩短关中断的时间。这样的函数就是DPC函数。一般而言,中断服务前期的操作是比较紧迫的,并且是必须关中断的,此时可以很快地对外部设备进行操作。此后,剩下的那部分操作便可以稍后在开中断的条件下执行。所以有人曾经把这部分操作称为中断服务的”后半(Bottom Half)”,也有人把这两半分别称为”硬中断”和”软中断”。之所以要把中断服务分成前后两半,是因为一次中断服务的后半不如另一次中断的前半那么紧迫。
为此,内核中要有个DPC请求队列,中断服务程序执行完它的”前半”之后就把一个DPC请求挂入这个队列,要求内核调用相应的DPC函数,然后(形式上)就从中断返回了。接着,如果没有别的中断请求,内核就会扫描这个DPC请求队列,依次在开中断的条件下执行这些DPC函数,直至又发生中断或执行完队列中的所有DPC函数。至于当前线程所要执行的程序,则只有在DPC请求队列为空的时候才会继续得到执行。显然,这里所体现的是”急事急办”的原则,中断是最急的,DPC函数其次,最后才是当前线程。Windows内核的IRQL(即运行级别)就反映了这些活动的轻重缓急,DPC函数是在DISPATCH_LEVEL级别上执行的。
与DPC函数的执行有关的另一个问题是堆栈的使用。我们知道,中断服务程序所使用的堆栈就是当前线程的系统空间堆栈。中断服务程序一般都是比较轻小的,占用一下当前线程的堆栈不至于会有问题;但是DPC函数就不同了,DPC函数有可能是比较大的,如果仍旧占用当前线程的堆栈,在最坏的情况下有可能造成堆栈溢出,所以最好是为DPC函数的执行另外配备一个堆栈。
Windows内核把DPC请求队列放在每个CPU的PRCB数据结构中。此外,PRCB结构中还有一些别的与DPC有关的字段:
typedef struct _KPRCB
{
......
ULONG DpcTime;
ULONG DebugDpcTime;
ULONG InterruptTime;
ULONG AdjustDpcThreshold;
......
struct _KDPC_DATA DpcData[2]; //两个DPC请求队列
PVOID DpcStack;
ULONG MaximumDpcQueueDepth;
ULONG DpcRequestRate;
ULONG MinimumDpcRate;
volatile UCHAR DpcInterruptRequested;
volatile UCHAR DpcThreadRequested;
volatile UCHAR DpcRoutineActive;
volatile UCHAR DpcThreadActive;
ULONG PrcbLock;
ULONG DpcLastCount;
......
PVOID DpcThread;
KEVENT DpcEvent;
UCHAR ThreadDpcEnable;
......
LONG DpcSetEventRequest;
......
KDPC CallDpc;
......
} KPRCB, *PKPRCB;
其中的DpcData[2]是大小为2的_KDPC_DATA结构数组,其结构定义如下:
typedef struct _KDPC_DATA
{
LIST_ENTRY DpcListHead; //用于DPC请求队列的队列头
ULONG DpcLock;
volatile ULONG DpcQueueDepth;
ULONG DpcCount;
} KDPC_DATA, *PKDPC_DATA;
可见,每个_KDPC_DATA结构中都有个队列头,这就是用于DPC请求队列的。
DpcData[]的大小为2,说明有两个DPC请求队列,它们是:
#define DPC_NORMAL 0
#define DPC_THREADED 1
显然,其中之一是”常规”的DPC请求队列,另一个是”线程化”的DPC请求队列。目前ReactOS已经实现的是常规DPC请求。
设备对象的数据结构DEVICE_OBJECT中有个成分Dpc,这是个KDPC数据结构,用来设置有关本设备对象的DPC函数的信息:
typedef struct _KDPC
{
UCHAR Type; //DpcObject或ThreadedDpcObject
UCHAR Importance; //紧迫程度
USHORT Number; //CPU号码(在多处理器系统中)
LIST_ENTRY DpcListEntry; //用来挂入DPC请求队列
PKDEFERRED_ROUTINE DeferredRoutine; //指向具体的DPC函数
PVOID DeferredContext; //执行DPC函数时的上下文
PVOID SystemArgument1; //执行DPC函数时的参数
PVOID SystemArgument2;
volatile PVOID DpcData; //指向所挂入的KDPC_DATA结构
} KDPC, *PKDPC, *RESTRICTED_POINTER PRKDPC;
设备对象在初始化时通过KeInitializeDpc()设置好它的KDPC数据结构。
VOID NTAPI
KeInitializeThreadedDpc(IN PKDPC Dpc, IN
PKDEFERRED_ROUTINE DeferredRoutine,
IN PVOID DeferredContext)
{
/* Call the internal routine */
KiInitializeDpc(Dpc, DeferredRoutine, DeferredContext,
ThreadedDpcObject);
}
VOID NTAPI
KiInitializeDpc(IN PKDPC Dpc, IN PKDEFERRED_ROUTINE DeferredRoutine,
IN PVOID DeferredContext, IN KOBJECTS Type)
{
/* Setup the DPC Object */
Dpc->TypeType = Type;
Dpc->Number = 0;
Dpc->Importance= MediumImportance;
Dpc->DeferredRoutineDeferredRoutine = DeferredRoutine;
Dpc->DeferredContextDeferredContext = DeferredContext;
Dpc->DpcData = NULL;
}