在 C++ 中进行 WMI 查询






4.92/5 (69投票s)
2005年5月31日
27分钟阅读

553991

10959
一个示例驱动的指南,介绍如何在 C++ 中编写 WMI 消费者。
摘要
在互联网上搜索有关 WMI 消费者编程的文档时,大多数找到的文档都是为 C# 程序员编写的。关于如何在 C++ 中使用 WMI 的文章相当稀少。这甚至更令人惊讶,因为 MSDN 提供了大量的 C++ 代码片段。然而,这些片段未能构成一个真正的教程。本文探讨了实现 WMI 消费者的各种方面。在随附的示例过程中,重点放在如何通过查询从 WMI 中检索信息。
尽管本文具有介绍性,但它假设读者熟悉 C++ 环境中的 (D)COM 编程。一些 SQL 知识是一个优势,但不是必需的。
I 介绍
动机
尽管普通用户不一定这样认为,但操作系统的演变与计算机硬件的演变一样令人惊叹。在恐龙统治地球的石器时代,计算机是装满电线和真空管的巨型黑盒子,为它们设计执行的某些任务进行硬编码。这些是现代操作系统的早期祖先。特别是,它们不提供管理任务的支持。这部分是因为在那个时候,只有五台机器的用户基础,没有这种需求。
今天,回顾这种演变的初步终点,情况发生了巨大变化。现代计算机已经变得无处不在,前所未有的多功能工具。它们的操作系统变得越来越大,越来越复杂的环境。最重要的是,它们不再是曾经的“专用架构”机器。每台机器都可以支持大量的硬件和软件配置。操作系统反映了这一点;例如,在 Windows 平台上,从 Windows 95 开始,微软引入了注册表作为存储配置数据的集中位置。还添加了其他系统管理工具和组件。
由于网络功能现在是一个关键要求,因此操作系统集成支持不仅可以管理桌面上的本地计算机工作站,还可以管理网络基础设施,这一点变得越来越重要。公司网络很容易由 100 多台计算机组成,远程管理功能尤其有利。WMI (Windows Management Instrumentation) 是这些需求的集成解决方案。
WMI 应用程序的一个突出示例是 Windows XP 和 Windows 2000 中内置的任务监视器。它令人印象深刻地演示了 WMI 的一个重要特性 - WMI 有一个 API,可以从您自己的应用程序中调用。这意味着不再需要通过命令行解释器调用实用程序应用程序(例如 PING.EXE)并解析其输出来获取结果的繁琐方式。
从程序员的角度来看,WMI 是一个基于 DCOM 的服务,它提供各种同步和异步访问许多系统管理相关数据结构的方法。尽管这些信息中的许多可以通过其他方式访问,但正是统一的 API 可以使 WMI 对程序员非常友好。
本节的其余部分将简要概述 WMI 及其常用 SQL 派生工具 WQL。第二节介绍本地计算机上的同步查询。异步查询是第三节的主题。最后,在第四节中,讨论了第三种与 WMI 通信的方式——事件。
WMI 和 WQL 概述
Windows 管理规范 (WMI) 为应用程序提供有关计算机(可能在甚至包含整个企业的网络中)的状态或性能数据。它从各种来源(如 Win32(或 Win64)系统、注册表或其他自定义来源)检索这些数据。特别是,WMI-API 隐藏了数据的实际位置,以支持对所有状态或性能数据的更抽象视图。
图 1
WMI 引入了*托管对象*的概念。托管对象是计算机上由 WMI 管理的某个逻辑或物理组件。例如,硬盘驱动器上的分区是一个托管对象。另一个示例是 SNMP 服务或像处理器这样的物理组件。在更高层面,例如对于网络,路由器配置可以通过 WMI 进行管理。从 WMI 获取有关托管对象信息的应用程序称为*消费者*。
对于消费者来说,托管对象由*提供程序*表示。WMI 提供程序是一个 COM 组件,它维护来自托管对象的信息,并通过 WMI 将其转发给消费者。提供程序实现 CIM 定义的特定接口并向 WMI 注册。通过实现这些接口,开发人员可以编写满足其需求的自定义提供程序。WMI 附带了一些预定义的提供程序,称为*标准提供程序*。本文中的示例仅使用标准提供程序。图 1 说明了这一点;与提供程序的交互被 WMI 模糊化,对客户端而言,标准提供程序和自定义提供程序都被隐藏;WMI 公开的数据结构似乎已与操作系统合并。
有关托管对象的信息由 WMI 以 WMI 提供程序创建的类的实例的形式公开。但是,通常情况下,消费者不会直接访问这些实例。相反,消费者会*查询* WMI,然后 WMI 将查询转发给相应的提供程序。提供程序从托管对象的请求属性子集中构建一个结果集,该结果集通过 WMI 返回给消费者(图 2)。查询以 *WQL* 指定,WQL 是 ANSI SQL 的一个子集,带有一些扩展以适应 WMI 的需求。SQL 和 WQL 之间最重要的区别是 WQL 仅支持只读查询。
图 2
对于熟悉 SQL 的人来说,WQL 可以很容易地理解:要查询实例的属性,将属性名称(由实例的类定义)作为列名插入到 SELECT
语句中,并以类名作为表名。例如
SELECT Name FROM Win32_Processor
生成 CPU 名称。与 SQL 一样,星号是通配符,表示返回所有列。
根据查询模式的不同,其结果以与 ADO 或 OLEDB 非常相似的结果集返回。执行查询有三种模式:同步、异步和半同步。前两种模式正如其名称所示,而最后一种模式具有混合性质:查询立即返回一个有效的结果集,但该集合的内容(即“真实”结果)稍后由 WMI 以异步方式填充。选择使用哪种查询模式对性能和安全性都有影响。
有关 WQL 及其各个方面的更深入讨论,请参阅 MSDN Library。
II 同步查询
COM 和 WBEM 定位器设置
与任何调用基于 COM 的服务的进程或线程一样,编写 WMI 消费者的第一步是通过调用 CoInitializeEx
来设置 COM。通常 - 对于异步查询绝对必要 - WMI 指定 COINIT_MULTITHREADED
。这会将调用线程(或进程)置于多线程单元中。OLE 客户端是例外 - 它们仅限于单元线程执行。这给异步查询带来了问题,将在第二节中讨论。
除非在注册表中设置了适当的全系统默认安全上下文,否则下一步是通过调用 CoInitializeSecurity
设置进程范围的安全上下文;由于这里只考虑消费者,RPC_C_AUTHN_LEVEL_DEFAULT
和 RPC_C_IMPL_LEVEL_IMPERSONATE
是唯一要设置的参数。或者,如果之前调用 CoInitializeSecurity
时使用了不合适的参数,则可以为每个代理设置调用 CoSetProxyBlanket
或 IClientSecurity::SetBlanket
。
设置 COM 和 COM-security 后,IWbemLocator
类用于访问 WMI 服务
CComPtr< IWbemLocator > locator; HRESULT hr = CoCreateInstance( CLSID_WbemAdministrativeLocator, NULL, CLSCTX_INPROC_SERVER, IID_IWbemLocator, reinterpret_cast< void*** >( &locator ) );
IWbemLocator::ConnectServer
连接到指定的 WMI 主机。本文使用本地计算机上的 WMI 服务。
IWbemLocator::ConnectServer
定义如下
HRESULT ConnectServer( const BSTR strNetworkResource, const BSTR strUser, const BSTR strPassword,const BSTR strLocale, LONG lSecurityFlags, const BSTR strAuthority, IWbemContext* pCtx, IWbemServices** ppNamespace )
第一个参数 strNetworkResource
指定了消费者要连接的 CIM 命名空间。就像它们的 C++ 对应物一样,CIM 命名空间用于消除 CIM 层次结构中名称的歧义,并对它们施加一些词法结构。它们的形式是 \\server\namespace1\namespace2... 或 //server/namespace1/namespace2...。server
指定命名空间所在的机器。如果命名空间位于本地机器上,则可以省略 server
,即命名空间规范采用 namespace1/namespace2... 或 namespace1\namespace2... 的形式。root\default
指定默认命名空间,它总是定义的。
用户和密码在第二和第三个参数中传递。对于本地机器,它们必须设置为 NULL
,否则调用将失败。strLocale
给出区域设置,NULL
表示当前区域设置。lSecurityFlags
通常设置为 WBEM_FLAG_USE_MAX_WAIT
,这保证调用在两分钟内返回。这可以防止消费者在目标机器关闭时无限期阻塞。
在 strAuthority
中,可以指定一个 Windows 域,用户将在该域中进行身份验证。同样,在本地计算机上或在 strUser
参数中指定域时,此参数必须为 NULL
。最后,pCtx
设置为 NULL
,除非在少数情况下提供程序需要 IWbemContext
实例。
大多数预定义 WMI 结构的命名空间位于 root\CIMV2
命名空间中。对于本地计算机,连接通过以下方式建立
CComPtr< IWbemServices > service;
hr = locator->ConnectServer( L"root\\cimv2", NULL, NULL, NULL,
WBEM_FLAG_CONNECT_USE_MAX_WAIT, NULL, NULL, &service );
处理 WMI 时,务必记住 WMI API 期望基于 wchar
的字符串。
简单查询 - 查询 CPU 信息(CPUTest 项目)
本小节涵盖了使用 WMI 最简单的场景——不带输入参数的本地同步查询。例如,需要找出本地 CPU 的名称及其时钟频率。WMI 将这些信息保存在 Win32_Processor
类的实例中。有关各种 WMI 类(特别是它们拥有的属性)的详细信息,请参阅 MSDN Library。
成功完成上一小节所述的服务设置后,现在可以查询 WMI 以获取所需信息。由于本节处理同步查询,因此调用 IWbemServices::ExecQuery
。其原型是
HRESULT ExecQuery( const BSTR strQueryLanguage, const BSTR strQuery, LONG lFlags, IWbemContext* pCtx, IEnumWbemClassObject** ppEnum )
第一个参数 strQueryLanguage
指定查询语言,必须始终设置为 L"WQL"
。查询本身的 WQL 语句在 strQuery
中传递。lFlags
指定各种标志,最重要的是 WBEM_FLAG_FORWARD_ONLY
,它告诉 WMI 生成一个只能遍历一次的枚举作为结果集。这种枚举效率更高。稍后,将讨论其他相关标志。pCtx
的含义与之前相同。结果集作为枚举指针在 ppEnum
中返回。例如
CComPtr< IEnumWbemClassObject > enumerator; hr = service->ExecQuery( L"WQL", L"SELECT * FROM Win32_Processor", WBEM_FLAG_FORWARD_ONLY, NULL, &enumerator );
查询 Win32_Processor
类实例的所有属性。结果集在 enumerator
中返回。
可以通过重复调用 IEnumWbemClassObject::Next
来遍历包含结果的枚举。此方法可以返回一个或多个 IWbemClassObject
实例。如前所述,这些实例不是实际的 WMI 托管对象实例。在这种情况下,它们将是 Win32_Processor
类的实例,它们充当包装器,实现对查询请求的属性(且仅限于这些属性)的访问(图 3)。然而,它们可以相互识别,在本文的其余部分中,只要无害,就会这样做。
图 3
枚举中返回的实例数是查询中指定类的实例数,即 FROM
子句的参数。一些 WMI 类只实例化一次。其他类可能实例化多次,也可能不实例化多次。例如,Win32_Processor
为 WMI 主机上安装的每个 CPU 实例化;也就是说,在单处理器机器上只有一个实例,在多处理器机器上可能有多个实例。对于实例化多次的类,实例可以通过它们的属性来区分。如果超类名是 FROM
子句的参数,这也是正确的。这将在下一小节中讨论。
关于这一点,这里需要强调的重要一点是,在处理 WMI 时,IWbemClassObject
是消费者访问托管对象数据的唯一接口。当然,在编写 WMI 驱动程序时,情况将大不相同。
IEnumWbemClassObject::Next
的原型是
HRESULT Next( LONG lTimeout, ULONG uCount, IWbemClassObject** ppObjects, ULONG* puReturned )
lTimeout
指定等待结果集中的结果可用的超时时间(以毫秒为单位)。如果超时到期但没有结果可用,则返回 WBEM_S_TIMEDOUT
。WBEM_INFINITE
表示无限超时。对于同步查询,此参数实际上被忽略,因为结果(如果有)总是立即可用。下一个参数 uCount
是期望在 ppObjects
中返回的最大对象数。puReturned
是实际返回的对象数。
ULONG retcnt;
CComPtr< IWbemClassObject > processor;
hr = enumerator->Next( WBEM_INFINITE, 1L, &processor, &retcnt );
因此返回 processor
中 Win32_Processor
的第一个实例。
因为在本例中只从 enumerator
中提取了一个 IWbemClassObject
实例,所以为了方便起见,可以使用 CComPtr<>
。当通过调用 IEnumWbemClassObject::Next
提取多个实例时,必须传递一个普通的 IWbemClassObject*
数组,并且稍后在数组中的每个指针上调用 IUnknown::Release
。
查询到的 Win32_Processor
类实例的属性在 processor
中返回,然后将使用它来检索感兴趣的属性。这是通过调用 IWbemClassObject::Get
并提供相关属性的名称来完成的
HRESULT Get( LPCWSTR wszName, LONG lFlags, VARIANT* pVal, CIMTYPE* pvtType, LONG* plFavor )
属性值以 VARIANT
联合的形式在 pVal
中返回。此处可以使用 _variant_t
包装器类。除了属性值,还可以返回其 CIM 类型和源信息。例如,CPU 的名称可以像这样检索
_variant_t var_val; hr = obj->Get( L"Name", 0, &var_val, NULL, NULL );
向查询传递参数(LocalPing 项目)
显然,目前为止所介绍的这种相对原始的 WMI 查询方法是远远不够的。虽然给定机器上安装的 CPU 数量通常很少,但其他托管对象的实例可能在机器上大量存在,例如代表管理实体。
例如,一旦计算机连接到任何网络,就必须处理有关网络基础设施的信息。这可能包括从高级网络服务和工具到低级协议栈的任何内容。WMI 提供特定的类来访问此类信息。一个例子是著名的工具 PING.EXE。它用于确定网络上给定 IP 地址的可达性。
WMI 通过公开 Win32_PingStatus
类提供内置的 ping 功能。不必为 PING.EXE 构建命令行并通过某个 exec
库函数执行它,而是可以直接查询 WMI 以获取 Win32_PingStatus
的实例。当然,一般来说,人们更希望 ping 非常特定的 IP 地址,而不是整个互联网。因此,必须通过某种方式将要 ping 的 IP 地址告知 WMI。
目标 IP 地址通过 Win32_PingStatus
类的 Address
属性指定。这是通过使用 WHERE
子句扩展查询的 SELECT
语句来完成的。一个简单的 WHERE
子句采用以下形式之一
WHERE property operator constant
WHERE constant operator property
从词法上看,这与 SQL 语义非常相似。主要区别在于,在 SQL 中 WHERE
更像是一个过滤器,而在 WQL 中 WHERE
还可以指定输入参数。以下代码片段展示了如何 ping 给定的 IP 地址,在本例中是本地环回接口
CComPtr< IEnumWbemClassObject > enumerator; hr = service->ExecQuery( L"WQL", L"SELECT * FROM Win32_PingStatus " \ L"WHERE Address=\"127.0.0.1\"", WBEM_FLAG_FORWARD_ONLY, NULL, &enumerator );
Ping 的结果,即目标 IP 是否可达,在 StatusCode
属性中返回。值为 0
表示成功。除了要查询的 WMI 类和 WHERE
子句不同之外,“LocalPing”项目中的相关源代码与上一小节中的示例代码的工作方式几乎相同,因此无需进一步讨论。
可以通过结合运算符来指定多个 WMI 参数,例如
CComPtr< IEnumWbemClassObject > enumerator; hr = service->ExecQuery( L"WQL", L"SELECT * FROM Win32_PingStatus " \ L"WHERE ( Address=\"127.0.0.1\" ) " \ L"OR ( Address=\"192.168.1.1\" )", WBEM_FLAG_FORWARD_ONLY, NULL, &enumerator );
检索查询结果时,请记住返回的枚举现在将包含多个实例。有关 WHERE
子句的详细信息,请参阅 MSDN 或 VS2003 在线文档。
同时查询多个 WMI 类
有时,同时执行两个或多个查询以获得更好的运行时行为是一个好主意。直观的方法——像在 SQL 中那样在 FROM
子句中指定多个类——WQL 不支持。解决方案是查询基类的实例,然后使用类的*系统属性*限制结果。WMI 系统属性是每个类都存在的伪属性。除了其他信息,它们的目的是提供反射信息。例如,可以通过访问 __CLASS
属性来检索实例上的类名。
在一个 SELECT
语句中要查询实例的所有类都必须共享至少一个共同的基类,无论是直接的还是间接的,该基类在 FROM
子句中指定。这意味着不共享基类的类不能以这种方式查询。例如,可以这样查询 Win32_Keyboard
和 Win32_PointingDevice
类的实例
SELECT * FROM CIM_UserDevice WHERE ( __CLASS="Win32_PointingDevice" )
OR ( __CLASS="Win32_Keyboard" )
通常,返回的枚举将是异构的。因此,必须注意在每个实例上使用正确的属性名称等。同样,可以使用 _CLASS
属性来识别类。
III 异步和远程查询
异步查询(AsyncPing 项目)
到目前为止,查询 WMI 一直以同步方式进行。也就是说,WMI 消费者线程启动查询,然后等待结果传播回来。特别是,在等待时,线程停止执行,并且只有在查询完成后才恢复执行。虽然这显然有效并兑现了其承诺,但它有一个明显的缺点——WMI 查询可能需要一些时间才能完成;查询线程可以通过计算其他任务来利用这些时间,从而更好地利用资源。
WMI 提供异步查询。进行异步查询时,查询线程不会等待查询完成。相反,它提供实现特殊 COM 接口的类实例,称为*接收器*。接收器实现特定方法,WMI 提供程序代表消费者调用这些方法来处理 WMI 查询的结果。这意味着 WMI 提供程序和消费者在进行异步查询时都进行进程外执行。对于消费者来说,结果计算是在进程外完成的(就像同步查询一样)。对于提供程序来说,结果处理是在进程外完成的,因为是提供程序触发处理,而不是消费者。这种情况对安全性有一些影响,将在下一小节中讨论。
在进行异步查询时,调用不会立即返回任何 IWbemClassObject
实例。相反,IWbemObjectSink
类的一个实例被传递给 IWbemServices::ExecQueryAsync
。这是 IWbemServices::ExecQueryAsync
的原型
HRESULT ExecQueryAsync( const BSTR strQueryLanguage, const BSTR strQuery, long lFlags, IWbemContext* pCtx, IWbemObjectSink* pResponseHandler )
lFlags
参数用于控制查询执行方式的某些细节。例如,与同步查询一样,可以请求返回双向迭代器。此外,可以指示 WMI 向接收器报告正在进行的调用的间歇状态。pResponseHandler
是实现实际结果处理的接收器实例。从 API 角度来看,这是两种方法中仅有的两个区别。异步执行 Win32_PingStatus
查询的代码如下所示
CComPtr< CPingSink > pPingSink = new CPingSink; hr = service->ExecQueryAsync( L"WQL", L"SELECT * FROM Win32_PingStatus " \ L"WHERE Address=\"127.0.0.1\"", 0, NULL, pPingSink );
CPingSink
是前面提到的接收器类。它实现了 COM 接口 IWbemObjectSink
。此接口定义了两个方法(当然,除了 IUnknown
的方法之外)
HRESULT Indicate( long lObjectCount, IWbemClassObject** apObjArray ); HRESULT SetStatus( long lFlags, HRESULT hResult, BSTR strParam, IWbemClassObject* pObjectParam );
调用 IWbemObjectSink::Indicate
来处理 apObjArray
中返回的查询结果。此数组中的结果数量由 lObjectCount
指示。如果需要,提供程序可以多次调用 Indicate
。WMI 提供程序调用 SetStatus
向消费者指示完成和/或进度信息。如果已在 ExecQueryAsync
的 lFlags
参数上设置了 WBEM_FLAG_SEND_STATUS
,则提供程序可以多次调用 SetStatus
。如果未设置,则只调用一次并指示查询完成。在前者情况下,strParam
和 pObjectParam
用于复杂的错误信息。
使异步查询更安全(SecAsyncPing 项目)
与 WMI 异步交互的决定有两个主要影响。同步查询本质上是简单的单线程,而异步查询——或者更确切地说,涉及的结果处理——更像是多线程类型:查询线程(即消费者)在启动查询后继续执行,一段时间后 WMI 线程将执行对象接收器的回调。控制对象接收器生命周期的代码必须反映这一点。
第二个含义是第一个含义的直接推论——由于 WMI 在消费者的进程中执行对象接收器的回调,它可能使用与消费者不同的安全上下文来执行此操作。因此,具有较低权限的 WMI 提供程序可能被允许访问具有较高权限的消费者的数据结构。对于消费者来说,它是一个受信任的组件。显然,在某些情况下这是不可接受的。下一小节将讨论的一种解决方案是改为使用半同步查询。另一种解决方案是在不安全的单元中执行对象接收器。
在不安全的单元中执行接收器意味着在另一个特殊的进程中执行它,该进程执行访问检查。根据消费者运行在 Windows XP 还是 Windows Server 2003 平台,这方面的细节略有不同。基本思想是围绕一个存根对象进行包装,该存根对象被传递给 IWbemServices::ExecQueryAsync
而不是接收器对象。存根对象将提供程序的调用中继到实际的接收器,接收器的执行由 UNSECAPP.EXE 托管。访问检查由 UNSECAPP.EXE 在 Windows Server 2003 的情况下或由接收器本身在 IWbemObjectSink::Indicate
和 IWbemObjectSink::SetStatus
方法中执行。WMI 设置和接收器对象的结果处理与上一小节中的完全相同。
为了实现这种方法,WMI 消费者通过调用 CoCreateInstance
实例化 IUnsecureApartment
类
CComPtr< IUnknown > pApartment; HRESULT hr = CoCreateInstance( CLSID_UnsecuredApartment, NULL, CLSCTX_LOCAL_SERVER, IID_IUnsecuredApartment, reinterpret_cast< void** >( &pApartment ) );
在 Windows Server 2003 下,返回的实例实际上是 IWbemUnsecuredApartment
类。使用此类而不是也受支持的 IUnsecuredApartment
使回调的安全性更容易实现。当然,在 Windows Server 2003 下运行的消费者如果愿意,可以使用 IUnsecuredApartment
。这里应该指出的是,不要直接实例化 IWbemUnsecuredApartment
。MSDN 在此示例中完全错误。
实际的存根创建非常简单。在 Windows XP 下运行时,消费者调用 IUnsecuredApartment::CreateObjectStub
HRESULT CreateObjectStub( IUnknown* pObject, IUnknown** ppStub )
pObject
是接收器对象,ppStub
是新创建的存根对象。如果 pObject
为 NULL
,该方法将返回 E_POINTER
。如前所述,访问检查的实现是消费者的责任。在示例项目中,这是 CPingSinkUnsecure
类。
在 Windows Server 2003 下,如果注册表值 HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\WBEM\CIMOM\UnsecAppAccessControlDefault 为零(默认值),则 UNSECAPP.EXE 不执行访问检查。在 Windows Server 2003 之前的平台上,此密钥不可用。创建存根对象时,此行为可以被覆盖。存根创建是通过调用 IWbemUnsecuredApartment
实例上的 CreateSinkObject
来完成的
HRESULT CreateObjectSink( IUnknown* pSink, DOWRD dwFlags, LPCWSTR wszReserved, IWbemObjectSink** ppStub )
在 pSink
中调用原始接收器后,dwFlags
的值控制 UnsecApp.exe 的行为:WBEM_FLAG_UNSECAPP_DEFAULT_CHECK_ACCESS
使用上面提到的注册表,WBEM_FLAG_UNSECAPP_CHECK_ACCESS
和 WBEM_FLAG_UNSECAPP_DONT_CHECK
执行或不执行访问检查,忽略注册表值。例如,要强制执行访问检查,可以像这样调用 CreateSinkStub
CComPtr< IWbemObjectSink > pStub; CComPtr< IWbemUnsecuredApartment > aprt = pApartment->QueryInterface( IID_IWbemUnsecuredApartment, reinterpret_cast< void** >( &aprt ) ); HRESULT hr = aprt->CreateSinkStub( pSink, WBEM_FLAG_UNSECAPP_CHECK_ACCESS, NULL, &pStub );
然后,消费者传递存根而不是接收器对象,就像之前在上一小节中一样执行查询
hr = service->ExecQueryAsync( L"WQL", L"SELECT StatusCode FROM Win32_PingStatus " \ L"WHERE Address=\'127.0.0.1\'", 0, NULL, pStub );
半同步查询(SemiSyncPing 项目)
如上一小节所述,异步查询可能会引起一些安全问题,因为它们假定 WMI 提供程序是受信任的组件。在某些情况下,这是不可接受的。WMI 消费者可以不回退到效率较低的同步查询方法,而是选择进行*半同步*查询。这是一种执行模式,它将同步查询结果的安全处理与异步查询结果计算的多线程特性结合在一起。同时,它消除了实现接收器类的需要;结果直接由 IEnumWbemClassObject
实例返回,就像同步查询一样。
因此,与同步查询相比,半同步查询的机制非常简单:同步查询计算其结果(可能在另一个线程或进程中),并在完成时返回一个准备好的枚举器作为结果,而半同步查询则准备枚举器,将其元素标记为待定,然后返回,同时在另一个线程中继续实际的结果计算。在这两种情况下,当调用 IEnumWbemClassObject:Next
时,此方法检查枚举中下一个元素的状态,并在必要时等待指定的时间,直到元素的状态变为非待定。
随附的源代码反映了同步和半同步机制之间的这些相似之处。除了 lFlags
中增加了一个 WBEM_FLAG_RETURN_IMMEDIATELY
标志之外,对于半同步查询,ExecQuery
调用是相同的。此标志指示 WMI 以半同步方式处理查询,并立即完成 ExecQuery
调用,返回 WBEM_S_NO_ERROR
作为结果代码(前提是没有其他错误条件待处理)
CComPtr< IEnumWbemClassObject > enumerator; hr = service->ExecQuery( L"WQL", L"SELECT * FROM Win32_PingStatus " \ L"WHERE Address=\"127.0.0.1\"", WBEM_FLAG_FORWARD_ONLY | WBEM_FLAG_RETURN_IMMEDIATELY, NULL, &enumerator );
调用成功完成后,消费者现在通过调用 IEnumWbemClassObject::Next
并在 lTimeOut
参数中给定超时时间,主动从 enumerator
中返回的枚举中轮询结果
while ( ( hr = enumerator->Next( 1L, 1L, &ping, &retcnt ) ) == WBEM_S_TIMEDOUT );
对于同步查询,此参数设置为 WBEM_INFINITE
,表示超时永不失效。WBEM_NO_WAIT
表示根本没有超时。如果枚举中的结果可用,Next
调用将返回 WBEM_NO_ERROR
结果代码。超时过期由返回 WBEM_S_TIMEDOUT
表示。如果结果集为空或已到达末尾,则返回 WBEM_S_FALSE
。
如果枚举包含多个元素,并且消费者确定它不想使用所有这些元素,则在枚举上调用 Release
将导致 WMI 停止计算结果,并最终释放枚举及其内容。
半同步查询是非同步查询的推荐方式。它们比异步查询更容易设置,并且避免了与异步查询相关的多线程和安全问题。但是,在某些情况下可能需要异步方法。例如,在某些场景中,主动轮询的消费者要求可能难以实现。
IV 事件通知查询
到目前为止,WMI 已被用于检查代表托管对象的简单数据。但还有更多内容。WMI 还允许应用程序在触发某些事件时收到通知。请求此类通知是通过进行*事件通知查询*来完成的。这种特殊类型的查询与之前看到的查询有两种不同:首先,事件通知查询可以是异步或半同步的,但绝不能是同步的。其次,事件消费者的编程模型略有不同。
永久和临时事件消费者
对从 WMI 接收事件通知感兴趣的应用程序称为*事件消费者*。事件消费者有两种类型——*永久*事件消费者和*临时*事件消费者。临时事件消费者是希望在一段时间内接收事件通知的应用程序。这段时间过去后,通常受应用程序生命周期的限制,应用程序会向 WMI 注销其查询。相比之下,永久事件消费者在应用程序完成后仍将保持活动状态。Windows XP 预装了一些永久事件消费者,例如,允许将事件与命令行脚本的执行绑定。这些预装的消费者被称为*标准*事件消费者。由于两种事件消费者在编程角度上差异不大,因此本文的其余部分将不讨论永久事件消费者。
为了向 WMI 注册以接收事件通知,事件消费者会以 WQL 语句的形式准备一个查询。在 WQL 查询中,事件类型由 FROM
子句指定。像往常一样,可以使用基类来指定多种类型。但是,为了构成有效的查询,指定的类必须派生自 __Event
类。MSDN 列出了可能的(基)类。与 WMI 数据模型更改相关的事件称为*内在*事件。具体来说,内在事件由 WMI 本身生成;它们不需要提供程序存在。内在事件仅限于类的创建、修改和删除,实例和命名空间以及一些更具管理性质的事件。
反映 WMI 数据模型之外变化的事件称为*外在*事件,并且必须派生自 __ExtrinsicEvent
。这里仅为完整性而提及它们。
事件的 WQL 查询(DiskChange 项目)
如上所述,要请求事件通知,消费者会执行一个 WQL 查询,并在 FROM
子句中指定事件类。除了之前看到的查询之外,FROM
子句之后还必须跟着一个 WITHIN
子句。此子句指定一个以秒为单位的时间间隔,告诉 WMI 多久更新一次包含结果的枚举。这是与前几节中遇到的查询类型的主要区别——WMI 会持续向消费者发送事件通知(通过查询调用中传递的枚举或接收器),直到被告知停止。这是通过在枚举上调用 Release
或在异步情况下对接收器对象调用 IWbemServices::CancelAsyncCall
来完成的。
内在事件的数量很少,且性质更为通用——类的创建、修改和删除或实例。因此,它们不直接携带相关信息。相反,它们具有一个引用实际数据的属性。例如,引用实例操作的事件类包含一个 TargetInstance
属性,该属性继承自 __InstanceOperationEvent
,用于指定所涉及的实例。此属性在 WHERE
子句中通过使用 ISA
运算符与感兴趣的类进行检查。
逻辑磁盘驱动器(软盘、CD/DVD 驱动器或硬盘)的更改由 __InstanceModificationEvents
指示,其中 Win32_LogicalDisk
的实例作为 TargetInstance
值。因此,可以通过以下 WQL 语句请求此类更改的事件通知
SELECT * FROM __InstanceModificationEvent WITHIN 10
WHERE TargetInstance ISA 'Win32_LogicalDisk'
此处指示 WMI 每 10 秒检查所有可用的 Win32_LogicalDisk
实例是否有新的修改,并相应地通知事件消费者。例如,此类事件是由插入 CD 或 DVD 到驱动器中触发的。当然,这种相当通用的通知请求可以通过将其限制到特定实例来使其更具事件特异性
SELECT * FROM __InstanceModificationEvent WITHIN 10
WHERE ( TargetInstance ISA 'Win32_LogicalDisk' )
AND ( TargetInstance.Name = "G:" )
此查询将其结果限制为逻辑驱动器“G:”。
事件通知查询不是通过调用 IWbemServices::ExecQuery
,而是通过在异步情况下调用 IWbemServices::ExecNotificationQuery
或 IWbemServices::ExecNotificationQueryAsync
来进行的。在这两种情况下,参数与普通查询相同,但有一个额外的限制:对于 ExecNotificationQuery
,必须指定 WBEM_FLAG_RETURN_IMMEDIATELY
和 WBEM_FLAG_FORWARD_ONLY
标志。如果未指定,则调用将失败。因此,调用 IEnumWbemClassObject::Reset
对此类枚举没有影响。
检索结果事件的方法与半同步查询完全相同(事件通知查询至少是半同步的!)。此外,只要在枚举上未调用 Release
,它就可以接收更多事件。
闭幕词
本文重点关注 WMI 消费者的视角。然而,许多方面仍未涉及,特别是那些与托管对象更直接交互的方面几乎未被触及。例如,遍历文件系统或目录管理结构以及如何利用它们来复制文件。WMI 不仅允许访问预定义的数据结构,还提供元数据功能来扩展现有结构并定义新结构。所有这些都与 WMI 提供程序的相反角色紧密相关,也许另一篇来自 WMI 提供程序方面的文章会更合适。
注意事项
一如既往,看似出色的系统如果仔细观察,也会出现缺点。WMI 也是如此,它确实会带来一些值得一说的严重问题。
防火墙和 RPC
作为 DCOM 应用程序,WMI 和 WMI 消费者在远程访问对象时将使用 RPC 服务。如今,这几乎肯定会引发防火墙配置问题。根据经验,TCP 端口 135 和 445 必须从防火墙的阻止列表中豁免,同时豁免 DCOM 动态分配的端口。最小化这些端口的数量可能相当棘手。
当在大型、异构管理的 корпоратив网络中跨子网运行时,如果系统管理员拒绝打开 TCP 135 和 445 端口,这将成为一个障碍。
请记住删除所有事件通知请求
进行事件通知查询时,请记住对这些查询创建的所有 IEnumWbemClassObject
实例调用 IUnknown::Release
。如果未这样做,例如通过 CTRL-C 不优雅地退出程序,WMI 将继续处理这些查询,尽管不再返回结果,但它将以惊人的速度和效率占用 CPU。另请参阅知识库中的 KB327542。
WMI 查询可能需要相当长的时间
某些类型的查询可能需要相当长的时间才能完成。例如,事件通知查询可能需要几秒钟才能交付第一个事件,即使指定了短的轮询间隔。在设计应用程序时,应该解决这个问题。
异步查询和 OLE
似乎异步查询不喜欢由单元线程启动,即调用 CoInitializeEx
时使用 COINIT_APARTMENTTHREADED
的线程。它们表现出的症状是 ExecQueryAsync
返回 WBEM_S_NO_ERROR
,但接收器从未被调用。直观的解决方法是改用 COINIT_MULTITHREADED
或进行半同步查询。
但是,有时无法使用 COINIT_MULTITHREADED
,因为应用程序需要在单线程单元中运行。例如,OLE 客户端就是这种情况。解决这种情况的方法是将所有 WMI 消费者内容分离到自己的线程中,并为该线程初始化自由多线程。