◆パフォーマンスモニタ returns 〜 カスタムパフォーマンスモニタの作成 その2

[ ホーム ] [ 上へ ] [ パフォーマンスモニタ returns 〜 カスタムパフォーマンスモニタの作成 その3 ]

<2.パフォーマンスDLLの作成>

レジストリにモニタ対象のアプリケーションをモニタする情報を登録して、 コントロールパネル→管理ツール→パフォーマンスから表示されるパフォーマンスデータ収集アプリを 起動すると、パフォーマンスモニタシステムは、レジストリ情報に従ってパフォーマンスDLL をロードして、その仕様に沿ったコールバック関数を呼ぶことによって目的のデータを収集しようとします。 このコールバック関数を実装することが今回のテーマの中心になります。
システムが呼び出す関数なのでそのシグネチャは決まっていて何を実装するかも指針が示されています。 そのコールバック関数は3つあり、次のような機能を実装します。

(2a.Open関数)

パフォーマンスデータ収集アプリがRegQueryValueExを呼び出すと パフォーマンスモニタシステムはレジストリ情報を元にDLLをロードします。ただし、別のアプリが RegQueryValueExを呼んでいたらDLLはロードされています。 DLLのロード後、 1つのスレッドが最初に一度だけ呼ばれる関数がOpen関数です。プロトタイプは

DWORD WINAPI OpenPerfData(LPWSTR  lpDeviceNames);

です。

引数 lpDeviceNames レジストリのHKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\application_name\Linkage のExportエントリに格納されている文字列が渡されています。
戻値 初期化等の必要処理が正常に行われた場合は、ERROR_SUCCESSを返却するようにします。 エラーなどが発生して、以降のパフォーマンスデータ収集ができない場合は ERROR_SUCCESS以外の値を返してイベントログに記録します。この場合、システムから 以降のCollect関数、Close関数は呼ばれません。

(2b.Collect関数)

Open関数で初期化処理が正常に終了してERROR_SUCCESSが返されると、 システムは続いてCollect関数を呼び出します。プロトタイプは次のとおりです。

DWORD APIENTRY CollectPerfData(IN     LPWSTR  lpwszValue,
                               IN OUT LPVOID  *lppData,
                               IN OUT LPDWORD lpcbTotalBytes,
                               IN OUT LPDWORD lpNumObjectTypes);
引数 lpwszValue RegQueryValueExの第二引数で指定される収集しようとしているパフォーマンスオブジェクトの リストが収められている文字列(ex. "Global", "Foreign xxx", "Costly", "344 132")
lppData PERF_OBJECT_TYPEを先頭とした、収集したデータを含む返却すべきデータブロックの先頭位置への ポインタがシステムより提供されます.正常にデータ収集ができて、*lppDataにデータを返却できたら、 次のオブジェクトがパフォーマンスデータを返却できるように、データを格納した最終位置 (次のオブジェクトがデータを格納する最初の位置)に*lppDataの位置を移動しなければなりません。
lpcbTotalBytes Collect関数でオブジェクト/カウンタの返却データに必要だと思われる総バイト数をシステムが 推定してここの引数に提供されます。*lppDataに正常にデータが書込みできたら、 格納したバイト単位のサイズを返却します。
lpNumObjectTypes 正常にデータを取得して返却できた時、システムに返却したカウンタオブジェクトの 種類数を設定します。
戻値 システムが推定して*lpcbTotalBytesに渡した返却用メモリ領域よりも、実際に返却するメモリ領域の ほうが大きい場合、つまり、結果が収まらないときはERROR_MORE_DATAを返します。
それ以外、正常にデータ収集できた場合やそもそもこのカウンタオブジェクトに関連する カウンタデータを収集する必要が無い時、あるいはエラー時の場合などERROR_MORE_DATAを返す以外は すべてERROR_SUCCESSを返します。

先頭がPERF_DATA_BLOCKでないのはこのDLLのCollect関数では、 あたなた作ったパフォーマンスデータだけをシステムは必要としていて、最終的に RegQueryValueExの第二引数に指定した範囲のカウンタデータを取得したシステムがデータを まとめてPERF_DATA_BLOCKを先頭にして呼び出し元に返却します。

データ収集の手順は次のようになります。

  1. 既に呼ばれたOpen関数が正常に行われたかの確認。
  2. lpwszValueの値で"Global"やこのDLLが扱うカウンタオブジェクトのインデックスが 指定されているか確認します。インデックスはOpen関数で 割り振られていなければなりません
    もし、このDLLが扱うカウンタが対象外でしたら 先ほどの終了処理を行います。
  3. モニタ対象アプリと通信するなどして、現在の作成すべきカウンタのインスタンス数を 算出してPERF_OBJECT_TYPE〜最後のインスタンスの最後のカウンタデータ値を収めるのに 必要なバイト数を計算します。その値が*lpcbTotalBytesより大きければ 先ほどの終了処理を行います。
  4. プロセス間通信によってモニタ対象のアプリからデータを 取得して、左図のデータ構造に沿うようにデータを作成していきます。つまり、 PERF_OBJECT_TYPEなどの各メンバの値とPERF_COUNTER_BLOCK以下のカウンタデータを *lppDataに適切な順序でコピーしていきます。

その際注意しなければならない点は、Collect関数内のローカル変数にデータをいったん 蓄えて*lppDataにコピーする場合です。前にも書きましたが、複数のスレッドがこのDLLに アクセスする可能性があるので、AスレッドがカウンタデータをCollect関数内のローカル変数xに 取得したとします。xを*lppDataにコピーするまでにスレッドBがCollect関数を呼び出して変数xを 初期化してしまったら、Aスレッドの収集データがおかしな値になります。こんなことを避けるために 収集データは直接*lppDataにコピーするのが吉かもしれません。

  1. モニタ対象のアプリケーションからデータを収集する際にはちゃんと同期をとるなどの処置が 必要ですが、今回のパフォーマンスモニタというテーマとは直接関係ないので プロセス間通信でデータ取得方法を確実に理解してください。
  2. 返却すべきデータをコピーしたら、そのトータルバイト数をPERF_OBJECT_TYPEの TotalByteLengthメンバと、lpcbTotalBytes引数に設定してください。このとき、 このバイト数は8の倍数でなければなりません。パフォーマンスデータはLONGLONG型の データが存在していてシステムは8バイトずつ読み取るようです。
    winperf.hを見るとわかりますが、PERF_OBJECT_TYPE、PERF_COUNTER_DEFINITION、 PERF_INSTANCE_DEFINITiONのサイズは8の倍数なので、この構造体のメンバの値を操作している分には 何も気にする必要はないですが、インスタンス名を格納する領域や、PERF_COUNTER_BLOCKの 先頭から最後のカウンタデータまでのサイズが8の倍数になるように気をつける必要があります。
  3. Collect関数でカウンタのインスタンス数も算出されているので*lpNumObjectTypesに設定します。
  4. もし、何らかのエラーが発生してもERROR_SUCCESSで関数を終了して、エラー内容は唯一の 方法であるイベントログを使うようにします。

(2c. Close関数)

パフォーマンスデータ収集アプリが終了してRegCloseKeyを発行するとスレッドごとに 最後に一度だけClose関数が呼ばれます。プロトタイプは次のとおりです。

DWORD WINAPI ClosePerfData();
戻値 すべてERROR_SUCCESS

<3.モニタリング対象アプリケーションからのデータ取得のための実装 −プロセス間通信

後で記述する例のモニタ対象アプリケーション側では、共有メモリを作成して そこにデータをコピーをします。パフォーマンスDLL側のCollect関数でこの共有メモリから データを取得する形でプロセス間通信を行っています。プロセス間通信にはいろいろな方法がありますが、 ここでは趣旨が違うのでこれ以上書きません。 例の共有メモリを使う方法については こちらで確認してください。

<4.イベントログの実装>

DLLの実装の際、エラー報告は先に書いたようにイベントログしかないので イベントビューア等で状況を把握できるようにイベントログを記録できるようにしなければなりませんが、 これも、3.のプロセス間通信と同様今回の趣旨と異なるので コードの掲載と簡単な説明に留めておきます。また、機会があれば書きたいと思います。

カスタムパフォーマンスモニタの実装でやるべきことの大筋は上記のような感じなので、 次から実践してみます。