Windows2000 服务器端应用程序开发设计指南-效能监视

  管理者、使用者以及开发者都知道监控电脑系统的健康情形是很重要的。由于Microsoft觉察到此事实,所以将效能监视建构在Windows 2000中。不幸的是,几乎没有应用程序使用到效能监视的功能。以下是几个原因:
  • Microsoft没有提供容易的显示效能资讯方法。
     
  • 开发者必须在它们的应用程序中承担显示效能资讯所需花费的时间。
     
  • Microsoft没有强调或提升效能监视的重要性。
     

我先检查了几年前在自己的应用程序中加入的效能资讯,对这个工作的复杂性大吃一惊,并尽可能的延迟了该工作。最后我的解决方案是建立一个C++ 类别,将一个使作业系统能看见效能资讯之处理程序封装起来,并让我能容易地将效能资讯加入任何应用程序。本章最后将会列出此C++ 类别。

效能监视的观点
 

在我们开始学习在程序中显示效能资讯的相关内容前,先解释Windows提供之效能监视的基本能力。您可以从许多观点来检查效能监视,而我也愿意对它们做些讨论。

让我们先从一个使用者的观点开始对效能监视做些检查。我将会解释系统如何组织效能资讯以及管理者、使用者和开发者该如何使用System Monitor ActiveX控制来测量系统的健康情形。

然后讨论一些开发者将效能监视加入应用程序(或者可能加到一个Windows服务中)的一般原因。对于这个介绍性的教材,我将会提出更多技术性的内容,并从系统和程序设计的观点来讨论Windows效能监视的架构。

从使用者观点看效能监视
 

系统监视器(System Monitor)是被包含在Windows内的一个ActiveX控制,允许管理者察看效能资讯。因为我对不熟悉此控制的人数大为吃惊,所以决定从一个简短的介绍开始。

为了试验此ActiveX控制,您可以从系统管理工具功能表中选择效能,或者您可以使用以下的方法在Microsoft管理主控台中(Microsoft Management Console,MMC)增加一个ActiveX控制:

  1. 执行Microsoft管理主控台应用程序(MMC.exe)。
  2. 从主控台功能表选择新增/移除嵌入式管理单元,然后按下新增按钮。
  3. 在新增独立嵌入式管理单元对话方块中选择ActiveX控制,然后按下新增按钮。
  4. 在插入ActiveX控制项精灵的画面按下一?按钮。
  5. 选择System Monitor Control并按下一?按钮,接下来再按下完成按钮。
  6. 在新增独立嵌入式管理单元对话方块中按下关闭按钮。
  7. 在新增/移除嵌入式管理单元对话方块中按下确定按钮。
  8. 在左窗格中选择系统监控程序节点。

在您正确地完成了每一步骤后,MMC视窗应该显示如图7-1的内容。

一开始,系统监控程序对于您想要监控的效能资讯没有任何概念,所以它的图表是空的。要将资讯加入图表时,您必须在工具列上按下新增(+)钮,以显示新增计数器对话方块(如图7-2所示)。如您所见,在对话方块中有很多选项可供使用者选择。在本章节中讨论效能计数器时,为了要让您知道如何组合这些资讯,所以现在我将要解释这些选项的内容。


 

 图7-1 在MMC中的系统监控程序项


 

 图7-2 系统监控程序的新增计数器对话方块

您的第一个要决定的是该从哪些电脑收集效能资讯。系统监控程序最佳的特性之一是它具有收集本地机器与远端机器之效能资讯的能力。事实上,它能够同时收集两个机器的资讯,并显示在单一图表中。这可以使管理者可以很容易的比较二台或更多电脑的效能资讯。

一旦您选择了一台电脑后,接着便会选择一个效能物件。效能物件即是一个在系统中提供效能资讯的元件。离开这个对话方块后,Windows便会显示许多物件,大部份是那些与系统相关的物件。这里有一些关于系统物件的范例:处理器(CPU本身)、处理程序(正在执行的应用程序)、实体磁盘机(硬盘机)、系统(作业系统本身)、线程(正在处理程序中执行的线程)以及内存(RAM)。

记住效能监视并没有局限在作业系统元件;设备驱动程序也能显示它们的效能物件。Telephony和TCP即是设备驱动程序物件的范例。当在电脑上设定Windows时,所有这些物件都具有特定的用处。

在效能监视的设计上,Microsoft没有缺乏远见:系统也允许服务及应用程序去显示它们自己的效能物件。一些服务的范例包含Indexing Service以及Distributed Transaction Coordinator(DTC)皆是。如何从一个服务或应用程序显示效能物件是构成本章大部份的内容。

效能物件的设计者也定义了物件支援的计数器。例如,在新增计数器对话方块中,处理程序物件提供了多个计数器的选择(显示在从清单选择计数器方块中)。清单方块中的每一个项目都表示您可以监控之关于处理程序的项目。例如,% Processor Time计数器将为您展示实际在处理器上执行的一个处理程序内之线程的百分比时间。Handle Count计数器会显示有多少核心物件已经被处理程序开启。ID Process计数器则显示全系统当处理程序被建立时,其被指定的唯一识别码。这些计数器仅仅表示可用于一个处理程序物件之计数器的的采取样品。

选择一个效能物件后,现在您可以将注意力转移到对话方块中的从清单选取例项部份。一个 实例 即是被给定至一个物件类型的实例名称。例如,在系统中执行的Process物件有许多的实例;每一个Process物件实例被它的 .exe文件名称而确认。大部份的物件皆支援实例,然而有一些却没有。例如,因为只有一个作业系统在电脑上执行,所以System物件从清单选取例项方块中并没有显示任何的项目。

图7-3显示了物件、实例以及计数器之间的关系。在左边,您会看到一个支援实例的物件;它也许会拥有零或多个现在与它关联的实例。每一个这些实例皆相同的计数器号码,但是计数器的值则会因不同的实例而有所不同。记住若一个支援实例的物件没有现在与它关联的实例,那么就不能取得计数器资讯。

位于右边之不支援实例的物件永远只有一组与它关联的计数器。


 

 图7-3 物件、实例以及计数器之间的关系

如同您在操件新增计数器的对话方块,您将会看见许多可用的物件,每一个计数器皆拥有一组自己的隐秘名称。若您按下对话方块中的解说按钮,会出现一个分离的视窗(如图7-4所示),它提供了现在被选择之计数器的说明。


 

 图7-4 解说文字视窗,提供一个关于效能计数器的附加资讯

Windows实际上允许解释性的文字与一个除了它本身的计数器外的效能物件。然而,目前系统监控程序并没有提供任何显示物件说明文字的方法。我希望Microsoft将来可以加强这方面的控制。

在选择了一个电脑、物件、计数器以及实例(如果可用的话)后,按下新增按钮以使系统监控程序开始绘制被指定的资讯。当您按下新增按钮时,新增计数器对话方块并不会被关闭,所以您可以容易地新增多个计数器至图表中。记住当您在新增计数器资讯至图表中时,您可以使用Shift和Ctrl键来选择多个实例和计数器。将被绘制在图表中的线段号码即是产生计数器号码时的实例号码。一旦您完成了新增计数器的动作后,请按下关闭按钮。

顺便一提,您将会注意到有一些多实例物件支援一个称为 _Total的虚拟实例。此虚拟实例实际上并不是一个物件的实例,然而它允许您方便地察看物件的所有实例计数器的总数。例如,选择Process物件之Page Faults/sec计数器的 _Total实例使图表显示所有处理程序的Page Faults/sec内容。

为一些计数器显示总数并没有任何意义。例如,如果您要为所有的处理程序实例绘制处理程序识别码的内容,您可以看到这个计数器仅展示一个为0的值,这是从不改变的。

从设计者的观点看效能监视
 

考虑将效能计数器资讯加到您的应用程序和服务器的原因有很多种。首要的是,就管理者而言,您想要容易的使用它并帮助办公室的人们检查电脑系统的健康情形。另外,许多应用程序的终端使用者不想陷入对他们毫无意义的统计资讯泥沼。权宜之计是使用效能计数器来显示资讯;一个让使用资讯做某些事的人可以容易的获得并存取它的方法。同样地,Windows跨越网路而使效能资讯为可用的。如前面章节中所提,一个使用系统监控程序的人可以选择要从哪台电脑收集资讯。使用Windows内建的效能监视显示您的应用程序效能资讯,并使您不需做任何附加的工作即可远端地存取它。

一个非常重要的议题是让许多人取消对执行效能的效能监视功能,这有些讽刺。效能监视并非不受控制的,它存在您应用程序内部的某处,您需要配置一个内存区块以储存现行的计数器资料,而且您的程序代码必须周性的更新这些值。更新这些值表示您的程序代码必须够大而且将会缓慢地执行,有些事我们总是试着去避免。然而,假如效能监视大大地受系统影响,则不会有人使用它,而且当Microsoft在设计效能监视到系统时,必然有考虑它。

当在为您的应用程序设计效能资讯时,必须小心地决定您想要监视的内容。例如,如果您有一个固定的计算回圈,当执行至回圈内时,您应该避免更新计数器的内容。从另一方面来说,您可能会编写一个服务器端的Client/Server应用程序。在这种情况下,您可能想要拥有一个追踪多少客户端已经连接的效能计数器、多少客户端目前已连接、每个客户端接收到的位元组数、每个客户端传送的位元组数、您的客户端建立了什么类型的要求等等。事实上,Microsoft Internet Information Services(IIS)持续确切地追踪了此资讯类型。

效能监视可以成为软件开发者的一个强大工具。我曾使用效能计数器来监控我的应用程序总共配置和释放了多少动态内存。如果忘记释放内存,系统监控程序会总是显示我的内存使用现为增加的情形。开发者和测试者一样可以使用效能计数器来监视任何一个正确运转的应用程序。

为了说明效能监视,我想展示一个建立股票物件的范例程序。这个物件的每一个内建符号中皆有一个实例,而计数器的内容则为有关股票的资讯—例如,最后的价格、高的价格、低的价格以及价格/利润比率。使用系统监控程序,您可以选择其中您有兴趣的股票和计数器以及在Internet上取得此资讯的应用程序,并使用系统监控程序绘制它的图表。不幸的是,我找不到一个允许我从它的股票行情指示器资讯有效地产生衍生工作的Internet站台。所以,虽然本章展示了一个不同的范例应用程序,股票物件的概念应该可以给您一个继承Windows效能监视之可能性的好想法。

效能物件及计数器的架构
 

为了显示您自己的效能资讯,您必须修改许多登录的内容。您可以把这些修正视为陷入二个群组的情形。第一组修正告诉系统您存在的计数器;第二组修正告诉系统如何为您的计数器取得效能资讯。如图7-5所示,初始的修正在以下的登录子机码下执行:

HKEY_LOCAL_MACHINE/SOFTWARE/Microsoft/Windows NT/CurrentVersion/Perflib


 
图7-5 ,您会看见在Perflib子机码下的二个Last Counter和Last Help值。这二个值分别告诉您在Counter和Help中使用的最大数字值。之前在我的登录中,Last Counter的值为2276,任何被加入之新的物件或计数器会从2278开始。同样地,任何被新增的Help文字会从2279开始。Microsoft为系统本身保留了一些数字。Base Index value(1847)指出Microsoft为了系统拥有的物件和计数器所保留的最大值。您加入的物件和计数器必须高于此数字。

图7-5 表示您已存在之计数器的登录设定

在Perflib子机码下是另一个子机码009,它符合定义在WinNt.h中的LANG_ENGLISH符号。在009子机码下有二个值:Counter和Help。这二个值是REG_MULTI_SZ类型。您可以用Windows中的RegEdt32.exe登录编辑器工具程序来检查或编辑这二个登录值。可惜的是,您不能使用RegEdit.exe工具(也附在Windows中),因为此工具有一个防止显示或编辑超过32 KB的内部限制。

计数器值包含一组定义物件及计数器的字串,以使系统意识到它。以下所列的内容显示了在这个值内的字串之一些范例:

2System4Memory6%Processor Time10File Read Operations/sec12File Write Operations/sec14File Control Operations/sec16File Read Bytes/sec18File Write Bytes/sec

Help值包含了一组说明效能物件或计数器意义的字串。以下是一个小型的Help值范例:

3System效能物件由应用在电脑上超过一个元件处理器的实例所组成。15Memory效能物件由在电脑上描述实体与虚拟内存行为的计数器组成。实体内存是电脑中的随机存取记忆体总数。虚拟内存则是由实体内存和磁盘的空间组成。多数的内存计数器会监视分页的情形,即是磁盘与实体内存间字码及资料页的移动情形。过度的执行分页会产生内存不足的状况,并导致所有的系统处理程序产生延迟。7%Processor Time是指处理器执行非闲置线程的时间百分比。这个计数器被设计成处理器活动的主要指示器。其计算方式是,先计算在某个取样区间中,处理器执行闲置处理程序所花费的时间,再用100% 减去这个时间(每个处理器都有一个闲置线程,如果没有执行其他线程的话,闲置线程会不停地执行回圈来消耗时间)。您可以检视取样区间中工作执行状况的百分比,这个计数器会显示取样区间中执行工作的平均百分比。它的计算方式是,监视服务停用时间,然后用100% 减去这个时间值。9%Total DPC Time是在取样区间中,处理器花在接收及服务Deferred Procedure Call(DPC)的时间百分比(DPC是一种插断,它比一般插断的优先权要低)。它是在电脑中所有处理器的Processor:DPC Time加总,经由处理器的数字而分开。% DPC Time是 % Privileged Time的组成元件,因为DPC在特殊权限模式中执行。它们被分开计算,而且不包含在插断计数器中。这个计数器显示取样区间中的平均忙碌时间百分比。11File Read Operations/sec是指电脑上所有磁盘中文件系统读取要求的结合速率,包括对文件系统快取的读取要求。计算方式是计算读取次数。这个计数器会显示最后两次取样观察值的差异,再除以取样区间的时间。13File Write Operations/sec是指电脑上所有磁盘中文件系统写入要求的结合速率,包括对文件系统快取的写入要求。计算方式是计算写入次数。这个计数器会显示最后两次取样观察值的差异,再除以取样区间的时间。15File Control Operations/sec是指文件系统其他操作的结合速率(不是读取也不是写入),例如对档案系统控制的要求及设备特性或状态资讯的要求。这是System:File Data Operations/sec的反转值,而且是操作次数的计数值。这个计数器会显示最后两次取样观察值的差异,再除以取样区间的时间。17File Read Bytes/sec是指在电脑上所有设备的文件系统读取要求所读取位元组的整体速率,包括对文件系统快取的读取。计算方式是计算每秒位元组数目。这个计数器会显示最后两次取样观察值的差异,再除以取样区间的时间。19File Write Bytes/sec是指在电脑上所有设备的文件系统写入要求所写入位元组的整体速率,包括对文件系统快取的写入。计算方式是计算每秒位元组数目。这个计数器会显示最后两次取样观察值的差异,再除以取样区间的时间。

您会注意到这些Counter的字串值是成对的。每一对字串值是由一个偶数和一个短字串组成。Help字串值也是成对的,但是它的数字是奇数。系统使用Counter值来决定哪一个效能物件和计数器在系统上是可用的。Help值则确认每一个物件计数器的说明文字;Help文字的数字值永远是一个大于物件或计数器的数字值。如前所述,系统监控程序目前没有为效能物件显示解说文字的方法。这表示您将不会看见与例如号码3及号码5相关的解说文字。

为了将您的效能物件及计数器加到系统,您必须将您的物件、计数器及说明字串到这些登录值中。请回头参阅 

现在我们将注意力转到登录的其他部份。为了显示您的效能物件、实例和计数器,必须建立一个负责回传效能资讯的DLL。一旦该DLL被建立,您必须将对登录建立的一些修改情形告诉系统。然而这些修改会被建立在登录的另外一个部份,如图7-6所示:

HKEY_LOCAL_MACHINE/SYSTEM/CurrentControlSet/Services/HWInputMon/Performance

这个唯一的子机码的HWInputMon部份确认定我的效能资料。当然,您将会用一个唯一的识别名称来取代您效能资料的部分。


 

 图7-6 在登录指示系统如何从DLL询问效能资讯的设定

在这个子机码中有许多个资料值。最重要的值、程序库指定了知道如何回传您的效能资讯之DLL的路径名称。此DLL必须汇出叁个函数:一个开启函数、一个收集函数以及一个关闭函数。您可以为这些函数选择任何您想要的名称,但是在登录中这些名称必须被指定为使用Open、Collect和Close值。

当被要求效能资讯时,系统将会载入您的DLL并立即它的呼叫Open函数。您的Open函数看起来应该像这样:

DWORD __declspec(dllexport)WINAPI Open(PWSTR){ 	// 注:参数通常会被忽略	// 初始化DLL	return(ERROR_SUCCESS);}

它给您的DLL一个初始化自己的机会。一旦初始化后,系统会周期性地呼叫您的Collect函数。此函数负责初始化一个包含所有您想回传之效能资讯的内存缓冲区。我将会在稍后的〈收集效能资料〉一节中讨论Collect函数的原型,并提供有关实作的详细内容。

当系统决定载出您的DLL时,它会呼叫Close函数,并给您执行任何清除所需动作的机会。Close函数看起来应该像这样:

DWORD __declspec(dllexport)WINAPI Close(){	// 清除DLL	return(ERROR_SUCCESS);}

目前为止所讨论的四个登录值只有一个是系统需要的。然而,对一些附加的登录值来说,它经常是有用的,如图7-6所示。就像之前曾提起的,当您增加新的效能物件和计数器至系统时,您必须附加您的效能物件和计数器字串至登录中。这些新的字串必须是唯一的数字。对您的效能DLL来说,记得哪些物件与计数器号码已经被指定是绝对必要的。最容易储存这个资讯的地方是在Performance登录子机码中。图7-6显示了我因为此原因而在登录中加入的First Counter、Last Counter、First Help以及Last Help值。当我的DLL被载入时,我开启了登录、取出这些值,并且根据我的Collect函数储存它们。因为当系统呼叫Collect函数时,它会传递这些数字以确认哪一些效能资讯应该被回传,所以需要知道您的物件和计数器的数字。

现在您可能想要知道您的DLL被载入到哪个处理程序中。这里有二种情形,依据您的效能资讯是由本地还是远端要求而定。当被系统监控程序从本地端机器询问效能资讯时,系统会将您的DLL载入经由系统监视器而建立的处理程序之位址空间(请参阅图7-7)。因为DLL正在另一个处理程序的位址空间中执行,所以彻底地测试您的程序代码并确认它是否健全的动作非常重要。如果您的程序代码中包含了任何的无穷回圈、呼叫ExitProcess或等待一些线程同步物件的死结情形,对您的处理程序将有不利的影响。


 

 图7-7 本机的效能监视

Microsoft设计了一个健全的系统监控程序以抵挡许多「被动式攻击」的行为。例如,当呼叫一个DLL的Collect函数时,系统监控程序会产生一个分离的线程;如果那个线程没有在一个固定的时间内回传,系统监控程序便会删除它,并且继续执行。系统监控程序也将对您的DLL函数之呼叫包装在一个结构化例外处理之架构中,以捕捉任何违规存取、除以0或其他您的程序代码可能产生的硬体错误。

当您的效能资讯被一个远端电脑所询问时,会出现稍微不同的情形,如图7-8所示。在这个情形下,远端电脑实际上是与一个内含WinLogon.exe之RPC服务器对谈。当WinLogon侦侧到一个对效能资讯的要求时,它会将您的DLL载入WinLogon的位址空间。然后从您的Collect函数回传的资讯会被安排至提出要求的机器中。显然地,这个远端通讯是为了系统监控程序而发生。


 

 图7-8 远端的效能监视

在WinLogon的位址空间里载x入一个效能DLL的分派比在应用程序的位址空间里载入DLL更为重要。如果DLL现在呼叫了ExitProcess函数,WinLogon将会终止执行,并会使整个作业系统当机。再次申明,测试效能DLL程序代码是相当重要的一件事。

收集效能资料
 

系统监控程序透过建立对登录的呼叫来要求效能资讯。为了收集效能资讯,负责提出要求的应用程序必须先呼叫RegQueryValueEx,如下所示:

// 用来询问效能资料的特殊登录机码HKEY hkeyPerf = HKEY_PERFORMANCE_DATA;// 取得效能资料之缓冲区大小DWORD cb = 10240;// 配置取得效能资料的缓冲区PBYTE pbPerfData = (PBYTE)malloc(cb);// 要求提供本地机器上的所有(「Global」)效能资料while (RegQueryValueEx(hkeyPerf, TEXT("Global"), 0, NULL,	pbPerfData, &cb) == ERROR_MORE_DATA){	// 缓冲区太小;使它变大并重试	cb += 1024;	pbPerfData = (PBYTE)realloc(pbPerfData, cb);}// 缓冲区够大// pbPerfData包含效能计数器资讯

说明

为了更清楚的说明,前面所列的程序代码中没有处理任何的错误检查。当您要在自己的应用程序中加入类似的程序代码时,请加入适当的错误检查机制。


若要重覆地要求效能资讯,只要再次呼叫RegQueryValueEx即可。RegQueryValueEx呼叫中的「Global」会告诉系统您想要回传系统中所有元件的效能资讯。这会是大量的资讯;如果您想要取得可用资讯的子集合,只要简单地将「Global」以一组物件数字取代即可(以空白分开)。例如,如果您想要取得System和Memory物件的效能资讯,可传递一个「2 4」的字串至RegQueryValuEx。您通常会传递物件数字给RegQueryValueEx,并且总是会回传与DLL符合的所有计数器和被指定之物件相关的实例资讯。

如果有一个应用程序想从一个远端电脑要求效能资讯,它必须做的事情即是在进入先前所列示程序代码之while回圈前,呼叫RegConnectRegistry:

RegConnectRegistry(TEXT("RemoteMachineName"), 	HKEY_PERFORMANCE_DATA, &hkeyPerf);

说明

在预设的情形下,管理者和系统对系统的效能计数器资讯拥有完全的存取权,同时与它互动的使用者只被允许读取权限。被应用在下列登录机码的许可权决定了谁可以存取效能计数器资讯:

HKEY_LOCAL_MACHINE/SOFTWARE/Microsoft/Windows NT/CurrentVersion/Perflib

您可以使用RegEdt32.exe工具来改变许可权。


如果您不要再建立任何效能资讯的要求时,应该呼叫以下的程序代码关闭登录:

RegCloseKey(hkeyPerf);

注意到即使您从来没有明确地呼叫RegOpenKeyEx来开启此机码,仍然应该建立此呼叫。


说明

不要在每次呼叫RegQueryValueEx后关闭机码—这个动作将会严重地损坏您的应用程序之执行效能。当一个例如系统监控程序元件呼叫RegQueryValueEx以要求效能资讯时,系统会查看目的机器的HKEY_LOCAL_MACHINE/SYSTEM/CurrentControlSet/Services子机码,并且为每一个包含Performance子机码的项目载入特定的DLL。如果这是DLL第一次被载入,DLL的Open函数会被呼叫。当RegCloseKey被呼叫后,系统会呼叫每一个DLL的Close函数并从处理程序的位址空间载出该DLL。只有在您的应用程序终止时呼叫RegCloseKey才能改善执行效能。


一旦被载入且初始化后,系统会呼叫每一个DLL的Collect函数。这是为了给DLL一个用它的效能资讯载入这个内存缓冲区的机会。Collect函数看起来应该像这样:

DWORD __declspec(dllexport)WINAPI Collect(PWSTR pszValueName, 	PVOID* ppvData, PDWORD pcbTotalBytes, PDWORD pdwNumObjectTypes){	// 收集效能资料	return(ERROR_SUCCESS);   // or ERROR_MORE_DATA if buffer too small}

第一个参数pszValueName是一个值与传递到RegQueryValueEx相同的Unicode字串。如果此字串为「Global」,那么Collect函数就必须回传它负责的所有效能资讯。如果该字串包含了一组以空白分开的数字,则DLL必须决定提供哪一个有关被要求之物件资讯,并且只回传与它相关的资讯。

ppvData参数是一个指向内存位址的指标。在输入至Collect函数中,*ppvData会指向DLL应该写入资讯之内存缓冲区。从Collect回传前,*ppvData应该被更新,以使它指向紧接在被放置至缓冲区之新资料后面的内存位址。下一个效能DLL将会从这个新的位址开始附加它的资料。

pcbTotalBytes参数是指向一个DWORD的指标,指示缓冲区的大小。在输入至Collect时,*pcbTotalBytes指示为了您的DLL而新增至缓冲区之可用资讯的位元组大小。在DLL附加它的资料至缓冲区后,Collect必须设定*pcbTotalBytes为所有被加至缓冲区之位元组总数。当Collect返回时,系统将会以剩下的缓冲区大小减去此值并传递这个新的数字至下个效能DLL。

pdwNumObjectTypes参数也是指向一个DWORD的指标,但是这个DWORD表示没有任何值被输入至Collect函数。一个单一的Collect函数可以为许多物件回传效能资讯。在Collect返回前,*pdwNumObjectTypes应该被设定为被加至资料缓冲区之效能资讯物件的数字。

例如,如果Collect函数被呼叫且pszValueName参数没有指示任何DLL负责的物件,Collect函数应该不要改变*ppvData的内容、设定*pcbTotalBytes和*pdwNumObjectTypes为0,并回传ERROR_SUCCESS。

如果Collect函数为您需要回传的资料决定了太小的资料缓冲区,那么您应该让*ppvData保持不变、设定*pcbTotalBytes和*pdwNumObjectTypes为0,并且回传ERROR_MORE_DATA。

如果Collect函数成功地将资讯附加至缓冲区,它会将新增的位元组数字附加至*ppvData、设定*pcbTotalBytes为被附加之位元组数字、*pdwNumObjectTypes则设定为被附加至物件的数字,并且回传ERROR_SUCCESS。

效能资讯之资料结构
 

在这个重点上,我们没有涵盖到的唯一项目即是Collect函数必须放置在呼叫者的资料区块里之效能资讯的格式。可惜的是,资料区块含有一套不同的资料架构,它的长度是可变的,充其量只是使这个资料缓冲区的内容建构更困难而已。图7-9显示了如何为一个物件安排效能资讯的内容。

这个特定的物件没有支援实例,但是支援了二个计数器。这意味着有叁个字串项目为了这个物件而存在登录中:一个确认物件名称且被指定其值为2938的字串;二个确定计数器名称并被指定其值为2940以和2942的其他字串。这叁个项目的Help字串分别为2939、2941以及2943。


 

 图7-9 描述一个不支援实例之效能物件的资料缓冲区

为了回报此物件的效能资讯,被传递到我们的Collect函数之资料区块首先必须有一个被放置在里面的PERF_OBJECT_TYPE结构。由于《Platform SDK》文件提供了此结构成员的细节部份,我将在表7-1中描述它们。

 表7-1 PERF_OBJECT_TYPE结构的成员
成员 说明
TotalByteLength DefinitionLength HeaderLength指示位元组大小。这些值必须被初始化。以正确地使一个像系统监视器的工具可以适当地在资料结构中流动。
ObjectNameTitleIndex ObjectHelpTitleIndex指示被指定至物件文字的数字以及当它们被加至登录时的Help内容。
ObjectNameTitle ObjectHelpTitle应该永远被设定为NULL,因为这些成员只有在应用程序要求资料时才会被使用。
DetailLevel指示此物件对大部份使用者来说有多「难以理解」。大部份的物件对初学者来说是可理解的。然而您可以指定您的物件可被初学者、进阶使用者、专家或高阶使用者所理解。
NumCounters指示物件提供了多少计数器。如图7-9所示,物件提供了二个计数器,因此有二个PERF_COUNTER_DEFINITION结构会紧接着此PERF_OBJECT_TYPE结构(我将会尽快讨论PERF_COUNTER_DEFINITION结构的内容)。
DefaultCounter当一个物件被选择时,此成员会指在预设计数器清单方块中的哪一个计数器应该被选择。系统监视器控制监控程序的新增计数器对话方块使用此成员。
NumInstances指示物件目前拥有多少实例。如图7-9所示,物件没有支援实例,所以会回传PERF_NO_INSTANCES(-1)。
CodePage如果物件支援实例,那么每一个实例皆以字串名称的形式而被回传。最好以Unicode字串格式来回传实例名称,所以设定此成员为0。然而,如果您较偏好回传非Unicode字串,那么就为实例字串名称设定为使用CodePage。
PerfTime PerfFreq应该永远被设定为0,因为这些函数只有在应用程序要求资料时才会被使用。

紧接在PERF_OBJECT_TYPE结构后的是一或多个PERF_COUNTER_DEFINITION结构,一个为每个被物件提供之计数器的结构。表7-2提供此结构成员之简短说明。

 表7-2 PERF_COUNTER_DEFINITION结构成员
成员 说明
ByteLength此结构的位元组长度。为了在此内存区块中移动而使用。
CounterNameTitleIndex CounterHelpTitleIndex当计数器和说明文字被加至登录中,指示被指定至计数器及说明的文字。
CounterNameTitle CounterHelpTitle应该永远被设定为NULL值,因为这些成员只有在应用程序要求资料时才被使用。
DefaultScale将要扩充的图表线段放大10倍,假设垂直轴为100(0说明一个扩充值为1,1表示扩充值为10,_1则说明1/10的扩充值等等)。
DetailLevel为大部份使用者指示此计数器的「理解程度」。多数的计数器是初学者可了解的。然而您还是可以说明您的计数器为初学者、进阶使用者、专家或者高阶使用者所能理解。
CounterType计数器的类型(请参阅WinPerf.h标头档内容以取得说明)。
CounterSize计数器资讯所使用的位元组数(通常为4或8位元组)。
CounterOffset从PERF_COUNTER_BLOCK开始至此计数器之第一个位元组的偏移量。

每一个PERF_COUNTER_DEFINITION结构皆定义了单一计数器的特征,但是计数器的实际值则不会从这些结构中回传。在PERF_OBJECT_TYPE结构以及所有PERF_COUNTER_DEFINITION结构之后是一个单一的PERF_COUNTER_BLOCK结构。PERF_COUNTER_BLOCK是一个长度可变的结构。它的第一个成员为ByteLength说明了在长度可变之结构中有多少位元组(必须包括此成员本身之4位元组)。紧接在此成员后是计数器的值。非常重要的一点是,必须使每一个新值都以32位元为界限而对齐,否则会发生错误例外的情形。

在PERF_COUNTER_DEFINITION结构内部的CounterSize成员指示有多少位元组已被计数器值使用,而CounterOffset成员则说明位于PERF_COUNTER_BLOCK结构内之计数器值的偏移量。

图7-10显示如何安排一个支援实例之物件的效能资讯。此特定的物件为每一个实例提供了计数器,而且目前拥有二个被定义的实例。因为只有一个计数器被定义,所以登录中的这个物件只存在二个字串项目:一个字串确认了物件名称并被指定一个2944的值;只一个字串则确定计数器名称,而且其值被指定为2946。物件及计数器的说明文字分别为2945及2947。


 

 图7-10 描述一个效能物件与二个实例的资料缓冲区,所有实例计数器值皆位于缓冲区中

这个内存区块开始处的安排就如同先前所提的范例一样:它以一个PERF_OBJECT_TYPE结构开始,其后则跟随着每一个由物件提供的计数器PERF_COUNTER_DEFINITION结构。然而,这是与结束相似的地方。由于此物件支援实例,接在PERF_COUNTER_DEFINITION结构后则是目前存在物件内之每个实例的PERF_INSTANCE_DEFINITION结构。表7-3对PERF_INSTANCE_DEFINITION结构的成员做了一个简短的说明。

 表7-3 PERF_INSTANCE_DEFINITION结构的成员
成员 说明
ByteLength此结构的位元组长度。为了在内存区块中移动而使用。
ParentObjectTitleIndex例可以是另一个子物件或子实例;例如,线程即是一个子处理程序。如果实例拥有一个父实例,此数字则代表被父物件指定的登录数字(0=no parent)。
ParentObjectInstance如果此实例拥有父实例,则此数字确认了与父实例之索引有关的父物件。
UniqueID一个实例可以被一个数字或一个字串名称所确认。如果您想要使用一个数字,只需设定此成员为该数字并将NameOffset和NameLength设定为0即可。如果您想要使用一个字串名称,则将此成员设定为-1,并适当地设定NameOffset和NameLength成员。
NameOffset从结构开始至此实例的Unicode字串名称之位元组数字。
NameLength此结构的Unicode字串名称的位元组(非字元)数字(为零终止字元增加2位元组)

紧接在PERF_INSTANCD_DEFINITION结构后的固定长度部份是可变长度的部份。这个部份包含了此实例的Unicode字串名称,以零字元终止。如果该名称中包含了一个字元为奇数数字(包括零字元),您必须附加另外2位元组的空白,以使下个PERF_INSTANCE_DEFINITION or PERF_COUNTER_BLOCK结构从32位元边界开始。如果实例名称为一个事件号码,则不需附加空白。

除去效能计数器DLL中的错误
 

如您所见,经DLL的Collect函数所产生的资料区块非常复杂,而且通常会使多数开发者花费许多时间来对它做侦错。为了帮助您除去错误,Microsoft在作业系统中新增了一些对Collect函数行为做分析的支援。如果系统侦测到任何的问题,它将会产生一个在系统之应用程序事件记录档中加入一个项目。图7-11提供了一个范例。


 

 图7-11 一个被回报至应用程序记录档的效能DLL错误

在预设的情形下,系统不会执行此附加的确认动作,因为它会影响到取回效能计数器资料的速度。为了执行这个检查动作,请到下列之登录子机码中做设定。

HKEY_LOCAL_MACHINE/SOFTWARE/Microsoft/Windows NT/CurrentVersion/Perflib

在此机码下,新增一个名称为ExtCounterTestLevel的REG_DWORD值,并设定它为表7-4所列之其中一个值。

 表7-4 指定测试效能资料等级之ExtCounterTestLevel值
说明
1系统应为了一致性而检查所有物件和计数器长度。
2系统应该做缓冲区溢位和分页保护的检查。
3系统不应该执行任何的检查动作。
4不要配置一个保护的分页。如果登录值不存在,这是预设的设定。

每当系统侦测到一个效能资料的问题时,它会写入一个项目到系统的应用程序记录档中。您可以控制此项目的内容:在同一个子机码下建立一个名称为EventLogLevel的REG_DWORD值,并且设定它为表7-5所列的其中一个值。

 表7-5 指定何种事件应被记录至事件记录档中的EventLogLevel值
说明 事件识别码范围
0系统应该永远不回报任何事件到事件记录档中。(不可应用的)
1系统应该在事件记录档中回报使用者相关之事件。1000-1005、 1017-1020
2系统应该在事件记录档中回报警告和错误。1000-1015、 1017-1020、 1016、2000-2003
3系统应该在事件记录档中回报资讯事件以及警告和错误。1000-1015、 1017-1020、 1016、2000-2002、 3000-3001

表7-6显示了可被放置在事件记录中的事件类型。

 表7-6 指示效能资料问题的事件
事件识别码 说明
1000试图从 <calling module name> 对效能资料的存取被 <username> 拒绝。
1001在延伸计数器DLL中由一个收集程序「<dll name>」为「<service name>」服务而返回比可用空间大的缓冲区大小。被计数器DLL回传的效能资料不会被回传至Perf Data Block中。溢位的大小为DWORD 0资料。
1002一个被收集程序在延伸计数器DLL「<dll name>」为了「<service name>」服务而回传的Guard Page。被计数器DLL回传的效能资料不会被回传至Perf Data Block中。
1003一个被延伸计数器DLL「<dll name>」为不正确之「<service name>」服务回传的物件长度。被回传之物件总长度与被回传之缓冲区路之长度不符。被计数器DLL回传的效能资料不会被回传至Perf Data Block中。被回传之物件数为DWORD 0资料。
1004一个被延伸计数器DLL「<dll name>」为不正确的「<service name>」服务所回传之物件实例长度。加上物件定义结构的实例总长度与物件大小不符。被计数器DLL回传的效能资料不会被回传至Perf Data Block中。不良物件之物件标题索引是DWORD 0资料。
1005无法在DLL「<dll name>」中为「<service name>」服务配置「<open proc name>」开启程序。为了此服务而回传的效能资料无法使用。Error Status为DWORD 0资料。
1006无法在DLL「<dll name>」中为了「<service name>」服务而配置「<collect proc name>」之收集程序。为了此服务而回传的效能资料无法使用。Error Status为DWORD 0资料。
1007无法在DLL「<dll name>」中为了「<service name>」服务而配置「<close proc name>」之关闭程序。为了此服务而回传的效能资料无法使用。Error Status为DWORD 0资料。
1008在DLL「<dll name>」中执行开启「<service name>」服务的程序失败。此服务的效能资料无法使用。被回传的Status控制码为DWORD 0。
1009在DLL「<dll name>」中执行开启「<service name>」服务程序时,产生一个例外。此服务的效能资料无法使用。被回传的Exception控制码为DWORD 0。
1010在DLL「<dll name>」中执行「<service name>」服务的收集程序时,产生一个例外或回传一个无效的状态。被计数器DLL回传的效能资讯不会回传至Perf Data Block中。被回传的例外或状态控制码为DWORD 0。
1011无法开启程序库「<dll name>」中指定的「<service name>」服务。此服务的效能资料无法使用。被回传的Status控制码为DWORD 0资料。
1012被系统回报的一个闲置处理程序时间小于最后被回报的时间。该资料显示目前时间和最后被系统回报之闲置处理程序时间。
1013在延伸计数器DLL「<dll name>」为了「<service name>」而被回传一个大于可使用空间之缓冲区,而且可能有应用程序堆积错误的收集程序。除非问题已被修正成可预防未来产生的错误,否则此DLL应该被停用或从系统移除。存取此效能资料的应用程序应该被重新启动。被计数器DLL回传的效能资料不会被回传到Perf Data Block中。溢位大小为DWORD 0资料。
1014尝试从Server Object收集资料时产生一个错误。被函数回传的Error控制码为DWORD 0。在IO Status Block中回传的Status是DWORD 1。IO Status Block之资讯栏为DWORD 2。
1015在「<dll name>」程序库中为了等待收集函数「<function name>」效能资料完成而产生逾时的情形。这也许是试图呼叫此延伸计数器或正在收集资料的服务时,系统正处于忙碌的状态。
1016在「<dll name>」程序库中为了「<service name>」服务而建立的资料缓冲区没有对齐8位元组边界。这可能会导致为应用程序而尝试读取效能资料缓冲区时产生问题。向这个程序库或服务的制造者询问此问题的修正问题或取得此程序库的更新版本。
1017收集由于一或多个因为「<service name>」服务产生错误之效能计数器而停用的效能计数器资料。这些错误已被强迫写至应用程序事件记录档。这些错误应在效能计数器再次使此服务致能前即被修正。
1018收集由于一或多个因为「<service name>」服务产生错误之会议阶段而被效能计数器程序库停用的效能计数器资料。这些错误已被强迫写至应用程序事件记录档中。
1019在延伸计数器DLL「<dll name>」中因为「<service name>」服务不正确而回传的一个物件之定义栏位。在物件定义结构中的定义区块总长度与物件定义标头中指定的不符。被此计数器DLL回传的效能资料不会被回传至Perf Data Block中。不良物件之标题索引为DWORD 0资料。
1020被使用的缓冲区大于「<dll name>」之延伸DLL收集函数为了「<service name>」服务而传递的缓冲区大小。在缓冲区中被传递的尺寸为DWORD 0,而被回传的尺寸为DWORD 1。
2000被回传的指标与经由Collect程序为了「<service name>」而在延伸计数器DLL中回传的缓冲区长度不符。Length将会被调整至符合的大小,而效能资料将会显示在Perf Data Block中。被回传的长度为DWORD 0资料,新的长度则为DWORD 1资料。
2001「<service name>」服务没有Performance子机码或该机码无法被开启。没有效能计数器会被此服务收集。Win32错误码在资料中回传。
2002在DLL「<dll name>」中为「<service name>」服务而执行之开启程序比已被建立的等待完成时间长。也许是试图执行此延伸计数器或正在收集资料服务时,系统正处于忙碌状态的原因。
2003效能程序库「<dll name>」中对「<service name>」服务的设定资讯与储存在登录中之被信任的效能程序库资讯不符。在此程序库中的函数不会被视为已信任。
3000在DLL「<dll name>」中「<service name>」服务开启程序被呼叫且回传成功执行。
3001一个被更新的效能计数器程序库文件已被侦测。「<dll name>」之效能计数器已被执行。

您应注意到列在表7-6中的一些事件并没有对于时间的设定。例如,如果DLL的Open函数占用太久以致于无法返回,系统会假设效能计数器DLL产生错误,并产生一个识别码为2002的事件。在登录中设定OpenProcedureWaitTime值,以让系统知道在回报事件前应等待多久的时间。这个值与另外二个值(ExtCounterTestLevel和EventLogLevel)一样,被列在相同登录子机码下。此外,这个值也可让您设定Open函数所需之毫秒数。


说明

这个值只会影响系统应等待事件记录项目产生的时间。设定一个逾时时间并不会迫使系统丢弃Open函数而去做其他事。如果Open函数没有返回,要求效能资料之应用程序会停止执行。


一个单一的登录值被用在所有的Open函数上,每一个效能计数器DLL皆可以设定一个唯一的Collect函数函数逾时值。为了为DLL之Collect函数设定一个时间限制(毫秒),可以建立一个名称为Collect Timeout的REG_DWORD登录值(在该名称之间必须有空白)。这个值必须被放置在以下的子机码之后:

HKEY_LOCAL_MACHINE /SYSTEM/CurrentControlSet/Services/ServiceName/Performance

再者,此子机码的ServiceName选项部份是您的服务或应用程序之执行资料之唯一识别的一部份。如果您的DLL之Collect函数没有在被指定的毫秒时间内返回,此时会产生一个识别码为1015的事件,并被加入系统的应用程序记录档中。

HWInputMon范例应用程序
 

HWInputMon范例应用程序(「07 HWInputMon.exe」),显示在本节最后的列表7-1中,展示一个键之敲击以及滑鼠移动的效能资讯。包括一个简单的DLL(「07 HWInputMonPerfInfo.dll」)用来收集并回传效能资讯,显示在列表7-2中。此范例应用程序的原始程序代码以及DLL存放在随书光碟上的07-HWInputMon以及07-HWInputMonPerfInfo目录中。

HWInputMon建立了一个名为Hardware Input的效能物件并显示了四个计数器: Keystrokes、Keystrokes/sec、Mouse moves和Mouse moves/sec,如图7-12所示。我强烈地建议您详细地了解此范例应用程序。


 

 图7-12 HWInputMon范例应用程序之新增计数器对话方块

CperfData类别
 

为了简单地新增效能物件和计数器到HWInputMon中,我建立了一个C++ 类别,名称为CperfData。CperfData档(PerfData.h)和它的支援档(Optex.h以及RegKey.h)皆存放在随书光碟上。CperfData做了所有冗长及令人厌烦的工作,例如被分享的内存管理、资料结构初始化、建构内存区块、物件实例的新增和移除以及登录的管理。如果您在自己的应用程序或服务中使用CperfData类别,在您的程序代码中增加效能资讯的最困难部分将是决定该显示哪个物件和计数器。

透过参考这个原始码的注解,对于了解详细的CperfData成员函数的部份应该不成问题。然而,为了要使用此类别来新增效能计数器至您应用程序,您不需要了解它内部的运作方式,只需知道如何设定此类别即可。

使用CperfData类别
 

为了建立一个显示效能资讯的服务(或应用程序),您将需要二个专案:一个可执行服务或应用程序之Win32应用程序专案以及一个收集与回传效能资讯的Win32 DLL专案。

一旦建立了这二个专案后,必须建立一个对您想要应用程序显示之特定物件及计数器的定义符号标头档。如HWInputMon范例,此标头档的名称为HWInputPerfDataMap.h,如列表7-3中所示。

您可以用PERFDATA_DEFINE_OBJECT巨集指令码(在PerfData.h中定义)来定义一个物件的符号。此巨集指令码有二个参数:一个您可以在应用程序中用来参考此物件的符号名称,以及一个您定义之识别码。您必须不重覆使用识别码数字,而识别码必须不为0。建立计数器的正确方式与上述的方式相同,只是此时应使用PERFDATA_DEFINE_COUNTER巨集指令码。任何须改变或更新一个效能监视的原始程序代码中必须包含此标头档。

现在您会建立一个指示您的服务或应用程序支援的物件与计数器表格。为了方便说明,我也将此资讯包含在HWInputPerfDataMap.h中。使用宣告在PerfData.h标头档中的巨集指令码可使这个表格的建立变得非常容易。这些巨集指令码会建立一个被我呼叫之效能资料的对映。此对映与MFC程序设计所使用的讯息对映类似。

为了建立一个效能资料对映,可从那些说明一个定义您的物件程计数器的结构阵列之PERFDATA_MAP_BEGIN巨集指令码开始。在此巨集指令码之后为一或多个PERFDATA_MAP_OBJ与PERFDATA_MAP_CTR巨集指令码。注意对映中的项目顺序是很重要的,由它的计数器跟随其后,并依此类推。

为了宣告一个物件,必须使用PERFDATA_MAP_OBJ巨集指令码。此巨集指令码需要7个参数:

  1. 确认物件之程序设计符号。
  2. 物件的Unicode字串名称(它会被新增至登录中)。
  3. 物件的Unicode解说文字(它也会被新增至登录中)。
  4. 物件的细节层次。
  5. 当物件在系统监控程序中被选择时,一个预设应被选择的计数器之程序设计符号名称。
  6. 物件支援之实例的最大数字(如果物件不支援实例,则传递PERF_NO_INSTANCES)。
  7. 可以在实例字串名称中显示的最大字元数(如果物件不支援实例,则传递0)。

在您加入一个物件至对映后,即可使用PERFDATA_MAP_CTR巨集指令码来为物件新增一或多个计数器。此巨集指令码需要6个参数:

  1. 确认计数器之程序设计符号。
  2. 计数器的Unicode字串名称(此名称会被新增至登录中)。
  3. 计数器的Unicode解说文字(也会被加至登录中)。
  4. 计数器的细节层次。
  5. 计数器的预设小数值。
  6. 计数器的类型。

在您将所有的物件和计数器新增至对映后,即可使用PERFDATA_MAP_END巨集指令码来完成对应的设定。此巨集指令码只有一个参数—您的服务或应用程序名称—而且被用来在以下所示之登录机码中建立计数器资讯:

HKEY_LOCAL_MACHINE/SYSTEM/CurrentControlSet/Services/ServiceNamePerformance

这个巨集指令码会终止对映并建立一个CperfData类别的全域实例。此全域实例的名称为g_PerfData,当您想要操作效能物件、实例或计数器时,在您的程序代码中将会需要参考此变数。

注意到效能资料的对映都被它自己放置在一个原始程序代码文件中。因为应用程序和DLL在它们的专案中必须包含此档,所以这是必要的。

让我们将注意力转移到一些说明如何正确地使用CperfData类别的公用成员函数上。先看位于HWInputMon.cpp中的_tWinMain函数(请参阅 

要适当的为应用程序显示效能资讯,登录必须依据本章前面所提的方式而设定。

CperfData有一个静态成员函数,名称为Install:

void CPerfData::Install(PCWSTR pszDllPathname);

必须传递显示您的效能资讯之DLL路径名称给这个函数。在HWInputMon范例应用程序中,如果使用者在Install Performance Counter Data Into Registry讯息方块中按下了Yes按钮,那么_tWinMain便会呼叫此函数。为了决定DLL的完整路径名称,我用可执行档的完整路径名称并用DLL的文件名称来取代可执行档的名称—当然,假设可执行档和DLL档必须放在相同的目录中。

要从系统的登录中移除效能计数器资讯,必须呼叫CperfData的Uninstal函数:

void CPerfData::Uninstall();

在_tWinMain中,如果使用者在Remove Performance CounterData From The Registry讯息方块中按下Yes按钮,便呼叫此函数。当您正在对计数器做侦错的动作时,您也许会想使您的应用程序在关机期间在启动和移除功能表上安装这个登记资讯。使用此方法,如果您决定新增、删除或者移动任何在效能资料对映中的项目,登录将不会同时将它剔除。

一旦登录完成设定以后,会呼叫CperfData类别的Activate方法以告诉CPerfData物件可以开始记录效能计数器的资讯:

DWORD CPerfData::Activate();

这个函数配置了被分享的内存区块并且将包含在效能资料对映中的资讯初始化。一个应用程序应该只有在效能资讯已被安装后才呼叫此函数。


说明

在CperfData类别内部使用了一个内存对应档之核心物件来实作此分享内存区块。内存对映档将服务的位址空间对映至也将被应用程序之位址空间要求的位址空间(例如,MMC.exe或WinLogon)。因为这两个处理程序在不同的安全性内容下可以执行得很好,所以必须采取此步骤,以使他们能够与使用此核心物件彼此通讯。

为了使这两个处理程序可以互相通讯,CperfData使用一个安全性描述子来建立它的内部核心物件,以允许Everyone的GENERIC_ALL存取权限。依据您的需求,您可能会想改变此描述子,但是我认为大部份的开发者会觉得它已足够。


一旦效能资料已被启动,_tWinMain便会为它的其中一个物件新增实例。只要呼叫AddInstance即可新增实例:

INSTID CPerfData::AddInstance(BOOL fIgnoreIfExists, OBJID ObjId, 	PCTSTR pszInstName, OBJID ObjIdParent = 0, INSTID InstIdParent = 0);

fIgnoreIfExists参数会告诉函数如果一个指定的实例名称已经存在时,是否还要新增此实例。ObjId参数是确认取得新的实例之物件的程序设计符号。pszInstName参数是实例的字串名称。最后二个参数允许您指示哪一个实例是另一个物件实例的子实例。多数的实例没有父实例,所以您通常只会传递前叁个参数给此函数。如果函数执行成功,它会回传一个INSTID。这是我自己的资料型别,它是对处理新建立实例的handle。如果此函数执行失败,会回传 .1。

CperfData类别有另一个AddInstance版本:

INSTID CPerfData::AddInstance(BOOL fIgnoreIfExists, OBJID ObjId, 	LONG lUniqueId, OBJID ObjIdParent = 0, INSTID InstIdParent = 0);

此版本与第一个是相同,除了它允许您使用唯一的识别码而非字串来识别实例外。

因为实例就和您的应用程序在执行时一样,可以有许多的变化,所以您应该在任何时间随意的新增新的实例。您也可以呼叫RemoveInstance以移除实例:

void CPerfData::RemoveInstance(OBJID ObjId, INSTID InstId);

现在我们取得了一个有趣的教材—改变一个计数器的值。有二个存在的函数允许您去改变一个计数器的值:

LONG&	CPerfData::GetCtr32(CTRID CtrId, int nInstId = 0) const; __int64&	CPerfData::GetCtr64(CTRID CtrId, int nInstId = 0) const;

如果您想修改的计数器值有32位元,请呼叫GetCtr32;如果计数器值为64位元,则呼叫GetCtr64。在侦错阶段,如果您执行了一个不适当的呼叫,原始码会产生一个判断提示(Assertion)。对这二个函数,您必须为您定义的计数器传递程序设计符号。如果此计数器存在一个不支援实例的物件中,可以忽略第二个参数。如果此计数器被包含在一个支援实例的物件中,您就必须传递INSTID(被AddInstance回传)给第二个参数。

这些函数不是回传一个LONG就是 __int64的参考值,用来确认在被分享的内存区块中的计数器值。

用此参考值改变一个计数器值是一件微不足道的事。以下是一个范例:

LONG& lCounterValue =	 g_PerfData.GetCtr32(SOME_COUNTER_SYMBOL); lCounterValue = 5;		// 建立计数器值为5lCounterValue++;			// 计数器值增加1lCounterValue *= 13;	// 将计数器值乘13

有更简单的方法!如果您想要改变计数器的值,只要在应用程序的原始程序代码中加入刚刚所提的那些程序代码即可。这几行程序代码可以非常快的执行,而且应该不会影响到您的应用程序之执行效能。

HWInputMon.cpp/********************************************************************模组:HWInputMon.cpp通告:Copyright (c)2000 Jeffrey Richter********************************************************************/#include "../CmnHdr.h"	// 请参阅附录A#include <WindowsX.h>#define HWINPUTPERFDATAMAP_IMPL#include "HWInputPerfDataMap.h"///LRESULT CALLBACK LowLevelKeyboardProc(int nCode,	WPARAM wParam, LPARAM lParam){	if (nCode == HC_ACTION){		switch (wParam){		case WM_KEYDOWN:	case WM_SYSKEYDOWN:		case WM_KEYUP:	case WM_SYSKEYUP:			g_PerfData.GetCtr32(HWINPUT_KEYS)++;			g_PerfData.GetCtr32(HWINPUT_KEYSPERSEC)++;			break;		}	}	return(CallNextHookEx(NULL, nCode, wParam, lParam));}///typedef enum {	mciFirst = 0,	mciTotal = mciFirst,	mciLeft,	mciMiddle,	mciRight,	mciLast = mciRight}MOUSECLCKINST;CPerfData::INSTID g_MouseClckInstToPrfInstId [mciLast + 1];///LRESULT CALLBACK LowLevelMouseProc(int nCode, WPARAM wParam, LPARAM lParam){	if (nCode == HC_ACTION){		if (wParam == WM_MOUSEMOVE){			g_PerfData.GetCtr32(HWINPUT_MOUSEMOVES)++;			g_PerfData.GetCtr32(HWINPUT_MOUSEMOVESPERSEC)++;		}		BOOL fDown = ((wParam == WM_LBUTTONDOWN) ||			(wParam == WM_MBUTTONDOWN) || (wParam == WM_RBUTTONDOWN));		if (fDown){			MOUSECLCKINST mci = mciLeft;		if ((wParam == WM_LBUTTONDOWN) || (wParam == WM_LBUTTONUP))			mci = mciLeft;		if ((wParam == WM_MBUTTONDOWN) || (wParam == WM_MBUTTONUP))			mci = mciMiddle;		if ((wParam == WM_RBUTTONDOWN) || (wParam == WM_RBUTTONUP))			mci = mciRight;		g_PerfData.GetCtr32(MOUSECLCKS_CLICKS,			g_MouseClckInstToPrfInstId[mciTotal])++;		g_PerfData.GetCtr32(MOUSECLCKS_CLICKSPERSEC,			g_MouseClckInstToPrfInstId[mciTotal])++;		g_PerfData.GetCtr32(MOUSECLCKS_CLICKS,			g_MouseClckInstToPrfInstId[mci])++;		g_PerfData.GetCtr32(MOUSECLCKS_CLICKSPERSEC,			g_MouseClckInstToPrfInstId[mci])++;		}	}	return(CallNextHookEx(NULL, nCode, wParam, lParam));}///int WINAPI _tWinMain(HINSTANCE hinstExe, HINSTANCE, PTSTR pszCmdLine, int){	static TCHAR szAppName[] == TEXT("Hardware Input Monitor");	if	(MessageBox(NULL,		TEXT("Install Performance Counter Data into Registry?"),		szAppName, MB_YESNO) == IDYES){		TCHAR szPath[_MAX_PATH];		GetModuleFileName(hinstExe, szPath, chDIMOF(szPath));		lstrcpy(_tcsrchr(szPath, TEXT('//')) + 1,			TEXT("07 HWInputMonPerfInfo.dll"));		g_PerfData.Install(szPath);	}	if (MessageBox(NULL,		TEXT("Collect Performance Counter Data?"),		szAppName, MB_YESNO) == IDYES){		g_PerfData.Activate();		// 加入四个Mouse Click Object Instances		g_MouseClckInstToPrfInstId[mciTotal] ==			g_PerfData.AddInstance(TRUE, PERFOBJ_MOUSECLCKS, TEXT("_Total"));		g_MouseClckInstToPrfInstId[mciLeft] ==			g_PerfData.AddInstance(TRUE, PERFOBJ_MOUSECLCKS, TEXT("Left"));		g_MouseClckInstToPrfInstId[mciMiddle] ==			g_PerfData.AddInstance(TRUE, PERFOBJ_MOUSECLCKS, TEXT("Middle"));		g_MouseClckInstToPrfInstId[mciRight] ==			g_PerfData.AddInstance(TRUE, PERFOBJ_MOUSECLCKS, TEXT("Right"));		// 安装低阶键盘和滑鼠的拦截程序(Hook)		HHOOK hhkLowLevelKybd = SetWindowsHookEx(WH_KEYBOARD_LL,			LowLevelKeyboardProc, hinstExe, 0);		HHOOK hhkLowLevelMouse = SetWindowsHookEx(WH_MOUSE_LL,			LowLevelMouseProc, hinstExe, 0);		// 保持此应用程序为执行状态,直到我们通知它停止为止		int x = IDRETRY;		while (x == IDRETRY){			if (x == IDRETRY){				// 重新设定所有计数器为0				g_PerfData.LockCtrs();				g_PerfData.GetCtr32(HWINPUT_KEYS) = 0;				g_PerfData.GetCtr32(HWINPUT_KEYSPERSEC) = 0;				g_PerfData.GetCtr32(HWINPUT_MOUSEMOVES) = 0;				g_PerfData.GetCtr32(HWINPUT_MOUSEMOVESPERSEC) = 0;				MOUSECLCKINST mci = mciFirst;				while (mci <= mciLast){					g_PerfData.GetCtr32(MOUSECLCKS_CLICKS,						g_MouseClckInstToPrfInstId[mci]) = 0;					g_PerfData.GetCtr32(MOUSECLCKS_CLICKSPERSEC,						g_MouseClckInstToPrfInstId[mci]) = 0;					mci = (MOUSECLCKINST) ((int) mci + 1);				}				g_PerfData.UnlockCtrs();			}			x = MessageBox(NULL,				TEXT("Click /"Retry/" to reset the counters../n")				TEXT("Click /"Cancel/" to terminate the application."),				szAppName, MB_RETRYCANCEL);		}		UnhookWindowsHookEx(hhkLowLevelKybd);		UnhookWindowsHookEx(hhkLowLevelMouse);		// 移除四个Mouse Click Object Instances		g_PerfData.RemoveInstance(PERFOBJ_MOUSECLCKS,			g_MouseClckInstToPrfInstId[mciTotal]);		g_PerfData.RemoveInstance(PERFOBJ_MOUSECLCKS,			g_MouseClckInstToPrfInstId[mciLeft]);		g_PerfData.RemoveInstance(PERFOBJ_MOUSECLCKS,			g_MouseClckInstToPrfInstId[mciMiddle]);		g_PerfData.RemoveInstance(PERFOBJ_MOUSECLCKS,			g_MouseClckInstToPrfInstId[mciRight]);	}	if (MessageBox(NULL,		TEXT("Remove Performance Counter Data from the Registry?"),		szAppName, MB_YESNO) == IDYES){		g_PerfData.Uninstall();	}	return(0);}/End Of File /
 列表7-1 HWInputMon范例应用程序
HWInputMonPerfInfo.cpp/********************************************************************模组:HWInputMonPerfInfo.cpp通告:Copyright (c)2000 Jeffrey Richter说明:显示HWInputMon之效能资讯的DLL********************************************************************/#include "../CmnHdr.h"	// 请参阅附录A#define PERFDATA_COLLECT_SUPPORTED// 注:PERFDATA_COLLECT_SUPPORTED必须被定义在此专案中#if !defined(PERFDATA_COLLECT_SUPPORTED)#error PERFDATA_COLLECT_SUPPORTED must be defined for this project#endif#define HWINPUTPERFDATAMAP_IMPL#include "../07-HWInputMon /HWInputPerfDataMap.h"/End Of File /
 列表7-2 HWInputMonPerfInfo DLL
HWInputPerfDataMap.h/********************************************************************模组:HWInputPerfDataMap.h通告:Copyright (c)2000 Jeffrey Richter说明:效能物件和计数器的定义********************************************************************/#ifdef HWINPUTPERFDATAMAP_IMPL#define PERFDATA_IMPL#endif#include "PerfData.h"///PERFDATA_DEFINE_OBJECT(PERFOBJ_HWINPUT,100);PERFDATA_DEFINE_COUNTER(HWINPUT_KEYS,101);PERFDATA_DEFINE_COUNTER(HWINPUT_KEYSPERSEC,102);PERFDATA_DEFINE_COUNTER(HWINPUT_MOUSEMOVES,103);PERFDATA_DEFINE_COUNTER(HWINPUT_MOUSEMOVESPERSEC,104);PERFDATA_DEFINE_OBJECT(PERFOBJ_MOUSECLCKS,200);PERFDATA_DEFINE_COUNTER(MOUSECLCKS_CLICKS,201);PERFDATA_DEFINE_COUNTER(MOUSECLCKS_CLICKSPERSEC,202);///#ifdef HWINPUTPERFDATAMAP_IMPL///PERFDATA_MAP_BEGIN()	PERFDATA_MAP_OBJ(PERFOBJ_HWINPUT, TEXT("Hardware Input"),		TEXT("The Hardware Input object type includes those counters")		TEXT("that apply to keystrokes and mouse moves."),		PERF_DETAIL_NOVICE, HWINPUT_KEYS, PERF_NO_INSTANCES, 0)	PERFDATA_MAP_CTR(HWINPUT_KEYS, TEXT("Keystrokes"), TEXT("The number 	of down and up keystrokes"), PERF_DETAIL_NOVICE, 0, PERF_COUNTER_RAWCOUNT)	PERFDATA_MAP_CTR(HWINPUT_KEYSPERSEC, TEXT("Keystrokes/sec"),		TEXT("The number of down and up keystrokes per second"),		PERF_DETAIL_NOVICE, 0, PERF_COUNTER_COUNTER)	PERFDATA_MAP_CTR(HWINPUT_MOUSEMOVES, TEXT("Mouse moves"),		TEXT("The number of mouse moves"),		PERF_DETAIL_NOVICE, 0, PERF_COUNTER_RAWCOUNT)	PERFDATA_MAP_CTR(HWINPUT_MOUSEMOVESPERSEC, TEXT("Mouse moves/sec"),		TEXT("The number of mouse moves per second"),		PERF_DETAIL_NOVICE, 0, PERF_COUNTER_COUNTER)	PERFDATA_MAP_OBJ(PERFOBJ_MOUSECLCKS, TEXT("Mouse Clicks"),		TEXT("The Mouse Clicks object type includes those counters")		TEXT("that apply to mouse button clicks."),		PERF_DETAIL_NOVICE, MOUSECLCKS_CLICKS, 4, 10)	PERFDATA_MAP_CTR(MOUSECLCKS_CLICKS,TEXT("Clicks "),		TEXT("The number of down clicks"),		PERF_DETAIL_NOVICE, 0, PERF_COUNTER_RAWCOUNT)	PERFDATA_MAP_CTR(MOUSECLCKS_CLICKSPERSEC, TEXT("Clicks/sec"),		TEXT("The number of down clicks per second"),		PERF_DETAIL_NOVICE,0,PERF_COUNTER_COUNTER)	PERFDATA_MAP_END("HWInputMon")///#endif //HWINPUTPERFDATAMAP_IMPL/End Of File /
 列表7-3 HWInputMon之资料对映

同步存取计数器值
 

同步存取计数器值是每一个程序设计师需要严肃地去面对的一个议题。要「正确地」实作计数器,您应该在一个重要的部分或某些类似的事物中包装每个对计数器值的修正。然而,要求进去和离开一个关键部分的CPU时间通常会比CPU时间要求只改变简单的32位元或64位元值要高得多。

因为同步存取对您的应用程序之执行效能有重大的影响。我曾跟负责Windows之执行效能的Microsoft开发者提过这一点。他告诉我大部份的系统计数器不会同步存取计数器值。当然这减少了同步处理计数器值的成本,但是却意味着该值有被讹用的可能性。然而,计数器值误用的情形很不容易产生,所以开发小组认为速度所带来的利益远远超过不精确资讯的可能性。是的,系统监控程序可以显示一个被统计资讯丢弃之不正确值,但是这个可能性非常小。

在建立及测试效能物件和计数器的经验来看,我同意Windows小组的看法。那就是,适当地对计数器值做同步存取的动作以避免产生一个不正确值的可能性。然而,当在设计C++ 类别时,它必须能让您为自己建立这个选择权的能力,所以C++ 类别提供了叁个公共的函数,如果您觉得适合的话,它允许您锁定计数器或解开锁定:

void CPerfData::LockCtrs()const; BOOL CPerfData::TryLockCtrs()const;void CPerfData::UnlockCtrs()const;

多数的应用程序不会使用到这些功能,您还是在CperfData类别中实作了这些函数的呼叫。例如,当实作的Collect函数被呼叫时,它会随时锁定计数器资讯并解开锁定。这么做是必要的,因为Collect函数有许多工作要做,而当它正在收集资料时,在比对另一个将被执行指令之附加成本是无足轻重的。

另外,当在新增或移除物件实例时,我也锁定了被分享的内存区块。这可以防止资料架构发生误用以及程序代码毁坏的情形。HWInputMon.cpp模组也说明了从_tWinMain函数内部使用这些函数的方法。

请注意这个重要的情形:由于在不同处理程序中执行的线程会存取被分享的记忆区块,所以将这些线程同步的一个简单的方式即是使用一个mutex核心物件。然而,等待一个核心物件伴随的即是一个对重要的执行效能之击中。关键的部分必须负担更好的效能,但是非常遗憾的是,它只能在一个单一处理程序中用来对线程做同步的处理。因为我在若干处理程序中需要非常快速的从线程到分享内存缓冲区存取之专用通路,所以我决定用我的Coptex类别来同步存取分享内存缓冲区,请参阅《Programming Applications for Microsoft Windows, Fourth Edition》(Jeffreyb Richter, Microsoft Press, 1999)之内容以取得此类别的描述。

为了解有关这个CperfData类别的使用方法,请查阅随附的原始程序代码以取得更多的资讯,以察看设定此专案以及工作区的方法。

可惜的是,此说明文字并没有任何意义。我曾指出此「错误」给Microsoft,但是他们决定不修正它,因为Windows 2000已经快要发行了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值