Windows驱动编程基础|Windows驱动编程基础(下)之电源管理


Windows驱动编程基础(下)之电源管理

  • 第五章 电源管理
    • 电源管理概况
    • 电源状态的上升和下降
    • 电源管理优化
      • IRP_MN_WAIT_WAKE
        • 那么这个IRP_MN_WAIT_WAKE是干啥用的呢?
        • 再次讲解PoRequestPowerIrp函数
      • IRP_MN_POWER_SEQUENCE
        • 电脑省电的驱动支持:
        • 我们说这两个函数搭配起来是怎么用的?

第五章 电源管理 电源管理概况 Windows驱动编程基础|Windows驱动编程基础(下)之电源管理
文章图片

Windows驱动编程基础|Windows驱动编程基础(下)之电源管理
文章图片

SystemPowerState系统电源状态到DevicePowerState设备电源状态的转换:
假设我们的设备当前处于工作状态,操作系统的电源管理器PM想把我们的设备置为休眠状态,这个时候PM会发IRP_MN_QUERY_POWER这么一个IRP发给我们的驱动,这个IRP带的消息是把设备的电源用电量降低:
Windows驱动编程基础|Windows驱动编程基础(下)之电源管理
文章图片

这个IRP的输入参数是IO_STACK_LOCATION中的Parameters.Power结构体,里面的成员分别是:
Windows驱动编程基础|Windows驱动编程基础(下)之电源管理
文章图片

SystemContext是操作系统用的我们不用去管他,State是说要把电源置为哪一个状态,ShutdownType是说电源的关闭类型;
IRP_MN_QUERY_POWER这个IRP的意思是,操作系统的电源管理器PM想让我们的驱动把这个设备放在一个低电源状态的时候,首先PM会问我们写的这个驱动你能不能把你这个设备的电源置为4这个系统电源状态,PM就会发这个IRP给我们的驱动,这个IRP里面就得带一个状态类型,这个状态类型就是说你这个状态是操作系统那6个状态啊,还是物理设备的那4个状态,这个状态你必须说明是操作系统的那个电源状态,还是设备的电源状态,有这两类电源状态的标准,这两个不一样;
首先你操作系统发过来你得说你自己发的是设备的电源分类,还是操作系统的电源分类,所以Type就是指这两个电源分类;
State就是具体的电源状态,例如4这个电源状态是指操作系统的电源分类,那么4就对应PowerSystemSleeping3这个电源状态,如果说Type是设备的电源状态,那么4就对应PowerDeviceD3这个停止状态了;
ShutdownType,当操作系统要把我们电源断电的时候,它会给我们一个断电原因,这个原因就在ShutdownType里面:
Windows驱动编程基础|Windows驱动编程基础(下)之电源管理
文章图片

比方上面所选字段是说你的电脑可能要重启,你就没有必要真正把电源断掉,它还要重启,你就让它上着电,就是说你可以做些优化。
IRP_MN_QUERY_POWER这个IRP就是问我们驱动能不能把电源置为Type和State这两个合起来表示的电源状态,ShutdownType这个是说如果把电源断电的话,那么它给一个断电原因。
IRP_MN_QUERY_POWER这个IRP有可能是操作系统的电源管理器发给我们的,也有可能是另外一个驱动发给我们这个功能驱动的,这都有可能的,这个时候我们就知道别人想让我们干啥,问我们能不能把电源设置到某一种状态。
通过IO_STACK_LOCATION里面所给的这些信息,我们就知道有人希望我们把这个设备的电源状态置为哪一种状态了,然后我们的功能驱动收到这个IRP以后该怎么办呢?
分两种情况,你得先看看这个设备处于哪一种状态,假如这个设备正处于工作状态,正在传输数据呢,那就肯定不能断电了;
还有就是,你这个设备虽然现在是闲着的,但是你这个设备现在能不能断电,你也得问一下,问总线驱动你这个设备现在能不能断电,有可能你这个设备就不能断电,例如总线控制器上面挂了3个设备,这3个设备都挂在总线控制器一个开关上,显然不能通过这个开关只把一个设备断电,否则其他设备也被断电了,所以说,你只有当这3个设备都需要断电的时候,你才能关开关,这个时候这3个设备才能断电,你总线驱动知道你这个总线设备上只有一个开关,并不是每一个设备都有自己单独的开关,这个时候我们这个设备的功能驱动它问了一圈,发现这个设备是闲着的,然后它就会问总线驱动能不能让这个设备断电,总线驱动可以回答YES,也可以回答NO;
显然,第一种情况我们的功能驱动回答NO的话,那就没有必要再问总线驱动了,你直接告诉电源管理器或者其他驱动断不了电;
如果你检查这个设备的状态,或者有没有人用这个设备,答案是这个设备可以断电的话,你再去问总线驱动可不可以断电;
也就是说这两种情况是逻辑与的关系,只要有一个人回答NO,那整个结果就是NO,只有两人都回答YES才可以断电;
这就是我们处理这个IRP的一个办法,那我们怎么回答其他驱动或电源管理器呢?
Windows驱动编程基础|Windows驱动编程基础(下)之电源管理
文章图片

我们在IRP里面有一个IO_STATUS_BLOCK:
Windows驱动编程基础|Windows驱动编程基础(下)之电源管理
文章图片

那么你在Status里面返回SUCCESS,人家就知道可以断电,如果你返回成功以外的值的话,它就认为你回答是NO,当前设备不能置为你要求的那种状态,这就是我们处理这个IRP的一个过程。
IRP_MN_QUERY_POWER这个IRP分两种情况,一般我们电源管理器PM发过来的都是SystemPowerState这种类型,其他驱动发过来的一般都是DevicePowerState这种类型。
假如我们现在所写的功能驱动收到这个IRP以后,我们功能驱动首先得把SystemPowerState这种类型的电源状态4,对应到DevicePowerState这种类型的电源状态,你比方说4状态就可以对应DevicePowerState这种类型的4状态,也就是D3(D3就断电了),这个对应关系是,谁写的这个功能驱动谁决定这个事情,我就这么定的,你操作系统让我把它放在4状态,那么我这个设备就断电;
我们的功能驱动就可以先问总线驱动,也可以先看看我们当前的设备是不是正在处理某一个数据传输的IRP,你是先看看有没有人用这个设备,还是先问问总线驱动这个设备能不能断电,这两个不分先后,你可以自己决定先问哪个;
你问总线驱动你这个设备能不能断电,那么怎么问总线驱动呢,人家PM发过来的IRP_MN_QUERY_POWER这个IRP不能动,先放在FDO对应的功能驱动里面,先不要动,这个时候接着创建一个新的IRP;
大家注意,IRP_MN_QUERY_POWER这个IRP不能用IoAllocateIrp函数来创建,必须要用操作系统给我们提供的PoRequestPowerIrp函数来创建并发送这个IRP给底下的总线驱动,我们的电源IRP必须用这个函数来创建:
Windows驱动编程基础|Windows驱动编程基础(下)之电源管理
文章图片

为什么必须要用这个IRP来创建呢,操作系统认为我们的驱动同一时间只能处理一个电源的IRP_MJ_POWER这种类型的IRP,所以说当操作系统发出一个这种电源的IRP给一个驱动,它就不允许另外一个电源的IRP发出了,它不能并行而只能串行发送这种电源IRP,操作系统里面会维护一个队列;
如果用IoAllocateIrp这种函数创建IRP,然后用IoCallDriver函数直接把IRP发给驱动的话,那么它会失控,有可能两个人同时创建两个电源IRP,然后同时把这个电源IRP都发给我们驱动了,操作系统认为这种情况是不允许的,所以操作系统自己内部做了一个控制,它是怎么控制的呢,就是操作系统给了我们这么一个函数,我们电源IRP必须用这个函数来创建,当我们调用操作系统这个PoRequestPowerIrp函数的时候,操作系统就知道了有人要创建一个电源IRP,并且要把这个电源IRP要发给某一个设备,操作系统知道这个事,如果说这个设备上没有电源的IRP在处理,那么这个函数就成功了;如果这个时候驱动里面正在处理电源IRP,那么操作系统就知道了,它会记住的,你上次就是通过我的手创建的,你这次又通过我的手创建,上次那个电源IRP还没完呢,你又来了,那么你给我滚回去;
所以说通过这个函数操作系统就可以对我们发IRP进行串行控制了。
该函数第2个参数是子功能号,我们应该写IRP_MN_QUERY_POWER;
刚才我们讲了操作系统的一个电源分类对应我们设备的一个电源分类,所以第3个参数我们可以设DeviceState这个状态;
注意该函数最后一个参数Irp我们一般设置为NULL,我们不会使用这个值的,但是有些情况下我们需要记住这个返回的IRP,以后会说这种情况;
当这个函数调用完以后,我们新创建的这个IRP就发送给DeviceObject这个参数指向的设备了,这个设备就开始处理这个IRP了,这个设备处理IRP的结果,在我们设置的PowerCompletion回调函数:
Windows驱动编程基础|Windows驱动编程基础(下)之电源管理
文章图片

我们说有一个设备创建了这个电源IRP发给另外一个设备,另外的这个设备把这个IRP完成了, 大家注意谁完成这个IRP该函数的这个DeviceObject参数就指向谁,参数IoStatus非常重要,比方说FDO把这个电源IRP发给底下的PDO,PDO处理完了返回这个IRP的时候就会调用我们PowerCompletion这个函数,那么这个时候PDO返回的结果就会传给这个IoStatus,根据IoStatus我们就知道这个PDO回答的是YES还是NO了,如果IoStatus.Status的值是0,即回答的是YES,总线驱动就可以在PowerCompletion这个回调函数里面完成操作系统电源管理器发过来的这个IRP_MN_QUERY_POWER(例如可以断电)。
我们功能驱动查询完了当前设备有没有IRP,发现这个设备当前是闲着的,能够断电,查询完了得到的答案是YES;
然后我们问总线驱动能不能断电,怎么问总线驱动呢,就是我们的功能驱动调用PoRequestPowerIrp函数创建一个电源IRP,发给总线驱动,总线驱动在PoRequestPowerIrp函数里面有一个回调函数,在那个回调函数里面通过Status它就知道回答是YES还是NO,如果Status里面的是YES的话,我们在回调函数里面就可以完成操作系统电源管理器发过来的IRP_MN_QUERY_POWER这个电源IRP,如果回答的是NO的话,也可以完成这个IRP给它返回一个否就行了(例如不能断电);
在这个回调函数里面,可以完成电源管理器给我们发送过来的IRP,当PoRequestPowerIrp这个函数创建的电源IRP被完成以后,这个IRP会自动被销毁。
当电源管理器通过这个IRP它就知道你这个驱动对应的设备现在能不能放到我想要的SystemPower的第4种状态,假如得到的回答是NO,它就不吱声了,如果回答的是YES,这个设备是可以置于SystemPower的第4种状态,这个时候我们的电源管理器就会接着发送IRP_MN_SET_POWER这个IRP。
当IRP_MN_QUERY_POWER这个IRP返回成功状态后(该功能驱动和底层的总线驱动都回答YES的话),操作系统的电源管理器会给我们的功能驱动发送IRP_MN_SET_POWER这个IRP(例如降低电源状态进入休眠或断电),然后我们的功能驱动首先要保存当前设备的状态或者设备所处的运行环境(设备里面的那些寄存器、内存范围、中断等),以便通电运行后恢复这些值,一般来说记到内存或者那些DUMP文件里面、休眠文件里面,然后我们功能驱动又通过PoRequestPowerIrp这个函数再创建一个电源IRP(IRP_MN_SET_POWER),同样有一个回调函数,因为回答的是YES,所以这个时候总线驱动就把电源断掉了,在回调函数里面把IRP_MN_SET_POWER这个IRP完成就行了。
Windows驱动编程基础|Windows驱动编程基础(下)之电源管理
文章图片

这个总线驱动通过PowerCompletion回调函数里面把IRP_2给完成,在IRP_2的完成例程Completion里面再把IRP_1给完成,降低电源状态就是这么一个过程。
如果我们现在4这种电源状态的想恢复的话(上升电源状态),PM会通过IRP_MN_QUERY_POWER这个IRP问一遍,得到YES答复以后,PM管理器再发IRP_MN_SET_POWER这个IRP过来,这个时候这个功能驱动先创建一个IRP_2,把这个设备的运行恢复以后,将之前保存的设备状态、设备的环境信息读出来,把设备的状态恢复好,再把IRP_1返回。
电源状态的上升和下降 回顾上节课:
我们功能驱动有可能收到两类IRP,一类是系统IRP,指的是IRP_MN_QUERY/SET_POWER这两个IRP里面所带的Parameters.Power.Type是SystemPowerState,一类是设备IRP,它的Parameters.Power.Type是设备电源状态DevicePowerState。
Windows驱动编程基础|Windows驱动编程基础(下)之电源管理
文章图片

如果功能驱动收到的是设备IRP的话,这个设备电源状态就不需要转换了,就没有必要找那种和系统电源状态对应的关系了,它就直接把这个设备IRP直接转发给我们总线驱动Bus driver,总线驱动收到这个IRP以后会进行处理,这个IRP它有一个完成例程,这个完成例程会根据你这个总线驱动所返回的YES或者NO的答复,把这个设备IRP直接完成就可以了,这是比较简单的一种状态;
Windows驱动编程基础|Windows驱动编程基础(下)之电源管理
文章图片

比较复杂的是它发过来一个系统IRP_1,这个时候我们的功能驱动就不能直接完成这个系统IRP_1,我们的功能驱动得通过PoRequestPowerIrp函数重新创建一个新的IRP_2发给总线驱动Bus driver,这个创建的新的2号IRP是一个设备IRP,这个新的IRP安装了一个回调例程Callback routine,这个新的IRP_2被完成以后呢会调用这个回调例程,在这个回调例程里面它会完成这个1号IRP_1;
所以说,如果我们收到的是一个系统IRP的话,我们得创建一个设备IRP,创建的这个设备IRP和系统IRP是有一定对应关系的,这个对应关系得由我们自己来找,在我们第2个IRP的回调函数里面就可以看见这个设备IRP被完成的一个状态,根据设备IRP完成的状态,我们再把系统IRP在这个回调函数里面给它完成。
我们得知道我们的设备支持哪几种设备电源状态,有两种办法,一种是问硬件工程师,还有一种是我们自己查,我们知道IRP_MJ_PNP中有IRP_MN_QUERY_CAPABILITIES这么一个子IRP,这个子IRP就是查询设备的一个能力,我们把这个IRP发给底下我们的总线驱动的时候,总线驱动就会告诉我们这个设备有哪些能力,我们就知道这个设备到底支持哪几种电源状态,在PNP的完成例程里面添加如下代码:
Windows驱动编程基础|Windows驱动编程基础(下)之电源管理
文章图片

经过测试,我们可以看到D1、D2这两个都不支持,它只要管D1、D2这两种状态支不支持就可以了,D0是工作状态,D3是断电状态,你任何一种设备都会支持D0和D3的,所以它只问支不支持D1和D2这两种状态,所以我们这个网卡只支持工作状态和断电状态。
接着我们开始设计系统电源状态和设备电源状态的对应规则:
显然一个状态得对应一个状态,这里我选择系统电源状态2、3和4都对应设备电源状态D3,你可以有自己的选择方法,对应规则设计好了,就可以开始写代码了。
Windows驱动编程基础|Windows驱动编程基础(下)之电源管理
文章图片

函数PoStartNextPowerIrp这个是什么呢?
我们之前说过,操作系统认为我们的功能驱动同一时间只能处理一个电源IRP,所以我们在每一个驱动完成电源IRP的时候,都要通过这个函数告诉操作系统你可以启动第2个IRP了,大家注意每一个驱动都得告诉一声,你比如说设备栈中有好多个驱动,这个电源IRP一层一层传下来了,所以每一个驱动都得告诉一声,每一次你完成这个电源IRP的时候,不要忘了把这个函数调用一下,这个就是真正完成系统IRP;
大家注意IoStatus这个是设备IRP的处理结果,设备IRP的处理结果我们得告诉给系统IRP(pIrp->IoStatus.Status = IoStatus->Stattus),由系统IRP返回给我们电源管理器。
Windows驱动编程基础|Windows驱动编程基础(下)之电源管理
文章图片

我们这个设备既然要对电源进行管理,设备扩展内存里面的这个成员PowerOn,它就是记录当前我们电源的状态,当PowerOn的值等于0就是断电状态,等于1就是加电状态。
Windows驱动编程基础|Windows驱动编程基础(下)之电源管理
文章图片

我们在虚拟机中测试电源管理驱动程序的时候,当安装好该驱动后,我们点开始、电源、睡眠,虚拟机睡眠后,你再点击继续运行此虚拟机,这个时候DebugView里面就会输出电源管理的信息:
Windows驱动编程基础|Windows驱动编程基础(下)之电源管理
文章图片

这可以看见我们整个电源,系统在休眠状态下它会让我们网卡的电源状态发生改变,现在这个驱动就演示了如何控制我们这个网卡设备的电源。
电源管理优化 这节课我们讲DO_POWER_PAGEBLE和DO_POWER_INRUSH这两个东西。
我们有时候在DriverAddDevice函数里面创建设备对象的时候,会给设备对象的Flags设置一个DO_POWER_PAGEBLE标志:
Windows驱动编程基础|Windows驱动编程基础(下)之电源管理
文章图片

而有的人会放一个DO_POWER_INRUSH,那么这两个有什么区别呢?
一般电气设备启动的时候电流比较大,比如工厂里面的泵比较多,这个泵有一个开关,这个开关里面有保护,保护你这个泵的启动电流不能太大,启动电流有可能是正常运行电流的10倍,或者泵出问题了,导致泵的运行电流变大,这个开关都会跳掉,还有的情况是漏电了,进来的电流和出去的电流大小不一样了,进去的和出去的就不平衡了,当漏电比较严重的时候也会导致泵跳掉了。
我们电脑这些外围设备的启动电流比较大,我们就应该在驱动中创建设备的时候,给设备设置上DO_POWER_INRUSH这个标志;
当你这些外围设备做的比较好,启动电流跟运行电流基本上一样大的话,你就加DO_POWER_PAGEBLE这个标志;
所以原则上说,这两个标志是由我们设备的电气特性决定的,加上这两个标志以后对我们操作系统有什么影响呢?对我们电气设备有什么影响呢?
这两个标志对我们代码有什么影响呢?
电脑上所接的的那些电气设备,比如有3个INRUSH设备,2个PAGEBLE设备,当电脑加电启动的时候,会先把INRUSH设备一个一个设置为工作电源状态,为了避免电流过大造成电脑电源损坏,不会一下子全部设置;
由于PAGEBLE设备的启动电流跟运行电流基本上一样大,这种设备可以一下子全部设置成工作电源状态,让它们同时加电。
Windows驱动编程基础|Windows驱动编程基础(下)之电源管理
文章图片

处理IRP_MN_QUERY_POWER这个子IRP的派遣函数的中断优先级,跟发送这个IRP的程序的中断优先级是一样的,所以处理这个子IRP的派遣函数有可能运行在DISPATCH_LEVEL这个中断优先级上,当然在这种情况下你的电气设备是DO_POWER_INRUSH这个特性的设备,也就是说当我们把设备设置成DO_POWER_INRUSH这种特性的时候,我们就要考虑处理这个子IRP的派遣函数就有可能在DISPATCH_LEVEL这个中断优先级上运行,那么我们这个子IRP的派遣函数就必须位于非分页内存里面(#pragma code_seg()),因为在DISPATCH_LEVEL这个中断优先级上不能发生页面故障。
由于我们外围设备它们的电气特性不一样,根据它们的电气特性,给我们设备对象设置PAGABLE或INRUSH这两个不同的标志,当设置好以后,你在处理IRP_MN_QUERY/SET_POWER这两个子IRP的时候,你就应该考虑你这个派遣函数到底运行在哪个中断优先级上。
有可能你的派遣函数运行在DISPATCH_LEVEL中断优先级上,你这个派遣函数里面就不能用这种事件内核对象EVENT或者其他内核对象,因为我们说内核对象同步的时候这些内核对象必须运行在小于DISPATCH_LEVEL的中断优先级,事件内核对象在DISPATCH_LEVEL中断优先级上运行的条件是非常非常苛刻的,大家几乎就不要去用,所以大家是初学者的话要记住,内核的那些同步对象不要运行在DISPATCH_LEVEL上,很容易造成死锁,死锁的原因就是设置内核对象和那个等待这个内核对象的线程都是具体的一个线程,一旦离开这个线程环境,在DISPATCH_LEVEL中断优先级上,线程的那个调度模块就没法正常运行了,因为我们的线程调度程序它就运行在DISPATCH_LEVEL中断优先级上。
大家注意也不要用spinlock,它不像我们的WaitForSingleObject和WaitForMultipleObjects这两个不占CPU,spinlock是一直占着CPU,相当于CPU一直在那不停的循环检测某一个数据。
那怎么办呢?
你就把要处理的事情发在那个完成例程里面,就不要用事件内核对象来同步它了,总之你要想办法,想出不要使用事件内核对象还要完成这种工作的办法。
IRP_MN_WAIT_WAKE
Windows驱动编程基础|Windows驱动编程基础(下)之电源管理
文章图片

我们可以看到,这个子IRP发送的时候,也就是说我们的派遣函数一定是在PASSIVE_LEVEL中断优先级上运行,那么我们的派遣函数就可以使用内核对象了,虽然我们的完成例程运行在DISPATCH_LEVEL中断优先级上,如果你使用事件内核对象来同步你的代码的话,你一般在完成例程里面使用KeSetEvent这个函数,这个时候你在完成例程里面就可以使用事件内核对象,因为你的派遣函数是在PASSIVE_LEVEL中断优先级上,你的KeWaitForSingleObject这个函数在派遣函数里面呢,现在关键是我们的完成例程有可能运行在DISPATCH_LEVEL中断优先级上,所以我们要看KeSetEvent这个函数的使用条件:
Windows驱动编程基础|Windows驱动编程基础(下)之电源管理
文章图片

Windows驱动编程基础|Windows驱动编程基础(下)之电源管理
文章图片

Windows驱动编程基础|Windows驱动编程基础(下)之电源管理
文章图片

Wait这个参数我们一般设置成FALSE,KeSetEvent函数就可以运行在DISPATCH_LEVEL中断优先级上,也就是当这个参数设置为TRUE的话,那么KeSetEvent函数就必须运行在APC_LEVEL这个中断优先级上。
Windows驱动编程基础|Windows驱动编程基础(下)之电源管理
文章图片

所以通过这一句话你可以知道,处理这个子IRP的派遣函数里面你就可以用事件内核对象来同步,事件内核对象就可以用了,
在完成例程里面你调用KeSetEvent这个函数是完全没有问题的,只要那个KeWaitForXXX等待函数在PASSIVE中断优先级上就可以了,设置一个内核对象变成有信号状态的函数可以运行在DISPATCH中断优先级上。
这是为什么呢?
大家可以想一想,当我们线程碰到KeWaitForXXX函数的时候,因为我们线程运行在PASSIVE这个中断优先级上,那么我们这个线程会被线程调度程序放在等待状态,那么现在有另外一段代码,这段代码运行在DISPATCH中断优先级上,这段代码里面有一个KeSetEvent函数,就把Event这个内核对象设置成有信号状态,只有当这段代码全部运行完成以后,我们KeWaitForXXX函数所在的线程它才能接着运行,线程的调度器看见事件内核对象变成有信号状态以后,它会把这个线程由等待状态放到那个运行列表里面,那么这个时候这个线程就会运行,它不会造成死锁。
那么这个IRP_MN_WAIT_WAKE是干啥用的呢? 你的电脑长时间不用,屏幕都是黑屏,好像停止不动不工作了,这个时候我想用电脑的时候我会把鼠标晃晃,拍几下键盘按键,电脑就亮了我们又可以用了,这些动作其实是唤醒我们的电脑,那么为什么我们一晃鼠标、一动键盘这个电脑就被唤醒了呢?这个它是怎么做到的?
还有一个电脑的远程唤醒,这是通过网卡来唤醒的,这又是怎么做到的呢?
电脑长时间不用的话,操作系统的电源管理器会发一个IRP_MN_WAIT_WAKE给键盘,键盘这个物理设备所对应的总线驱动拿到这个子IRP后就阻塞了(假设这个键盘具有唤醒功能这个前提条件),电脑中的其他设备也会被PM设置为休眠电源状态;
一旦有人按了按键,这个键盘所对应的总线驱动肯定会知道这个事情,就会完成这个子IRP并返回给PM,操作系统就会逐个设置电脑中的其它设备为工作电源状态,电脑就被唤醒了。
Windows驱动编程基础|Windows驱动编程基础(下)之电源管理
文章图片

再次讲解PoRequestPowerIrp函数
NTSTATUS PoRequestPowerIrp( [in]PDEVICE_OBJECTDeviceObject, [in]UCHARMinorFunction, [in]POWER_STATEPowerState, [in, optional] PREQUEST_POWER_COMPLETE CompletionFunction, [in, optional] __drv_aliasesMem PVOIDContext, [out]PIRP*Irp );

Windows驱动编程基础|Windows驱动编程基础(下)之电源管理
文章图片

我们可以看到,这个函数的PowerState参数只能设置设备的电源状态DEVICE_POWER_STATE,也就是说我们驱动程序用PoRequestPowerIrp函数创建IRP_MN_SET_POWER的时候,我们所设置的必须是设备的电源状态,我们的驱动没有办法自己发出一个里面是系统电源状态的IRP,也就是说PowerState参数一定是设备的电源状态,不能是系统的电源状态,系统电源状态是由操作系统电源管理器发出的,我们驱动发出的是设备的电源状态。
IRP_MN_POWER_SEQUENCE
【Windows驱动编程基础|Windows驱动编程基础(下)之电源管理】Windows驱动编程基础|Windows驱动编程基础(下)之电源管理
文章图片

Windows驱动编程基础|Windows驱动编程基础(下)之电源管理
文章图片

这个IRP的输出是,我们的电源在D1下的时间,在D2下的时间,在D3下的时间,这个IRP是需要我们的物理设备对象PDO对应的驱动来完成的,我们功能驱动一般不处理这个IRP,大家注意这个IRP也有一个DISPATCH,那就说明内核对象不能用来同步它的完成例程了。
这个IRP就是返回我们设备在哪种电源状态下待了多长时间,那么这个IRP有什么用呢?
为了优化电源管理,假如我们的设备当前已经处于D1电源状态,此时系统或者其他驱动不知道这个设备的电源状态,又给这个设备设置D1电源状态,这样是不是就浪费电了;
所以,我们的功能驱动可以给PDO发送两次IRP_MN_POWER_SEQUENCE这个子IRP,根据两次返回的结果我们的功能驱动可以判断出这个设备当前处于什么电源状态(例如两次的D2和D3的时间都相等,D1的时间肯定就不相等,说明设备当前仍然处于D1状态),就避免了给这个设备设置两次D1电源状态,避免浪费系统资源。
电脑省电的驱动支持: PoRegisterDeviceForIdleDetection、PoSetDeviceBusy这两个函数也是为了让我们电脑省电的。
电脑长时间不用的话,例如显示器可能会进入屏保程序,这是什么原理呢?
如果一个设备长时间不用的话,那么最好把它置于一个休眠状态,这样省电,那么怎么样规定这个时间呢,你是等5分钟进入休眠状态,还是等10分钟进入休眠状态呢,那肯定是由程序控制的,这又是怎么控制的呢?
Windows驱动编程基础|Windows驱动编程基础(下)之电源管理
文章图片

这个函数是告诉我们的I/O管理器中的电源管理器一个事情,这个函数一调用就会在我们I/O管理器里面弄一个计数,这个计数是随着时间的推移会不断地递减,好像一个沙漏一下沙子不停的往下漏、沙子会不停的减少,当这个计数减少到0的时候,这个I/O管理器就会给我们设备发一个IRP_MN_SET_POWER这个子IRP来设置我们设备的电源状态,因为这个计数时间耗完了以后,I/O管理器就认为这个设备长时间没人用,那就让它歇着吧,也就是发送一个设置电源状态的IRP,然后用这个IRP让这个设备处于低电源状态。
这个函数的第2个参数,是以省电为优先策略的一个计数;
第3个参数是以性能为优先的一个计数;
我们在设置笔记本电源的时候,就会让你选是以性能为优先,还是以省电为优先,这两个参数其实就是笔记本电脑电源设置的那两个选项的意思,你可以把这两个参数理解为两个以秒为单位的时间。
第4个参数的值只能是设备的电源状态。
那有人说了,你这个函数一调用,设备不就死了么,当计数减少到0的时候这个设备肯定停了,那么另外一个配合函数就来了。
Windows驱动编程基础|Windows驱动编程基础(下)之电源管理
文章图片

PoSetDeviceBusy函数的参数就是PoRegisterDeviceForIdleDetection这个函数的返回值,PoRegisterDeviceForIdleDetection这个函数会给我们返回一个引用计数:
Windows驱动编程基础|Windows驱动编程基础(下)之电源管理
文章图片

PoSetDeviceBusy这个函数的作用:I/O管理器里面不是有一个引用计数么,假如你这个引用计数是100,每过一秒就递减一次值,假如这个时候这个函数被调用了(假设此时引用计数是96),这个函数就会把引用计数的值96重新给它改成100,接着从100这个计数值又重新开始了,到0的时候函数PoRegisterDeviceForIdleDetection就会发送IRP_MN_SET_POWER这个IRP给设备设置成低电源状态。
我们说这两个函数搭配起来是怎么用的? PoRegisterDeviceForIdleDetection这个函数给设备A申请了一个引用计数叫Count,Count的值从100开始随着时间的推移开始减少了,该函数的DeviceObject参数所指向的这个设备,会处理一些IRP,当我们这个设备驱动里面每处理一个IRP,我们就调用PoSetDeviceBusy函数,就是说每次有IRP出现的时候我们就调用PoSetDeviceBusy这个函数重置这个Count的值,每次出现一个IRP我们这个Count就重新变成100了,因为我们每个IRP出现的时候都调用PoSetDeviceBusy这个函数把Count设置成100了,PoSetDeviceBusy这个函数是一个置位函数;
Windows驱动编程基础|Windows驱动编程基础(下)之电源管理
文章图片

这个引用计数就相当于一个沙漏,每次这个函数PoSetDeviceBusy被调用的时候,这个沙漏就重新给它填满,如果不断的有IRP发生在这个设备上的时候,那么这个沙漏不停的被填满不停的被填满,它就不会闲着了,当这个设备长时间不处理IRP的时候,那么这个沙漏就有可能漏完,也就是这个引用计数Count可能变成0,那么我们设备就休息了;
这两个函数就是干这个用的。
在IRP_MN_QUERY_POWER和IRP_MN_SET_POWER这两个IRP里面,我们没有必要重新恢复沙漏,都没有必要调用PoSetDeviceBusy这个函数,因为这两个IRP都不是让我们设备真正工作的,这只是设置电源状态的两个IRP;
一般来说在读写操作里面,你把这个函数PoSetDeviceBusy调用一下就可以了。

    推荐阅读