Windows の限界に挑む: ハンドル

翻訳元: Pushing the Limits of Windows: Handles (英語)

この記事は、「Windows の限界に挑む」シリーズの 5 回目の投稿です。この一連の記事では、物理メモリ、仮想メモリ、プロセス、スレッドなど、Windows で管理できるリソースの数量とサイズの上限について扱ってきました。

今回は、ハンドルの実装の限界を探り、解説します。ハンドルは、アプリケーションが操作する基本的なオペレーティング システムのオブジェクト (ファイル、レジストリ キー、同期プリミティブ、共有メモリなど) の開かれているインスタンスを表すデータ構造です。1 つのプロセスが作成できるハンドルの数については、2 つの制限があります。システムで設定されているプロセスごとのハンドルの最大数、およびハンドルとアプリケーションがハンドルを使って参照しているオブジェクトを格納するために使用可能なメモリの容量です。

多くの場合、ハンドルに対する制限は、標準的なアプリケーションやシステムが使用する数よりもずっと大きい値です。ただし、この制限を考慮せずに設計されているアプリケーションでは、開発者が予期しない方法で制限を超える場合があります。これらのリソースの有効期間をアプリケーション側で管理しなけばならないことは、より一般的な問題を引き起こします。仮想メモリの場合と同様に、リソースの有効期間の管理は優れた開発者にとっても大きな課題です。アプリケーションが不要になったリソースを解放しない場合、リソースのリークが発生し、最終的には制限に達して、そのアプリケーションや他のアプリケーション、またはシステム全般の動作が異常になったり、動作の診断が難しくなります。

以前の投稿で、この投稿で言及している概念の一部 (ページ プールなど) について説明しているので、併せてお読みになることをお勧めします。

ハンドルとオブジェクト

Windows のカーネル モード コアは、%SystemRoot%\System32\Ntoskrnl.exe イメージに実装されており、メモリ マネージャー、プロセス マネージャー、I/O マネージャー、構成マネージャー (レジストリ) などのさまざまなサブシステムで構成されています。これらのサブシステムはすべてエグゼクティブに含まれます。これらの各サブシステムは、オブジェクト マネージャーを使用して、アプリケーションに公開するリソースを表す 1 つまたは複数の型を定義します。たとえば、構成マネージャーは、開いているキーを表す key オブジェクトを定義します。メモリ マネージャーは、共有メモリを表す Section オブジェクトを定義します。エグゼクティブは、SemaphoreMutant (ミューテックスの内部名)、および Event 同期オブジェクト (これらのオブジェクトはオペレーティング システムのカーネル サブシステムで定義される基本的なデータ構造をラッピングします) を定義します。I/O マネージャーは、デバイス ドライバー リソース (ファイル システムのファイルを含む) の開いているインスタンスを表す File オブジェクトを定義します。プロセス マネージャーは、前回の「Windows の限界に挑む」の投稿で説明した Thread オブジェクトと Process オブジェクトを作成します。Windows ではリリースのたびに新しい種類のオブジェクトが導入され、Windows 7 では合計 42 種類が定義されています。定義されているオブジェクトを確認するには、管理者権限で Sysinternals Winobj (英語) ユーティリティを実行し、オブジェクト マネージャーの名前空間の ObjectTypes ディレクトリに移動します。

図 1

図 1

アプリケーションでこれらのリソースのいずれかを管理する場合は、最初に適切な API を呼び出して、リソースを作成するか、開く必要があります。たとえば、CreateFile (英語) 関数はファイルを開くか作成し、RegOpenKeyEx (英語) 関数はレジストリ キーを開き、CreateSemaphoreEx (英語) 関数はセマフォを開くか作成します。これらの関数が正常に実行されると、Windows によって、アプリケーションのプロセスのハンドル テーブルハンドルが割り当てられ、ハンドル値が返されます。この値はアプリケーションでは非透過値として処理されますが、実際には返されたハンドルのハンドル テーブル内でのインデックスです。

ハンドルが返されると、アプリケーションはそのハンドル値を以降の API 関数 (ReadFile (英語)SetEvent (英語)SetThreadPriority (英語)MapViewOfFile (英語) など) に渡すことによって、オブジェクトを照会または操作します。システムは、ハンドルが参照するオブジェクトを検索するために、ハンドル テーブル内のインデックスを調べて、対応するハンドル エントリ (オブジェクトへのポインターが格納されている) を特定します。ハンドル エントリには、プロセスがオブジェクトを開いたときにプロセスに付与されたアクセス権も格納され、これによってシステムは、アクセス許可が要求されていないオブジェクトに対して、プロセスが操作を実行することを許可しないようにすることができます。たとえば、プロセスが読み取りアクセス権で正常にファイルを開いた場合、ハンドル エントリは次のようになります。

図 2

図 2

このプロセスがこのファイルに書き込みを実行しようとすると、アクセス権が付与されておらず、キャッシュされた読み取りアクセス権は、システムがより高いレベルのアクセス チェックを再び実行しないことを意味しているため、この関数は失敗します。

ハンドルの最大数

最初の制限は、Windows の限界を調べるこのシリーズでずっと使用してきた Testlimit ツールで調べることができます。このツールは、こちら (英語) の『Windows Internals』についてのページからダウンロードできます。プロセスが作成できるハンドルの数をテストするために、Testlimit には、できるだけ多くのハンドルを作成することをプロセスに指示する -h スイッチが実装されています。このスイッチは、CreateEvent (英語) でイベント オブジェクトを作成した後、DuplicateHandle (英語) を使用して、システムから返されたハンドルを繰り返し複製します。Testlimit では、ハンドルを複製することによって新しいイベントの作成を回避しており、Testlimit が消費するリソースはハンドル テーブルのエントリ用のリソースのみです。64 ビット システムで、-h オプションを指定して Testlimit を実行した結果を次に示します。

図 3

図 3

ただし、この結果はプロセスが作成できるハンドルの総数を表しているわけではありません。プロセスの初期化中に、システム DLL によりさまざまなオブジェクトが開かれるからです。プロセスのハンドルの総数を確認するには、タスク マネージャーまたは Process Explorer にハンドル数の列を追加します。この場合、Testlimit の総数は 16,711,680 と表示されています。

図 4

図 4

32 ビット システムで Testlimit を実行した場合、作成できるハンドル数は若干異なります。

図 5

図 5 [拡大図]

ハンドルの総数も 16,744,448 と異なります。

図 6

図 6

この違いはどこから来るのでしょうか。その答えは、ハンドル テーブルの管理を担当するエグゼクティブが、プロセスごとのハンドルの制限、およびハンドル テーブルのエントリのサイズを設定する方法にあります。まれなケースですが、Windows がリソースの上限をハードコーディングで設定している場合、エグゼクティブは、プロセスが割り当てることができる最大ハンドル数として 16,777,216 (16*1024*1024) を定義します。特定の時点で 1 万個を超えるハンドルを開いているプロセスは設計がよくないか、ハンドル リークが発生しているかのいずれかであるため、1,600 万個という制限は基本的には無制限であり、プロセスでリークが発生してもシステムの他の部分に影響を与えないようにすることができます。タスク マネージャーに表示される数値と、ハードコーディングされた最大値が一致しない理由を理解するには、エグゼクティブがハンドル テーブルを構成する方法を調べる必要があります。

ハンドル テーブル エントリは、付与されたアクセス マスクとオブジェクト ポインターを格納するのに十分な大きさである必要があります。アクセス マスクは 32 ビットですが、ポインターのサイズは 32 ビット システムか、64 ビット システムかによって異なります。したがって、ハンドル エントリは、32 ビット版 Windows では 8 バイト、64 ビット版 Windows では 12 バイトです。64 ビット版 Windows では、ハンドル エントリのデータ構造を 64 ビットの境界に整列するので、64 ビットのハンドル エントリでは、実際には 16 バイトが消費されます。64 ビット版 Windows でのハンドル エントリの定義を、dt (dump type) コマンドを使用してカーネル デバッガーで表示したものを次に示します。

図 7

図 7

この出力を見ると、この構造が実際には共用体であり、オブジェクト ポインターやアクセス マスク以外の情報が格納されることもあるということがわかりますが、ここではこの 2 つのフィールドを強調表示しています。

エグゼクティブは、必要に応じて、ページ サイズのブロック単位でハンドル テーブルを割り当てて、ハンドル テーブルのエントリに分割します。これは、1 ページ (x86 システムでも x64 システムでも 4096 バイト) には、32 ビット版 Windows では 512 個のエントリ、64 ビット版 Windows では 256 個のエントリを格納できることを意味します。エグゼクティブは、ハンドル エントリに割り当てる最大ページ数を決定するために、ハードコーディングされた最大値である 16,777,216 を、1 ページあたりのハンドル エントリ数で除算します。32 ビット版 Windows では 32,768、64 ビット版 Windows では 65,536 になります。エグゼクティブは、各ページの最初のエントリをエグゼクティブ自体の追跡情報用に使用するので、プロセスで利用可能なハンドル数は実際には 16,777,216 からその数を引いた値になります。これが、Testlimit で得られる結果の理由です (16,777,216 - 65,536 = 16,711,680 および 16,777,216 - 32,768 = 16,744,448)。

ハンドルとページ プール

ハンドルに影響する 2 つ目の制限は、ハンドル テーブルを格納するのに必要なメモリの容量で、これはエグゼクティブによってページ プールから割り当てられます。エグゼクティブでは、プロセッサのメモリ管理ユニット (MMU) による仮想アドレスから物理アドレスへの変換の管理方法に似た、3 つのレベルのスキームを使用して、割り当てるハンドル テーブルのページを追跡します。これまでに、実際のハンドル テーブル エントリを格納する、最下位レベルと中間レベルの構成について見てきました。最上位レベルは、中間レベルのテーブルへのポインターとして機能し、32 ビット版 Windows では 1 ページあたり 1024 のエントリが含まれます。したがって、最大数のハンドルを格納するのに必要なページの総数は、32 ビット版 Windows の場合、16,777,216/512*4096 で 128 MB になります。これは、タスク マネージャーで表示された Testlimit のページ プールの使用量とも一致します。

図 8

図 8

64 ビット版 Windows では、最上位レベルのポインターの 1 ページには、256 のポインターがあります。つまり、ハンドル テーブル全体のページ プールの合計使用量は、16,777,216/256*4096 で 256 MB になります。64 ビット版 Windows で Testlimit のページ プールの使用量を調べると、この計算が正しいことがわかります。

図 9

図 9

ページ プールは、一般的に、これらのサイズに対応するのに十分な大きさがありますが、前述のように、これほど多くのハンドルを作成するプロセスは、ほぼ確実に他のリソースを使い果たしてしまい、プロセスごとのハンドル数の上限に達すると他のオブジェクトを開くことができなくなり、プロセス自体に障害が発生します。

ハンドル リーク

プロセスでハンドル リークが発生すると、時間経過と共にハンドル数が増加します。ハンドル リークに注意しなければならない理由は、Testlimit で作成されるような、すべてが同じオブジェクトを指すハンドルとは異なり、ハンドルがリークしているプロセスではオブジェクトもリークしている可能性が高いからです。たとえば、プロセスが作成したイベントを閉じることに失敗した場合、ハンドル エントリとイベント オブジェクトの両方がリークします。イベント オブジェクトは非ページ プールを使用するので、リークが発生すると、ページ プールに加えて非ページ プールにも影響します。

Process Explorer のハンドル ビューを使用すると、新しいハンドルが緑で、閉じられたハンドルが赤で強調表示されるので、プロセスでリークしているオブジェクトを視覚的に特定できます。緑のハンドルが多く、赤のハンドルが少ない場合、リークが発生している可能性があります。Process Explorer でのハンドルの強調表示を確認するには、コマンド プロンプトのプロセスを開始して Process Explorer でそのプロセスを選択し、ハンドル ビューの下側のペインを開いてから、コマンド プロンプトでディレクトリを移動します。移動する前の作業ディレクトリのハンドルは赤で強調表示され、移動した先の作業ディレクトリのハンドルは緑で強調表示されます。

図 10

図 10

既定では、Process Explorer は、名前を持つオブジェクトを参照するハンドルのみを表示します。プロセスが使用しているすべてのハンドルを表示するには、[View] メニューの [Show Unnamed Handles and Mappings] を選択します。コマンド プロンプトのハンドル テーブルに表示された名前がないハンドルを次に示します。

図 11

図 11

多くのバグと同様に、これを修正できるのは、リークが発生しているコードの開発者だけです。エクスプローラー、Service Host、Internet Explorer のように、複数のコンポーネントや拡張機能をホストできるプロセスでリークが見つかった場合、次に問題なるのはどのコンポーネントでリークが発生しているかということです。これを特定することができれば、問題のある拡張機能を無効化またはアンインストールして問題を回避したり、更新プログラムをインストールして問題を修正したり、バグをベンダーに報告したりすることが可能になります。

幸いなことに、Windows にはハンドル トレース機能があり、この機能を使用して、リークおよびその原因となっているソフトウェアを特定できます。この機能はプロセスごとに有効であり、アクティブな場合、エグゼクティブはハンドルが作成されるか、閉じられるたびにスタック トレースを記録します。この機能を有効にするには、マイクロソフトから無償でダウンロードできる Application Verifier (英語) を使用するか、Windows デバッガー (英語) (Windbg) を使用します。システムで、起動時からプロセスのハンドルのアクティビティを追跡するには、Application Verifier を使用してください。いずれの場合も、トレース情報を表示するには、デバッガーと !htrace (英語) デバッガー コマンドを使用する必要があります。

実際のトレースを確認するために、Windbg を起動し、前に開いていたコマンド プロンプトにアタッチしました。次に、!htrace コマンドを、-enable スイッチを指定して実行して、ハンドルのトレースを有効にしました。

図 12

図 12

プロセスの実行を継続させるために、もう一度ディレクトリを変更しました。次に、Windbg に切り替えてプロセスの実行を停止し、オプションを指定せずに htrace を実行しました。これにより、前回の !htrace スナップショット (-snapshot オプションによって作成される) 以降またはハンドル トレースが有効にした後にプロセスで実行された開く操作と閉じる操作がすべて一覧表示されます。同じセッションのこのコマンドによる出力を次に示します。

図 13

図 13 [拡大図]

イベントは最近のものから順に出力されるので一番下から見ていくと、コマンド プロンプトはハンドル 0xb8 を開いて閉じ、次にハンドル 0x22c を開き、最後にハンドル 0xec を閉じています。ディレクトリの変更の後で Process Explorer を最新の情報に更新すると、ハンドル 0x22c は緑で、0xec は赤で表示されますが、0xb8 は、このハンドルを開いてから閉じるまでの間に Process Explorer を最新の情報に更新していなければ表示されません。0x22c を開いたときのスタックは、それがコマンド プロンプト (cmd.exe) で ChangeDirectory 関数を実行した結果であることを示しています。Process Explorer にハンドル値の列を追加すると、新しいハンドルが 0x22c であることを確認できます。

図 14

図 14

リークのみを検索したい場合は、!htrace を -diff スイッチを指定して実行します。これにより、前回のスナップショット以降またはトレースの開始以降の新しいハンドルのみが表示されます。このコマンドを実行すると、予想どおり、ハンドル 0x22c のみが表示されます。

図 15

図 15 [拡大図]

最後に、ハンドル リークのデバッグに関するさらに多くのヒントが得られるすばらしいビデオを紹介します。これは、Channel 9 による Jeff Dailey のインタビュー映像です。彼は、マイクロソフトのエスカレーション エンジニアとして、日常業務でこのようなデバッグを行っています (https://channel9.msdn.com/posts/jeff_dailey/Understanding-handle-leaks-and-how-to-use-htrace-to-find-them/ (英語))。

次回は、その他のハンドル ベースのリソースである、GDI オブジェクトとユーザー オブジェクトの制限について考察していきます。これらのリソースへのハンドルは、エグゼクティブではなく、Windows サブシステムによって管理されているため、別のリソースを使用しており、制限の内容も異なります。

公開: 2009 年 9 月 29 日月曜日 5: 04 PM 投稿者: markrussinovich (英語)

ページのトップへ

共有

ブログにコピー: ([Ctrl] + [C] でコピーしてください)