特集
Windows 9x or Windows 2000?

7.Windows 9xのプロセス管理メカニズム(2)

デジタルアドバンテージ
2000/08/23


Windows 9xのスレッド スケジューリング

 ここまでで、Windows 9xのWin32アプリケーションでは、スレッドをスケジューリングの単位として、それらがプリエンプティブにスケジューリングされることが分かった。Windows 9xでは、アプリケーションの自主的な制御の解放を待つことなく、ハードウェア的に発生されるクロック割り込みに応じて、Windows 9xシステムが同時実行されているスレッドに制御を与える(この際、クロック割り込みから、次のクロック割り込みが発生するまでの時間をタイムスライス(time slice)と呼ぶ)。

 Microsoft Visual Studioなどの開発システムに含まれているProcess Viewerツールを利用すれば、その時点で実行されているプロセスやスレッドを一覧表示することができる。

プロセスやスレッドを表示するProcess Viewerツール
Microsoft Visual Studioなどの開発システムに含まれているProcess Viewerツールを使えば、Windows 9xのプロセスやスレッドの情報を得ることができる
  現在実行されているプロセス イメージの名前。
  プロセスID。
  基本プライオリティ。各プロセスのベースとなるプライオリティ。
  プロセス内のスレッド数。
  アプリケーションのタイプ。Windows 3.x以前の16bit Windowsアプリケーションか、Windows 95以降の32bit Windowsアプリケーションかを表す。
  プログラム コードの含まれているファイル パス名。
  プロセス内のスレッドのIDとプライオリティ。

 Windows 9xシステムでは、現在実行中のアプリケーションはもとより、システム用のプロセスやスレッドなどが多数同時に実行される。すでに述べたように、Windows 9xでは、ごく短い時間のタイム スライスごとに、これらのスレッドを実行していく。ただしこの際でも、すべてのスレッドが平等に実行されるわけではない。たとえば今、バックグラウンド側でスプレッド シートの再計算を実行しながら、フォアグラウンド側のワードプロセッサでキーボード入力を行っているとしよう。ときと場合にもよるだろうが、こんなとき多くのユーザーは、バックグラウンドの処理が多少遅くなったとしても、フォアグラウンドの文字入力がもたつくことなく、高速に処理されてほしいと考えるだろう。またたとえばデバイス ドライバなど、システム内で低レベルな位置づけにあるプログラムの処理(ファイルI/OやネットワークI/Oなど)の実行速度は、上位ソフトウェア(OSやアプリケーション)に大きな影響を及ぼすため、できるだけ素早く実行されることが望まれる。

 このようにマルチタスク システムでは、システム全体が効率よく運用できるように、同時実行されているプロセス(タスク)に優先順位を付け、優先度の高いものほど先に処理するように工夫する必要がある。OS内部でこのような働きを担う部分は、スケジューラ(scheduler)と呼ばれる。ちょっとしたレスポンス タイム(キーボードを押して、画面に文字が表示されるまでの時間など)が使い勝手に大きく影響を及ぼすWindowsのようなアプリケーション環境では、スケジューラの出来いかんによって、ウィンドウ システム全体の使いやすさや、ユーザーの精神的な負担が大きく異なってくる。非常に重要な機能だといってよいだろう。

 Windows 9xでは、各スレッドの実行プライオリティ(優先順位)として、0〜31までの32段階の整数を割り当てる。この数値は、値が大きいほど優先順位が高いことを表す。スレッドの実行プライオリティは、スレッドごとに固定の値というわけではなく、スレッドの状態に応じて、値が大きくされたり(優先度が高められたり)、値が小さくされたり(優先度を下げられたり)する。

 Windows 2000のそれに比較すると、Windows 9xのスケジューリング メカニズムは比較的簡単である。具体的には、Windows 9xシステム内部には役割の異なる2つのスケジューラがある。1つはプライマリ スケジューラ、もう1つはタイムスライス スケジューラである。

Windows 9xのスケジューラ
Windows 9xには、プライマリ スケジューラとタイムスライス スケジューラという役割の異なる2つのスケジューラがある。このうちプライマリ スケジューラでは、現在実行されている各スレッドの実行プライオリティを評価し、その時点で最も高いプライオリティを持つスレッドを特定する。次にタイムスライス スケジューラは、プライマリ スケジューラから通知された各スレッドに対し、タイムスライスの何%を割り当てればよいかを算出し、スレッドを実行する。

 たとえばこの図は、あるタイムスライスにおいて、5つのスレッド(スレッドA〜E)が実行されている状況を示している。まず最初にプライマリ スケジューラは、現在実行されている各スレッドのプライオリティを評価する。たとえばこの例では、最もプライオリティが高いスレッドはB(プライオリティ=16)とスレッドD(同じくプライオリティ=16)である。この場合プライマリ スケジューラは、これよりも低いプライオリティを持つすべてスレッド(スレッドA、スレッドC、スレッドE)をサスペンド状態(一時停止状態)とし、今処理中のタイム スライスにおいては、これらのスレッドは実行しない。そしてプライマリ スケジューラは、実行するスレッドがBとDであることを次のタイムスライス スケジューラに通知する。するとタイムスライス スケジューラは、今処理中のタイムスライス時間のうち、各スレッドに何%を割り当てればよいかを算出し、スレッド(今回はスレッドBとスレッドD)を実行する。スレッドの実行が完了したら、スケジューラは次のタイムスライスの処理(プライマリ スケジューラによるスレッドのプライオリティの評価)を行う。仮にスレッドのプライオリティが変化しなかったとすれば、次のタイムスライスでもスレッドBとDが実行されることになる。Windows 9xは、スレッドのプライオリティを調整することで、適切なスレッドが実行されるようにする。

 たとえばWindows 9xでは、フォアグラウンドのアプリケーションが切り替えられると、新たにフォアグラウンド側になったアプリケーションのスレッドのプライオリティを上げる。これにより、ユーザーが現在操作しているフォアグラウンド側のアプリケーションが優先的に実行されるようになる。

Windows 9xコア モジュールのサンク レイヤ

 以上が、基本的なWindows 9xのマルチタスク システムである。後述するWindows 2000のマルチタスク システムと比較すると、メカニズムは単純だが、Win32アプリケーションについては、Windows 2000と変わらず、各スレッドがプリエンプティブにスケジューリングされ、実行されるように見える。しかしWindows 9xには、本来のマルチタスク システムとは異なる部分に、重大な制限が加えられている。

 この制限に言及する前に、Windows 9xカーネルの構成について、さらに一歩踏み込んで説明しておこう。すでに「Windows 9xカーネルの概要」でも紹介したとおり、Windows 9xシステムのコア部分は、User、GDI、Kernelという3つのモジュールで構成されている。このうちUserは、ウィンドウの管理やメニュー、ダイアログの表示など、主にユーザー インターフェイスにかかわる機能を提供するモジュール、GDI(Graphics Device Interface)はグラフィックス描画サービスを提供するモジュール、Kernelはメモリ管理やプロセス管理などのOSの基本機能を提供するモジュールである。周知のとおり、Windows 9xでは32bit対応が施され、Win32アプリケーションを実行できるようになったのだが、プリエンプティブなマルチタスクを前提として、抜本的に32bit化が施されたのはKernelモジュールだけで、UserモジュールやGDIモジュールについては、一部は32bit化されたものの、主に互換性維持の理由から、従来の16bitモジュールをそのまま流用している。この様子を次に示す。

Windows 9xのサンク レイヤ
Windows 9xのカーネル モジュールは、主に互換性上の理由から、一部では従来の16bitモジュールがそのまま使われている。Win32アプリケーションから16bitモジュールを呼び出したり、逆にWin16アプリケーションから32bitモジュールを呼び出したりするために、Windows 9xにはサンク レイヤ(thunk layer)と呼ばれる機構が組み込まれている。

 Windows 9xのUser、GDI、Kernelモジュールは、実際には、各モジュールごとに16bit DLL版と32bit DLL版の2種類が提供されており、実行するアプリケーションがWin32か、Win16かによって、いずれのDLLを呼び出すかが決定される。図のUser32、GDI32、KERLEL32は32bit DLLを表し、User16、GDI16、KERNEL16は16bit DLLを表している。

 Kernelモジュールについては、スレッド管理やメモリ管理、ファイルI/Oなど、すべてが完全に32bitモジュールとして実装されており、KERNEL16は、単なるWin16アプリケーション用の入口にすぎない(図のKERNEL32の矩形が大きく、KERNEL16の矩形が小さいのはそのためだ)。Win16アプリケーションがKERNEL16を呼び出すと、それはサンク レイヤを通して32bit版のKERNEL32呼び出しに代えられる(16bitから32bitに変換されるので、この変換はサンク アップと呼ばれる)。もちろん、Win32アプリケーションによるKERNEL32呼び出しは、そのままKERNEL32モジュールで処理される。

 GDIモジュールでは、TrueTypeフォントのラスタライザや印刷サブシステムなど一部は32bit化されているものの(GDI32側に実装されているものの)、基本的なグラフィックス管理機能などは従来の16bit版モジュール(GDI16)をそのまま使っている。このためWin32アプリケーションがGDI32を呼び出したとき、GDI16を呼び出す必要がある処理については、サンク レイヤを通してGDI16呼び出しに代えられる(こちらは32bit→16bitへの変換なので、サンク ダウンと呼ばれる)。これとは逆に、Win16アプリケーションがGDI16を呼び出したとき、GDI32を呼び出す必要がある処理については、サンク アップ処理が行われる。

 Userモジュールは、Kernelモジュールとは正反対に、基本的に従来の16bitモジュールがそのまま使われている。このためWin32アプリケーションによるUSER32呼び出しは、サンク レイヤを通して、USER16呼び出しに代えられる(サンク ダウンされる)。

16bitコードを使い続けることの問題点とは?

 さて、Windows 9xでは、カーネル モジュールの一部として16bitコードがそのまま使われていることが分かった。従来の16bitコードをそのまま使うことのメリットは、従来のWin16アプリケーションとの完全な互換性が維持されることだ。それでは、16bitコードをそのまま使うことのデメリットは何だろうか?

 16bitコードを使う問題は、整数値の範囲が32bitに比べて小さいことではない。「コラム:Windows 3.xのマルチタスク システム」で述べたとおり、この16bitコードは、ノンプリエンプティブなマルチタスク環境であるWindows 3.x向けに設計されたもので、コードがリエントラント(再入可能)になっていないということだ。ノンプリエンプティブなマルチタスクでは、あるアプリケーションがシステム コールを行った場合、そのシステム コールの処理を終えて制御をアプリケーションに戻すまでは、別の誰かがシステム コールを発生することはあり得ない。しかしこれまでの説明から分かるとおり、Windows 9xのようなプリエンプティブなマルチタスク環境では、システム コールを処理している最中でも、制御が別のアプリケーションに与えられ、そのアプリケーションがさらにシステム コールを発生する(システム コールの再入が発生する)可能性がある。システム コールのコードをこのような再入に対応させるためには、グローバル変数などは使わず、呼び出し側のスタックなどを使って変数を格納するなどが必要だ(これによりプログラム コードでは、現在の呼び出し元に応じた変数の処理が可能になる)。

 つまり、16bitコードの最大の問題は、コードがリエントラントに設計されておらず、再入に耐えられない仕様だということである。

Windows 9x最大の弱点。Win16Mutex

 リエントラントでない16bitコードを使いながら、プリエンプティブなマルチタスク環境で再入が発生しないようにするために、Windows 9xでは、Win16Mutexと呼ばれるセマフォ(semaphore、鉄道の腕木式信号機のこと)を設定している。セマフォとは、たとえれば「使用中」を示すトイレの札のようなものである。具体的には、マルチタスク環境などで、複数のプロセスが共有する資源の競合を避けるために使用されるフラグ変数で、たとえば共有資源を使用するときには、このセマフォのフラグをセットし、使い終わったらリセットにするようにする(あるいは、フラグのセット/リセットの代わりに、それぞれセマフォの「取得」と「解放」と呼ばれることもある)。つまりこのフラグがセットの状態なら共有資源は誰かが「使用中」で、リセットの状態なら「未使用」だということだ。誰かが「使用中」であるときには、それが「未使用」になるまで、別のプロセスは待たなければならない。Win16Mutexは、この「使用中」の札を、16bitコード呼び出しに対して適用するものである。つまり、Win16Mutexがセットされているときには、他のスレッドによる16bitコード呼び出しがブロックされる(待たされる)。それではこのWin16Mutexは、いつセット(「使用中」)にされるのか?

 まず、Win16アプリケーションが実行されるときには、必ずWin16Mutexがセットされる。具体的には、Win16アプリケーションがメッセージを取り出すGetMessage API(またはPeekMessage API)呼び出しにリターンする時点で、Win16Mutexがセットされ、メッセージ ループによる次のGetMessage呼び出しが行われるまでWin16Mutexはセットされたままになる。

 これに対しWin32アプリケーションでは、16bitモジュールへのサンク ダウンが発生したときにWin16Mutexがセットされ、16bitコードの処理が完了した時点でWin16Mutexがリセットされる。したがってWin32アプリケーションの実行中は、すべての処理が32bitコードで完結するKernelモジュール呼び出しではWin16Mutexはセットされず、UserやGDIモジュール呼び出しではWin16Mutexがセットされる場合がある。

 今述べたように、Win16Mutexは、16bitコードへの再入をブロックするためのセマフォである。したがってWin16Mutexがセットされているときには、たとえプリエンプティブなスケジューリングによってWin32アプリケーションのスレッドが制御を得たとしても、16bitコード呼び出し(サンク ダウンを伴うAPI呼び出し)はブロックされてしまうということだ。筆者の経験で言えば、Windows 9x上であるアプリケーションがハングアップしたとき、他のアプリケーションで作業中のデータを救おうと、これらに切り替えようとしても、マウス カーソルが砂時計のまま、切り替えが行えないことが少なくない。分析したわけではないので、すべてのケースで当てはまるとはかぎらないが、ハングアップしてしまったアプリケーションが、Win16Mutexをセットしたままにしているために、ウィンドウの切り替えが実行できないと考えることはできないだろうか(これまでに述べたとおり、ウィンドウの切り替えなどはUserモジュールの処理である)。

 読者がプログラマで、Win32アプリケーションを開発するときに、Win16Mutexの影響を最小限に留めたければ、16bitサンク ダウンが発生する可能性が高いユーザー インターフェイスの処理(UserモジュールやGDIモジュールを呼び出す処理)と、サンク ダウンが発生しないKernelモジュール呼び出しを行うコード(たとえば、典型的にはファイルI/Oなど)を別々のスレッドにするとよい。こうすると、たとえユーザー インターフェイスの処理がWin16Mutexによってブロックされても、それ以外の処理を行うスレッドは処理を続けられるようになる。しかしこれは、Windows 9xでのみ通用する実装テクニックであり、純粋に効率的なアプリケーション設計のためのテクニックとはまったくレベルが違うことに注意すること。


 INDEX
  [特集]Windows 9x or Windows 2000?
     1.イントロダクション
     2.Windows 9xカーネルの概要
      コラム:Windows歴史、メモリの歴史 (1)
      コラム:Windows歴史、メモリの歴史 (2)
     3.Windows 2000カーネルの概要 (1)
     4.Windows 2000カーネルの概要 (2)
     5.プロセス管理の概要
     コラム:Windows 3.xのマルチタスク システム
     6.Windows 9xのプロセス管理メカニズム (1)
   7.Windows 9xのプロセス管理メカニズム (2)
     8.Windows 2000のプロセス管理メカニズム (1)
     9.Windows 2000のプロセス管理メカニズム (2)
 
 特集


Windows Server Insider フォーラム 新着記事
@ITメールマガジン 新着情報やスタッフのコラムがメールで届きます(無料)

注目のテーマ

Windows Server Insider 記事ランキング

本日 月間