Windows の限界に挑む: プロセスとスレッド

翻訳元: Pushing the Limits of Windows: Processes and Threads (英語)

この記事は、Windows の基本的なリソースの限界について考察する「Windows の限界に挑む」シリーズの 4 回目の投稿です。今回は、Windows でサポートされるスレッドおよびプロセスの最大数の制限について説明します。スレッドとプロセスの違いについて簡単に説明し、スレッドの制限や、プロセスの制限について考察していきます。初めにスレッドの制限を取り上げます。アクティブ プロセスにはそれぞれ最低 1 つのスレッドがある (終了されたプロセスで、別のプロセスが所有するハンドルで参照されているプロセスはスレッドを持たない) ため、プロセスに対する制限は、スレッドの上限値の影響を直接受けるからです。

一部の UNIX バリアントとは異なり、Windows のほとんどのリソースについて、固定の上限値はオペレーティング システムにコンパイルされておらず、これまでに説明してきた基本的なオペレーティング システムのリソースに基づいて制限が決定されます。たとえば、プロセスとスレッドは、物理メモリ、仮想メモリ、およびプール メモリを必要とするため、特定の Windows システムで作成できるプロセスやスレッドの数は、プロセスやスレッドをどのように作成するか、および最初にどの制約に到達するかによって異なりますが、最終的にはこれらのリソースのいずれかによって決まります。したがって、前回までの投稿をまだ読んでいない場合は、先にお読みになることをお勧めします。この記事では、これまでに説明した、予約済みのメモリ、コミット済みのメモリ、システム コミット制限、およびその他の概念について言及しているからです。

プロセスとスレッド

Windows のプロセスは、基本的には、実行可能イメージ ファイルの実行をホストするコンテナーです。プロセスはカーネル プロセス オブジェクトによって表され、Windows はこのプロセス オブジェクトと関連するデータ構造を使用して、イメージの実行に関する情報を格納および追跡します。たとえば、プロセスは、プロセスのプライベート データおよび共用データを保持する仮想アドレス空間を持ち、この仮想アドレス空間に実行可能イメージおよび関連付けられた DLL がマップされます。Windows では、アカウンティングと診断ツールによるクエリのために、プロセスによるリソースの使用を記録し、プロセスによるオペレーティング システム オブジェクトへの参照をプロセスのハンドル テーブルに登録します。プロセスはトークンと呼ばれるセキュリティ コンテキストで動作し、プロセスに割り当てられているユーザー アカウント、アカウント グループ、および特権がトークンによって識別されます。

さらに、プロセスには、プロセス内で実際にコードを実行する (技術的には、プロセスが実行されるのではなく、スレッドが実行される) スレッドと、カーネル スレッド オブジェクトで表されるスレッドが、1 つ以上含まれます。アプリケーションが既定の初期スレッドに加えてスレッドを作成する理由はいくつかあります。ユーザー インターフェイスを持つプロセスは、通常、複数のスレッドを作成して作業を実行し、メイン スレッドがユーザー入力とウィンドウ操作コマンドを処理できるようにします。また、スケーラビリティのために複数のプロセッサを利用するアプリケーションや、スレッドが同期 I/O 処理が完了するまで待機して停止している間も実行する必要があるアプリケーションでも、複数のスレッドを使用する利点があります。

スレッドの制限

スレッドの CPU レジスタの状態、スケジューリングの優先順位、リソース使用のアカウンティングなど、スレッドに関する基本的な情報に加えて、各スレッドには、スタックと呼ばれる、プロセスのアドレス空間の一部が割り当てられます。スレッドは、プログラム コードの実行時にスタックをスクラッチ ストレージとして使用して、関数のパラメーターを渡したり、ローカル変数を保持したり、関数のリターン アドレスを保存したりできます。システムの仮想メモリが浪費されないように、当初はスタックの一部だけが割り当てられ (つまりコミットされ)、残りは単に予約されます。スタックはメモリ内を下に向かって増大するので、システムはスタックのコミットされた部分の上に、アクセス時に追加メモリの自動コミット (スタック拡張と呼ばれる) をトリガーするガード ページを配置します。この図は、スタックのコミット済み領域が下方向に増大し、スタック拡張時にガード ページが移動する様子を、32 ビット アドレス空間の例で示しています (図の比率は大まかなものです)。

図 1

図 1

実行可能イメージの PE (移植可能な実行可能ファイル) の構造によって、スレッド スタック用に予約され最初にコミットされるアドレス空間の量が指定されます。リンカーの既定の設定では、1 MB が予約され、1 ページ (4 K) がコミットされますが、開発者はプログラムをリンクする際に PE 値を変更するか、または個々のスレッドについて CreateThread (英語) を呼び出すことによって、これらの値をオーバーライドすることができます。Visual Studio に付属する Dumpbin (英語) などのツールを使用して、実行可能ファイルの設定を確認できます。新しい Visual Studio プロジェクトで生成された実行可能ファイルについて、/headers オプションを指定して実行した Dumpbin の出力を以下に示します。

図 2

図 2

16 進数の数値を変換すると、スタックの予約サイズが 1 MB、初期コミットが 4 K であることがわかり、新しい Sysinternals の VMMap (英語) ツールを使用してこのプロセスにアタッチし、そのアドレス空間を表示すると、スレッド スタックの初期コミット済みページ、ガード ページ、および残りの予約済みスタック メモリを確認できます。

図 3

図 3

各スレッドはそれぞれプロセスのアドレス空間の一部を消費するので、プロセスが作成できるスレッド数の基本的な制限は、アドレス空間のサイズをスレッド スタックのサイズで除算した値によって決まります。

32 ビット スレッドの制限

プロセスにコードやデータがなく、アドレス空間全体をスタックとして使用できる場合でも、既定のアドレス空間が 2 GB である 32 ビットのプロセスでは、作成できるスレッド数は最大で 2,048 です。32 ビット版 Windows で、-t スイッチ (スレッドの作成) を指定して Testlimit (英語) ツールを実行し、スレッド数の制限を確認した場合、出力は次のようになります。

図 4

図 4

繰り返しますが、アドレス空間の一部は既にコードや初期ヒープによって使用されているので、2 GB のすべてをスレッド スタックとして使用できるわけではなく、作成されるスレッドの総数は理論的な制限である 2,048 には及びません。

Testlimit 実行可能ファイルは、大容量のアドレス空間に対応したオプションを指定してリンクしました。つまり、2 GB を超えるアドレス空間がある場合 (たとえば、32 ビット システムを Boot.ini の /3GB または /USERVA オプションを指定してブートした場合や、Vista およびそれ以降の increaseuserva でこれに相当する BCD オプションを指定してブートした場合)、そのアドレス空間を使用できます。32 ビット版プロセスを 64 ビット版 Windows で実行した場合、4 GB のアドレス空間が与えられますが、32 ビットの Testlimit が 64 ビット版 Windows 上で実行されたときに作成できるスレッドの数はいくつでしょうか。これまでの説明に従うと、答えは約 4096 (4 GB を 1 MB で割った値) ですが、この数字は実際にはもっと小さくなります。32 ビットの Testlimit を 64 ビット版 Windows XP で実行した場合の例を以下に示します。

図 5

図 5

このような差が発生するのは、64 ビット版 Windows で 32 ビット アプリケーションを実行すると、実際には 64 ビットのプロセスが、32 ビットのスレッドではなく 64 ビットのコードを実行するからです。その結果、64 ビット スレッド スタックと 32 ビット スレッド スタックの領域が各スレッド用に予約されます。64 ビット スタックには 256 K が予約されます (ただし、Vista 以前のシステムでは、初期スレッドの 64 ビット スタックは 1 MB です)。各 32 ビット スレッドは、最初から 64 ビット モードで実行されるので、開始時に使用していたスタック空間が 1 ページを超えると、一般的に、64 ビット スタックのうち少なくとも 16 KB がコミットされることがわかります。32 ビット スレッドの 64 ビットおよび 32 ビットのスタックの例を以下に示します ("Wow64" というラベルのスタックは、32 ビット スタックです)。

図 6

図 6

32 ビットの Testlimit は、各スレッドがスタック用に 1 MB + 256 K のアドレス空間を使用すると仮定した場合、64 ビット版 Windows では 3,204 のスレッドを作成できました (Vista 以前のバージョンの Windows で実行した最初の例では、1 MB + 1 MB が使用されるので例外です)。これは予想どおりの結果でしょう。しかし、32 ビットの Testlimit を 64 ビット版の Windows 7 で実行した場合の結果は、これとは異なります。

図 7

図 7

Windows XP での結果と Windows 7 での結果の違いは、Windows Vista で導入された ASLR (Address Space Layout Randomization) によってアドレス空間のレイアウトがよりランダムになったため断片化が発生するからです。DLL の読み込み、スレッド スタック、ヒープの配置のランダム化は、マルウェアによるコード インジェクションの防止に役立ちます。次の VMMap の出力からわかるように、357 MB のアドレス空間がまだ使用可能ですが、最大空きブロックのサイズは 128 K であり、32 ビット スタックで必要な 1 MB よりも小さくなっています。

図 8

図 8

前に説明したように、開発者は既定のスタック予約をオーバーライドできます。このようにする理由の 1 つに、スレッドのスタック使用量が常に既定の 1 MB よりも大幅に少ない場合にアドレス空間が浪費されないようにすることがあります。Testlimit では、PE イメージ内の既定のスタック予約サイズは 64 K に設定されており、-n スイッチと共に -t スイッチを指定すると、64 K のスタックを持つスレッドが作成されます。256 MB の RAM を搭載した 32 ビット版 Windows XP システムでの出力を以下に示します (この実験を小規模なシステムで実行したのは、この固有の制限を強調するためです)。

図 9

図 9

アドレス空間に何も問題がないのに、別のエラーがあることに注意してください。実際、64 K スタックの場合、約 32,000 のスレッド (2 GB/64 K = 32,768) を作成できるはずです。この場合は何が制限になっているのでしょうか。コミットやプールなど原因になりそうな項目を見ても、すべて制限値未満なので、この問題を解く鍵はなさそうです。

図 10

図 10

カーネル デバッガーで追加のメモリ情報を調べて初めて、常駐利用可能メモリがすべて使用され、しきい値に達していることがわかります。

図 11

図 11

常駐利用可能メモリは、RAM に保持しておく必要があるデータやコードに割り当てることができる物理メモリです。たとえば、非ページ プールや非ページ ドライバーは、デバイス I/O 処理用に RAM 内でロックされるメモリと同様に、常駐利用可能メモリに対して不利に働きます。各スレッドには、これまでに説明してきたユーザー モード スタックと、システム コールの実行時などカーネル モードで実行されるときに使用されるカーネル モード スタックがあります。スレッドがアクティブである場合、そのカーネル スタックはメモリ内でロックされるので、スレッドがページ フォールトを取得しないカーネル内でコードを実行できます。

基本的なカーネル スタックは、32 ビット版 Windows では 12 K、64 ビット版 Windows では 24 K です。14,225 個のスレッドでは約 170 MB の常駐利用可能メモリが必要になり、これは、Testlimit を実行していないときの、このシステムの空き容量と等しくなります。

図 12

図 12

常駐利用可能メモリの上限に達すると、多くの基本的な処理がエラーになり始めます。デスクトップの Internet Explorer のショートカットをダブルクリックしたときのエラーの例を以下に示します。

図 13

図 13

予想どおり、256 MB のRAM の 64 ビット版 Windows で実行した場合、Testlimit で作成できるスレッドは 6,600 に過ぎません。これは、256 MB の RAM の 32 ビット版 Windows で、常駐利用可能メモリが不足する前の約半分です。

図 14

図 14

前に "基本" カーネル スタックと述べた理由は、グラフィック関数やウィンドウ関数を実行するスレッドの場合は、最初の呼び出しを実行するときに、32 ビット版 Windows では 20 K、64 ビット版 Windows では 48 K という大きなスタックが取得されるからです。Testlimit のスレッドはこのような API を呼び出さないので、基本カーネル スタックを持ちます。

64 ビット スレッドの制限

32 ビット スレッドと同様、64 ビット スレッドでも既定で 1 MB がスタックとして予約されますが、64 ビット プロセスのユーザー モード アドレス空間はずっと大きい (8 TB) ので、多くのスレッドを作成する場合にアドレス空間は問題になりません。ただし、常駐利用可能メモリは、明らかに潜在的な制限要因になります。64 ビット版の Testlimit (Testlimit64.exe) では、256 MB の 64 ビット版 Windows XP システムで、-n スイッチを付けた場合も、付けなかった場合も、作成できたスレッド数は約 6,600 で、32 ビット版で作成できた数と同じです。これは、常駐利用可能メモリの制限があるためです。ただし、2 GB の RAM を搭載したシステムでは、Testlimit 64 で作成できたスレッド数は 55,000 に過ぎず、常駐利用可能メモリが制限要因である場合に作成できるはずのスレッド数 (2 GB/24 K = 89,000) をかなり下回っています。

図 15

図 15

この場合、システムの仮想メモリが不足し、"ページング ファイルが小さすぎる" というエラーが発生する原因は初期スレッド スタックのコミット値です。コミット レベルが RAM のサイズに達すると、スレッド作成の速度は非常に遅くなります。これは、システムでスラッシングが発生したり、新しいスレッドのスタック用の領域を確保するために、以前に作成したスレッド スタックのページ アウトが発生したりすると共に、ページング ファイルを拡張する必要があるからです。この結果は、-n スイッチを指定した場合も同じです。スレッドの初期スタック コミット値は同じだからです。

プロセスの制限

Windows でサポートされるプロセスの数は、明らかにスレッド数よりも少なくなります。これは、各プロセスには 1 つのスレッドがあり、プロセス自体が他のリソースも使用するからです。2 GB の RAM を搭載した 64 ビット版 Windows XP システムで 32 ビット版 Testlimit を実行した場合、約 8,400 のプロセスが作成されました。

図 16

図 16

カーネル デバッガーで調べると、常駐利用可能メモリの制限に達していることがわかります。

図 17

図 17

常駐利用可能メモリに関するプロセスのコストがカーネル モード スレッド スタックのみである場合、Testlimit は 2 GB のシステムでは 8,400 よりもはるかに多くのスレッドを作成できるはずです。Testlimit を実行していない場合、このシステムの常駐利用可能メモリの容量は 1.9 GB です。

図 18

図 18

Testlimit が使用していた常駐メモリの容量 (1.9 GB) を、作成されたプロセスの数 (8,400) で割ると、プロセスあたりの常駐メモリは 230 K であることがわかります。64 ビットのカーネル スタックは 24 K なので、約 206 K の使い道がまだ不明なままです。コストの残りの部分はどこから発生しているのでしょうか。プロセスを作成するときに、Windows はプロセスの最小ワーキング セット サイズに対応できるだけの十分な物理メモリを予約します。これによって、どのような場合でも、最小ワーキング セットを満たすのに十分なデータを保持するだけの物理メモリを利用できることがプロセスに対して保証されます。既定のワーキング セット サイズは 200 KB であり、このことは Process Explorer の表示で [Minimum Working Set] 列を追加すると確認できます。

図 19

図 19

残りのおよそ 6 K は、プロセスを表すために割り当てられる追加の非ページ可能メモリとして充当される常駐利用可能メモリです。32 ビット版 Windows 上のプロセスでは、カーネル モード スレッド スタックが小さいため、使用される常駐メモリは若干少なくなります。

ユーザー モード スレッド スタックの場合と同様に、プロセスは既定のワーキング セット サイズを、SetProcessWorkingSetSize (英語) 関数を使用してオーバーライドできます。Testlimit では、-n スイッチと -p スイッチを組み合わせて指定することで、メインの Testlimit プロセスの子プロセスで、ワーキング セットが最小値の 80 K に設定されるようになります。ワーキング セットを縮小するために子プロセスを実行する必要があるので、Testlimit は新しいプロセスを作成できなくなると、もう一度子プロセスを実行できるようにするために、スリープ状態になります。4 GB の RAM を搭載した Windows 7 システムで、-n スイッチを指定して Testlimit を実行すると、システム コミット制限と呼ばれる、常駐利用可能メモリ以外の制限の影響を受けます。

図 20

図 20

次のカーネル デバッガーのレポートを見ると、システム コミット制限に達しているだけではなく、数千回のメモリ割り当てエラーが、仮想およびページ プールの割り当ての両方で発生し、その後コミット制限に達していることがわかります (ページング ファイルがいっぱいになると、制限が引き上げられるので、実際には数回、システム コミット制限に到達しています)。

図 21

図 21

Testlimit を実行する前の最終的なコミット値は約 1.5 GB であったので、スレッドは約 8 GB のコミット済みメモリを消費しています。したがって、各プロセスはおよそ 8 GB/6,600、つまり 1.2 MB を消費しています。カーネル デバッガーの !vm コマンド (アクティブ プロセスごとに割り当てられたプライベート メモリを示す) の出力は、この計算を裏付けています。

図 22

図 22

前に説明した初期スレッド スタックのコミット値の残りは、プロセス アドレス空間のデータ構造、ページ テーブル エントリ、ハンドル テーブル、プロセス オブジェクトやスレッド オブジェクト、およびプロセスが初期化時に作成するプライベート データが必要とするメモリに由来しますが、その影響はごくわずかです。

スレッドとプロセスの十分な数とは

"Windows ではいくつのスレッドがサポートされるか" および "Windows ではいくつのプロセスを同時に実行できるか" という問いに対する答えは、状況によって異なります。スレッドがスタック サイズを指定する方法やプロセスが最小ワーキング セットを指定する方法の微妙な差異に加えて、特定のシステムでこの答えに影響する主な要因として、物理メモリの容量とシステム コミット制限の 2 つがあります。いずれの場合も、アプリケーションが作成するスレッドまたはプロセスの数がこれらの制限に近い場合は、合理的な数で同じ目標を達成できる別の方法が存在している場合がほとんどなので、アプリケーションのデザインを再考する必要があります。たとえば、スケーラブルなアプリケーションの一般的な目標は、実行中のスレッドの数を CPU (NUMA ではこれをノードごとの CPU と見なすように変更している) の数と等しくなるようにすることですが、これを実現する方法の 1 つとしては、同期 I/O の使用から非同期 I/O の使用に変更し、I/O 完了ポートを利用することによって、実行中のスレッドの数と CPU の数を同じにすることができます。

公開: 2009 年 7 月 8 日水曜日 10:21 AM 投稿者: markrussinovich (英語)

ページのトップへ

共有

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