ホーム  ざれごと  ワシントン州  ツール  NT豆知識  Win32プログラミングノート  私的用語  ジョーク  いろいろ  ゲーム雑記  Favorites  掲示板   Mail

Win32 雑学ノート

Last modified: Sun Aug 26 06:59:49 2007 PDT

一つ上へ


Win32 にまつわる雑学。 思いつくまま適当に書き散らしていこうかと思います。 対象としているのは、Windows NT 系の OS (Windows 2000, XP を含む)です。

ウィドウメッセージ

Send と Post

SendMessage と PostMessage の違いを正しく認識して使い分けていますか?

一言で言うと、Send されるメッセージは同期して処理され、直接ウィンドウプロシージャへ届けられる。 受け手がメッセージを処理している間、送り手は基本的にブロックされる。
Post されたメッセージはキューに突っ込まれて送り手と受け手は非同期、明示的にキューからメッセージを取り出す必要がある(これをメッセージループと呼ぶ)。

たとえば、同じスレッドに対して送られ、フックやらややこしいことがない場合、SendMessage は直接ウィンドウプロシージャを呼ぶだけのこともある。 当然のことながら、Send されたメッセージは、メッセージループを通ることはない。

これに対して Post されたメッセージは必ずメッセージキューを経由する。 メッセージをキューから取り出すためには、GetMessage や PeekMessage を呼ばなければならない。

メッセージキュー

メッセージキューは、Post されたメッセージやキーボードやマウスなどの input メッセージが蓄えられるキュー。 基本的には FIFO (First In First Out) だが、現実には単純な FIFO では足りない。 Input メッセージは、Post されたメッセージに比べて優先順位が低い。 これは、WM_KEYDOWN の処理を考えてみれば分かる。 WM_KEYDOWN は通常 TranslateMessage に渡されて WM_CHAR を生成する。 このとき、次の入力たる WM_KEYDOWN よりも WM_CHAR の方を先に処理しなければならないのは明白である。

そのほか、WM_PAINT や WM_TIMER など特殊なメッセージもあるが今は省略。 とにかくメッセージキューは非同期のメッセージが通る道、としておこう。

メッセージキューは、スレッドが GUI スレッドになった時点で作られる。 特殊な場合を除いてスレッドには固有のメッセージキューが割り当てられ、他のスレッドと干渉することは無い。 16ビット Windows の時代にはシステム全体でひとつのメッセージキューしかなかったが、完全にプリエンプティブな NT 系の OS では、ひとつの GUI スレッドがハングしてキューがスタックしても他のスレッドに対する影 響は抑えられる。

また、16ビット Windows ではキューに入れることのできるメッセージの数に高々数十個という制限があったが、NT 系 OS ではそのような制限はない。 ただし、ハングしたスレッドに対して送られる PostMessage が延々とメモリを食いつぶすことにならないよう、それぞれのメッセージキューに1万個という制限が課せられている。 なにしろ、キューに入るメッセージはセッションページドプールからアロケートされるため、無限に無駄な食いつぶしを許すわけにもいかないのだ。 この制限値はレジストリでカスタマイズできるが、そもそも1万個も PostMessage をキューする必要が本当にあるとは思えない。 もしそうなら設計を考え直したほうが良いだろう。

Send メッセージには明確なキューは存在しないが、複数のスレッドがほぼ同時にひとつのスレッドへメッセージを send することは考えうる。 ということで、内部的にはメッセージキューとは違った FIFO を構成しているが、アプリケーションからそれを意識する必要はまずない。

キューステータス

メッセージキューはステータスを保持しており、GetQueueStatus や MsgWaitForMultipleObjectEx などで利用できる。 基本的にこれらの API がチェックするキューステータスは、前回の呼び出し時との差である。 つまり、キューステータスはユーザーモードアプリからチェックされると消えてしまう。 MsgWaitForMultipleObjectEx に対しては、MWMO_INPUTAVAILABLE フラグを指定すると、"seen" 状態に寄らずステータスをチェックする。

Send のネスト

SendMessage は、ネスト可能。 これだけでははっきり分からないと思うが、次のようなケースを想定してみよう。

二つのスレッド A, B を考える。 まずスレッド A が B にメッセージを送る。 スレッド A は B がメッセージの処理を終了するまでブロックされる。 メッセージの処理中、スレッド B にスレッド A にメッセージを送る必要が起こり、SendMessage を呼ぶとしよう。 しかし、スレッド A はブロックされている (sleep している)。 こうしてごく簡単にデッドロックが発生してしまう。

このような事態を避けるため、スレッド A が SendMessage からの戻りを待っている間は、他のスレッドからの SendMessage を受け取ることができるようになっている。 なかなか普段このネストを意識することはないが、実際にはネスティングは非常に頻繁に発生している。

サンク (thunk)

NT 系 OS には ANSI 版と Unicode 版という二つの API があったりなかったりする。 たとえば、システムメトリクスを取得する GetSystemMetrics には A/W の違いはなく、ニュートラルな API がひとつあるだけだが、なぜか SendMessage / PostMessage には A/W の二つのバージョンがある。 メッセージにとっては ANSI も Unicode も関係なさそうなものだが、実はおおありなのである。

ここで少しサンクの話をしなければならない。 ここでいうサンクとは、非互換のものの間をとりもつ仕掛けで、フォーマットの変換を行って両者にとって意味が通るものにする、というようなものである。

メッセージにも A/W のタイプがある。 一番分かりやすいのは、WM_SETTEXT メッセージだろう。 これはウィンドウテキストを設定するためのメッセージだが、指定された文字列は ANSI かもしれないし、Unicode かもしれない。 SendMessageA が呼ばれたなら ANSI 文字列、SendMessageW が呼ばれたなら Unicode として扱うわけである。

先に述べたように、SendMessage は場合によっては直接ウィンドウプロシージャのコールバックで処理されることもある。 しかし、フックがかかっているとか A/W の変換が必要であるとか、他のスレッドにメッセージを送る場合などは、単純なコールバックでは済まされない。 他のスレッドにメッセージを送る場合を考えれば明白であろう。 少なくともコンテクストスイッチは必要である。 さらに、他のスレッドは他のプロセスに属する場合もある。 NT 系 OS ではプロセスは相互に不可視なため、メッセージの受け渡しに利用可能な共通の記憶領域というものは存在しない。 ではどうするか。 NT 4 以前の OS では、クライアント・サーバのサーバプロセスである CSRSS に LPC を通じて処理を委託する形をとっていた。 CSRSS 内部には全ての GUI プロセス・スレッドを管理するテーブルがあり、メッセージの送出・受け取りはサーバプロセス経由で行っていたのである。

NT 4 以降の OS では、周知の通りクライアント・サーバ型をやめ、従来サーバプロセスで管理していたテーブルはカーネルモードへ移された。 したがって、スレッドをまたぐ SendMessage の処理はユーザモードだけでは完結せず、LPC を使ってサーバに処理を委託する代わりに、実行はカーネルモードへと移され、そこでテーブルの更新を行う。 この方式の利点はまず第一にパフォーマンスである。 LPC を使ったクライアント・サーバ処理方式では、テーブルの更新を行うだけでもプロセスのコンテクスト・スイッチが必要となる。 これは、ユーザモードからカーネルモードへダイブすることに比べると相当高価な処理となる。 もうひとつの利点は、カーネルモードからはユーザモードのメモリがアクセスし放題ということ。 制約はあるが、カーネルモードからは、メモリマッピングのコンテクストを一時的に切り替えることで、他のプロセスのメモリを参照することも可能である。 クライアント・サーバ型ではメモリの共有を明示的に行わなければできなかった広範囲のメモリ参照が、カーネルモードドライバではごく簡単に当たり前のように行うことができる。 というのは、NT のメモリ管理がそういう風にできているからなのだが。

さて、カーネルモードからしてみると全てのユーザモードメモリは信頼できないメモリ領域である。 したがって、実質的な処理を始める前に、なにはともあれ入力データのコピーを作ることからはじめなければならない。 このとき、もとのデータが ANSI であれば、送り先の属性を見て必要なら Unicode に変換される。 もとのデータが Unicode で送り先が ANSI でも同じような変換が行われる。 予断だが、このときに用いられるコードページは システム ANSI CodePage (ACP) と呼ばれる。

この他、WM_COPYDATA などは当然のようにデータのコピーを作らなければならない。 なぜなら、送り元と受け取り先で共通にアクセスできるのはカーネルモードのメモリだけであり、繰り返しになるがユーザモードのメモリは信頼できないからである(いつ書き換えられても、 あるいはヒープからフリーされ ても不思議はない)。 そのために必要な処理を呼称していわゆる「サンク」ということになる。

このようなメッセージサンクは、メッセージの持つ引数(WPARAM と LPARAM)によって専用の処理が必要となる。

センドメッセージ総括

メッセージが Send されるとき、送り側はブロックされ、処理結果を LRESULT の形で受け取ることができる。 中には、バッファへのポインタを渡して処理結果を格納してもらうものもある。 受ける側が同じスレッドで ANSI / Unicode 変換の必要が無く、フックも存在しない場合、Send されたメッセージの処理はあて先 HWND のウィンドウプロシージャを直接呼び出すことで行われる。 それ以外の場合、処理はカーネル側へ持ち越される。 カーネルモードでは、センドメッセージ構造体をアロケートし、送り側と受ける側のスレッド構造体へリンクする。 受ける側にすでにセンドされたメッセージがあれば、鮮度メッセージ構造体はそのリストの最後へ追加され、順に処理される。

センドされたメッセージが受ける側で処理されるタイミングはいくつかあり、代表的なものに GetMessage で待たされている間、SendMessage でブロックされている間、などがある。

ポストメッセージ総括

ポストされたメッセージは必ずメッセージキューを介してアプリケーションへ届く。 PostMessage はスレッドをブロックせず、メッセージをキューへ登録するとすぐに戻る。 ポストされたメッセージは、GetMessage や PeekMessage などの API を呼び出すことで取得できる。 メッセージキューにはステータスがあり、API で取得可能だが、一度閲覧されたステータスはリセットされるので注意が必要。 GetMessage はポストされたメッセージだけではなく、インプットメッセージも取り出す。 インプットメッセージはポストされたメッセージより優先順位が低い。 特殊なケースでは、複数のスレッドのインプットメッセージキューが合体する・させることができる。

ウィンドウ HWND の上位構造

デスクトップ

NT 系の OS には、GUI セキュリティバウンダリとしてデスクトップとウィンドウステーションがある。 デスクトップとは、画面で普段目にするウィンドウが集まる単位とでも言おうか。 普通に OS を使っているとき、少なくとも二つのデスクトップを目にしている。 ひとつはデフォルトデスクトップと呼ばれる、アプリケーションが動作するデスクトップ。 もうひとつは、ログオンダイアログや "Welcome" 画面の見られるデスクトップ。

デスクトップ内ではウィンドウツリーは割と自由に変更できるが、デスクトップを越えての移動はできない。 また、デスクトップを越えてウィンドウメッセージを送ったり、HWND を評価することはできない。 デスクトップは NT Executive のオブジェクトであり、アクセス管理の ACL を設定することができる。 ほとんど目にしたことは無いがデスクトップを作成することもでき、他のウィンドウからの干渉を受けたくない場合には有効である。.pe

もうひとつ目にすることがあるのは、スクリーンセーバ用のデスクトップだろう。 セキュアなスクリーンセーバを使う設定になっているとき、スクリーンセーバは専用に作られたデスクトップ上で動作する。

ウィンドウステーション

これらのデスクトップのさらに上位に位置するのがウィンドウステーションである。 ウィンドウステーションには、大きく分けて I/O ウィンドウステーションと Non I/O ウィンドウステーションの2種類がある。 後述するセッションには高々ひとつだけ I/O ウィンドウステーションが存在できる。 I/O ウィンドウステーションとは、文字通り Input / Output を行うウィンドウステーション。 つまり、セッションにはひとつだけ、入力を受付画面に出力することのできるウィンドウステーションがある。 先に述べた二つのデスクトップも、この I/O ウィンドウステーションに属する。

では Non I/O ウィンドウステーションは何のために用意されているのだろう。 その答えは「サービス」である。 サービスは入出力の必要はない。 Non I/O ウィンドウステーションはセッションにいくつでも存在できる。 サービスが動作する LUID に応じて Non I/O ウィンドウステーションが作られ、それぞれ GUI 的に隔離された空間に置かれる。

セッション

ウィンドウステーションの上位に位置する、セッションというものがある。 セッションが導入されたのは Windows 2000 からで、ターミナルサービスが動作するために不可欠なものとなっている。 実は I/O ウィンドウステーションをいくつも作ることでマルチユーザを実現しようとしたようなのだが、GUI システムによる対処だけではアプリケーションコンパチビリティを完全には満足できないことが分かり、今のセ ッションモデルを採用した、といううわさもある。

セッションを一言で説明すると、ユーザモードプロセスのメモリ空間がメモリマネージャによって分離・独立しているように、カーネルモードにおけるドライバの分離・仮想化とでもいえる。 カーネルモードにはセッションに属するメモリ空間と、セッションに属さない中立のメモリ空間が存在する。 後者にはファイルシステムドライバなどが含まれ、前者には GUI を主とするドライバ群が含まれる。

で、具体的にはどうよ

GUI プロセスは、いずれかのセッションに属することになる。 TS が無効にされているシステムでも、起動時に用意されるセッションが少なくともひとつは存在する。

プロセス構造体にはメモリのマッピング情報が付随するが、カーネルモードのアドレス空間の一部分が所属するセッションによってごっそり変わっていることになる。

それぞれのセッションでは、それぞれの GUI システムが動作している。 これらはお互いに不可知に近く、特殊な操作を行わなければ不可視である。 SendMessage や PostMessage では他のセッションにウィンドウメッセージを送ることもできない。 同じ値の HWND が複数のセッションで使われる可能性も非常に大だが、それぞれ全く別のウィンドウを指しているのはいうまでもない。

オブジェクト プレフィクス

ここまでの話だけで住むならおそらくはウィンドウステーションの延長でもなんとかなっただろうが、オブジェクトのコンフリクトという話になるとこれはどうもいけない。 セッションが導入される前に書かれたアプリケーションは、セッション導入後でもきちんと動作しなければならない。 Windows 2000 以降に書かれたアプリケーションでも、セッションを全く意識していないのが普通だ。 ましてや Windows 2000 以前に書かれたアプリケーションにとっては、セッションなど知ったことではない。

複数のセッションが作成されそれぞれのセッションで同じアプリケーションが走っているケースを考えてみよう。 さて、この複数のアプリケーションがミューテックスで二重起動防止を行っているとしたらどうだろう。 GUI システムと違い、オブジェクトマネージャは基本的にはセッション中立である。 何らかの予防策をとらないと、セッションの間で(お互い不可視なのにもかかわらず)オブジェクトのバッティングが起きてしまうのである。

これを防ぐため、オブジェクトマネージャで管理されるオブジェクトには、暗黙のうちにセッションごとのプレフィクスが付けられ、名前の衝突を回避している。 実際にはもそっと複雑なのだが、非常に大雑把に言うとこうなる。

セッションをまたいで共通なオブジェクトは、ある特殊なプレフィクスを付けることでいまだに作成可能。

キーボード

スキャンコード

PC で使われるインタフェースでは、I/O チップを通った後の

バーチャルキーコード

USB

キーボードレイアウト

インプットロケール

日本語キーボードレイアウト

レイヤード構造

ダイナミックレイアウトスイッチング

キーボードの ID

NtとZw

Nt

Zw

Validation

User mode

User mode
- or Kernel mode

Table

User mode

User mode (default)
or Kernel mode
(OBJ_KERNEL in ObjAttr)

ZwClose assumes kernel mode.

To be continued...

Since 1996

一つ上へ

ホーム  ざれごと  ワシントン州  ツール  NT豆知識  Win32プログラミングノート  私的用語  ジョーク  いろいろ  ゲーム雑記  Favorites  掲示板   Mail