Windows DPC详解

news/2024/12/24 1:01:44

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;

1、Type 表示此内核对象的类型,在KOBJECTS枚举类型(ntos/inc/ke.h:122)中定义,缺省为 DpcObject = 0x13。此外WinXP/2003新增了一种ThreadedDpcObject = 0x18

2、 Number 在多处理器环境下用于指定此DPC对象加入到哪个处理器的DPC队列中,我们等会讨论多处理器时详细描述。缺省为 0

3、Importance 表示此DPC对象的重要性,在KDPC_IMPORTANCE枚举类型(ntos/inc/ntosdef.h:321)中定义,缺省为 MediumImportance = 1

4、DpcListEntry 是用于维护DPC队列的链表指针

5、 DeferredRoutine 是此DPC对象绑定的回调函数,后面DeferredContext、SystemArgument1和SystemArgument2分别是此回调函数被调用时的参数。如ISR中调用IoRequestDpc时,后面两个参数就用于传递Irp和Context参数给DPC的回调函数。

6、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队列

而在KeInsertQueueDpc函数中判断是否发出DPC中断请求时,也需要做更复杂的逻辑判断。

对DPC对象目标处理器就是当前处理器的情况,可以和前面单处理器时一样处理,直接发送DPC中断请求;但对于DPC对象目标处理器是其他处理器的情况,就必须使用KiIpiSend函数发送IPI(InterProcessor Interrupt)中断,通知目标处理器执行动作。此IPI中断是介于系统掉电中断(POWER_LEVEL)和时钟中断之间的特殊IRQL,专门用于在多处理器情况下协调多个处理器的工作。

此外就是在多处理器情况下,各种对DPC队列的操作都需要用此处理器控制块的DPC队列自旋锁保护起来,避免同步问题。

由此我们可以看到,实际上DPC队列是每个处理器一个的,我们完全可以将某个DPC对象绑定到某个处理器上,实现类似线程亲缘性(Thread Affinity)的效果,优化在多处理器环境下的性能。但这同时也带来一个问题,就是ISR程序可以和DPC回调函数同时被调用,某种程度上也造成了开发复杂度的增加,具体处理方法请参考DDK中相关文档。


Kernel-Mode Driver Architecture/Design Guide/Servicing Interrupts/DPC Objects and DPCs


http://www.niftyadmin.cn/n/3059536.html

相关文章

Linux下查看CPU型号,内存大小,硬盘空间的命令(详解)

1 查看CPU 1.1 查看CPU个数 # cat /proc/cpuinfo | grep "physical id" | uniq | wc -l 2 **uniq命令&#xff1a;删除重复行;wc –l命令&#xff1a;统计行数** 1.2 查看CPU核数 # cat /proc/cpuinfo | grep "cpu cores" | uniq cpu cores : 4 1.3 查看CP…

加拿大己亥猪年生肖邮票亮相 选用猪八戒形象

农历己亥年临近&#xff0c;加拿大邮政公司于当地时间1月17日正式揭晓新的猪年生肖邮票。这套邮票选用了中国古典名著《西游记》中猪八戒的形象。图为印有“福满人间”“笑口常开”“和气生财”及“瑞气祥和”字样的加拿大国内版己亥猪年生肖邮票四方联&#xff0c;以及国际版小…

『React Navigation 3x系列教程』createDrawerNavigator开发指南

这篇文章将向大家分享createDrawerNavigator的一些开发指南和实用技巧。 createDrawerNavigator抽屉效果&#xff0c;侧边滑出&#xff1a; createDrawerNavigator API createDrawerNavigator(RouteConfigs, DrawerNavigatorConfig): RouteConfigs(必选)&#xff1a;路由配置对…

如何在内核中重新编译某一个ko模块

例如&#xff1a;SDIO 模块 1、用 lsmod 查看sdhci 相关的模块。 2、rmmod sdhci-pci 和 sdhci 3、make modules SUBDIRSdrivers/mmc 4、find. -name *.ko 查找生成的.ko文件 5、insmod sdhci-pci.ko 和sdhci.ko 这样可以只是编译drivers/mmc 目录下的ko模块

命令行方式查看windows下机器的配置

需要在C:/Program Files/Common Files/Microsoft Shared/MSInfo目录下执行Msinfo32 命令&#xff1a;C:/Program Files/Common Files/Microsoft Shared/MSInfo>Msinfo32 /report d:/cmpInfo.txt /categories SystemSummary进入d盘使用type查看生成的文件d:/> type cmp…

Linux内核编译过程

准备工作 硬件&#xff1a;笔记本 系统&#xff1a; Ubuntu18.04 64位 下载内核 1、先安装ubuntu18.04的系统。 2、到内核官网下载最新的内核code&#xff1a; https://www.kernel.org/ 如图所示下载最新kernel 例如 Linux-4.19&#xff1a; 3、将下载的内核 Linux-4.1…

Linux Shell nohup命令用法

在应用Unix/Linux时&#xff0c;我们一般想让某个程序在后台运行&#xff0c;于是我们将常会用 & 在程序结尾来让程序自动运行。比如我们要运行mysql在后台&#xff1a; /usr/local/mysql/bin/mysqld_safe –usermysql &。可是有很多程序并不想mysqld一样&#xff0c;这…

必应搜索昨日起出现大规模的无法访问

昨天下午&#xff0c;必应搜索在国内出现大规模的访问故障&#xff0c;显示“无法访问此网站”&#xff0c;具体原因未知。必应由微软公司在2009年推出&#xff0c;英文名为 Bing&#xff0c;是全球领先的搜索引擎之一。对昨天下午起国内出现的大规模访问故障&#xff0c;有网友…