http://flier_lu.blogone.net/?id=1397656
x86架构设计在上是基于中断思想的,因而从dos到win32,操作系统中大量使用中断的概念来表达异步操作的行为。但与dos下独占的情况不同,win32下需要由系统对多任务进行调度,因此中断响应代码必须尽可能地简单,并且尽快的将控制权交还给系统。虽然这样一来系统调度的响应速度和实现过程方便了,但还是有很多功能需要在中断响应中完成。为此,win32核心提供了dpc(deferred procedure call)和apc(asynchronous procedure call)两个irql特殊的软件中断级别,用于实现延迟和异步的过程调用。
从irql分层来说,dpc和apc是介于较高级别的设备中断和最低级别的passive中断之间,由操作系统用于完成特殊方法调用的中断级别。与处理硬件操作的设备中断和更高级别的时钟、处理器中断不同,这两级中断纯粹是为了实现功能调用异步性而设计实现的,因此操作系统本身也对它们具有很强的依赖型。apc这里暂且不讨论,以后有机会再写篇文章专门讨论 :)
dpc在功能上可以理解为isr(interrupt service routine)的一部分。只是因为isr为了尽量简单和返回控制权给操作系统,而将一部分功能剥离出来放入相应dpc中,延迟调用。因为dpc的irql仅在apc和passive中断之上,所以系统可以从容地处理完高级别的中断后,再在dpc一级慢慢处理积累起来的相对并不那么紧急功能。
dpc在使用上可以理解为一个回调函数的封装对象。系统本身或者设备驱动程序,在合适的地方如设备驱动程序的adddevice函数或dispatchpnp函数处理irp_mn_start_device请求时,初始化一个dpc对象;在isr中判断是否需要进一步处理中断,是则请求将dpc对象插入到系统dpc队列中;系统处理完高irql后,会在irql dispatch_level级别慢慢处理dpc队列中的dpc对象;每个dpc对象封装的回调函数,会使用同时封装的调用参数,被系统调用,完成在isr中来不及完成的工作;如果需要进一步的工作,还可以继续请求插入dpc对象到dpc队列中。
dpc对象从最终用户角度有两种:dpcforisr和customdpc。前者是与设备驱动对象(device object)绑定的;后者则由驱动自行维护。但从实现上来说,只有一种dpc对象存在,dpcforisr所涉及的维护函数,实际上都是对customdpc的一个封装而已。
我们首先来看看初始化dpc对象的实现。keinitializedpc函数(ntos\ke\dpcobj.c:39)完成具体的dpc对象的初始化,实际上就是填充一个内存结构kdpc(ntos\inc\ntosdef.h:331)。
以下为引用:
//
// deferred procedure call (dpc) object
//
typedef struct _kdpc {
cshort type;
uchar number;
uchar importance;
list_entry dpclistentry;
pkdeferred_routine deferredroutine;
pvoid deferredcontext;
pvoid systemargument1;
pvoid systemargument2;
pulong_ptr lock;
} kdpc, *pkdpc, *restricted_pointer prkdpc;
type 表示此内核对象的类型,在kobjects枚举类型(ntos\inc\ke.h:122)中定义,缺省为 dpcobject = 0x13。此外winxp/2003新增了一种threadeddpcobject = 0x18
number 在多处理器环境下用于指定此dpc对象加入到哪个处理器的dpc队列中,我们等会讨论多处理器时详细描述。缺省为 0
importance 表示此dpc对象的重要性,在kdpc_importance枚举类型(ntos\inc\ntosdef.h:321)中定义,缺省为 mediumimportance = 1
dpclistentry 是用于维护dpc队列的链表指针
deferredroutine 是此dpc对象绑定的回调函数,后面deferredcontext、systemargument1和systemargument2分别是此回调函数被调用时的参数。如isr中调用iorequestdpc时,后面两个参数就用于传递irp和context参数给dpc的回调函数。
lock 保存此dpc对象所在dpc队列的自旋锁,用于锁定dpc队列,同时也用于判断此dpc对象是否被加入到一个dpc队列中。
了解了kdpc对象的结构,实际上维护代码就非常简单了。keinitializedpc函数将kdpc对象结构初始化为初值;ioinitializedpcrequest函数则只是对keinitializedpc函数的一个简单包装,如下
以下为引用:
#define ioinitializedpcrequest( deviceobject, dpcroutine ) (\
keinitializedpc( &(deviceobject)->dpc, \
(pkdeferred_routine) (dpcroutine), \
(deviceobject) ) )
注意winxp/2003下实际上keinitializedpc函数和keinitializethreadeddpc函数都是由一个kiinitializedpc函数完成具体工作的,只是传递的最后一个参数定义的对象类型不同。
keinsertqueuedpc函数(ntos\ke\dpcobj.c:89)实际上是系统对dpc队列维护的核心函数,其伪代码如下:
以下为引用:
boolean keinsertqueuedpc (in prkdpc dpc, in pvoid systemargument1,in pvoid systemargument2)
{
pkspin_lock lock;
kirql oldirql;
keraiseirql(high_level, &oldirql); // 提升当前irql到最高,屏蔽其它中断
pkprcb = kegetcurrentprcb(); // 获取当前处理器控制块
// 通过比较dpc->lock是否为空,来判断此dpc对象是否已经被加入到dpc队列;
// 如果dpc对象可以被加入到队列,则将当前处理器控制块的dpc自旋锁复制到dpc->lock中
if ((lock = interlockedcompareexchangepointer(&dpc->lock, &prcb->dpclock, null)) == null)
{
// 更新当前处理器控制块的统计信息
prcb->dpccount += 1;
prcb->dpcqueuedepth += 1;
// 更新dpc对象的参数信息
dpc->systemargument1 = systemargument1;
dpc->systemargument2 = systemargument2;
// 根据dpc对象优先级,决定将之加入到dpc队列的头部或尾部
if (dpc->importance == highimportance)
insertheadlist(&prcb->dpclisthead, &dpc->dpclistentry);
else
inserttaillist(&prcb->dpclisthead, &dpc->dpclistentry);
// 如果当前处理器没有dpc对象活动或dpc中断请求,则进一步判断是否发出dpc中断请求
if (prcb->dpcroutineactive == false && prcb->dpcinterruptrequested == false)
{
// 如果dpc对象优先级为中高;
// 或者dpc队列长度超过阈值maximumdpcqueuedepth;
// 或者dpc请求速率小于阈值minimumdpcrate
if ((dpc->importance != lowimportance) ||
(prcb->dpcqueuedepth >= prcb->maximumdpcqueuedepth) ||
(prcb->dpcrequestrate < prcb->minimumdpcrate))
{
// 满足触发条件,则发出dpc中断请求
prcb->dpcinterruptrequested = true;
kirequestsoftwareinterrupt(dispatch_level);
}
}
}
kelowerirql(oldirql);
return (lock == null);
}
这里的几个阈值,在kiinitializekernel函数(ntos\ke\i386\kernlini.c:246)中,根据全局变量kimaximumdpcqueuedepth、kiminimumdpcrate和kiadjustdpcthreshold确定。而这几个全局变量可以通过注册表项(hkey_local_machine\system\currentcontrolset\control\session manager\kernel\)下的dpcqueuedepth、minimumdpcrate和adjustdpcthreshold三个键值来设置。具体的设置方法,请参考msdn以及性能计数器的processor\% dpc time等动态指数。
而处理与驱动绑定的dpc对象的iorequestdpc函数只是keinsertqueuedpc函数的一个简单包装。
以下为引用:
#define iorequestdpc( deviceobject, irp, context ) ( \
keinsertqueuedpc( &(deviceobject)->dpc, (irp), (context) ) )
与keinsertqueuedpc函数对应的keremovequeuedpc函数(ntos\ke\dpcobj.c:272)实际上只是完成简单的将dpc对象从dpc队列中删除的功能。
最后对dpc对象属性进行修改的kesetimportancedpc函数(ntos\ke\dpcobj.c:367)和kesettargetprocessordpc函数(ntos\ke\dpcobj.c:401)实际上都是直接修改dpc对象结构的相应域。kdpc::number大于maximum_processors = 32时,用于指定dpc对象的目标cpu。如调用kesettargetprocessordpc(pkdpc, 2)后,pkdpc = maximum_processors + 2。
在了解了dpc对象和dpc队列的大致维护函数功能后,我们来看看稍微复杂一些的在多处理器下dpc队列的维护流程。
前面提到kdpc::number指定了dpc对象所用的处理器号,因此在keinsertqueuedpc函数开始获取处理器控制块时,需要判断number是否指向一个处理器,并从全局处理器控制块列表中获取相应的处理器控制块,为代码如下:
以下为引用:
if (dpc->number >= maximum_processors) // number大于maximum_processors时用于指定处理器
{
processor = dpc->number - maximum_processors;
prcb = kiprocessorblock[processor]; // 全局唯一的处理器控制块列表
}
else
{
prcb = kegetcurrentprcb();
}
kiacquirespinlock(&prcb->dpclock); // 使用自旋锁保护处理器控制块中的dpc队列