ハンドルの継承とマルチスレッド

今日は子プロセス問題のWindows版についてです。


Windowsで子プロセスと標準入出力を使って通信するためには、通常、

  1. 親プロセスが無名パイプを作成する
  2. パイプの一端のハンドルを「継承可能」にする
  3. STARTUPINFO構造体のhStdInput等に継承可能にしたパイプのハンドルを指定して、CreateProcess()を呼ぶ
  4. 継承可能にしたパイプのハンドルを閉じる
  5. パイプの閉じていない方のハンドルで子プロセスと通信をする

という手順を踏みます。


Windowsでは、ファイルやパイプ等のカーネルオブジェクトのハンドルが既定では継承可能になっていないため、誤って不要なハンドルをコプロセスに継承させてしまうことは少なくなっています。
確かに、この「必要になるまで継承可能にしない」という仕組みはシングルスレッド環境では上手く機能します。
しかし、マルチスレッドの環境では上記の手順2〜4の間に別のスレッドが同様の処理を行ってしまうと、コプロセスは余計なハンドルまで継承してしまいます。


この問題は、MicrosoftのKBにも載っています。
http://support.microsoft.com/kb/315939/ja
このKBでは、

  • 上記の手順2〜4をクリティカルセクションに置く
  • 標準入出力のハンドルだけを継承する中間アプリを経由させて実際のプロセスを起動する

という解決策が示されていますが、どちらも完全な解決策とはいえません。
実は、Windows VistaのCreateProcessにはこの問題のための拡張が施されています。
しかし、MSDNのドキュメントがいい加減なので調査した結果を残しておきます。


VistaのCreateProcess()はEXTENDED_STARTUPINFO_PRESENTオプションを指定することで、STARTUPINFOの代わりにSTARTUPINFOEXを渡すことができます。

typedef struct _STARTUPINFOEX {
    STARTUPINFO StartupInfo;
    PPROC_THREAD_ATTRIBUTE_LIST lpAttributeList;
} STARTUPINFOEX;

このlpAttributeListメンバに継承したいハンドルを設定するわけです。


lpAttributeListに渡すデータはInitializeProcThreadAttributeList()で作成します。

BOOL WINAPI InitializeProcThreadAttributeList(
    LPPROC_THREAD_ATTRIBUTE_LIST lpAttributeList,
    DWORD dwAttributeCount,
    DWORD dwFlags,  // 常に0
    PSIZE_T lpSize
);

InitializeProcThreadAttributeList()はよくある「1回目の呼び出しで必要なバッファサイズを取得して、2回目でバッファにデータを受け取る」タイプの関数です。
使い方はこうです。(説明を楽にするため、C++でなくC)

DWORD count = 3;                    // 属性の数
SIZE_T buf_size;                    // バッファサイズ
LPPROC_THREAD_ATTRIBUTE_LIST buf;   // バッファ

// 注: この呼び出しはFALSEを返す
// GetLastError() == ERROR_INSUFFICIENT_BUFFER
InitializeProcThreadAttributeList(NULL, count, 0, &buf_size);

buf = (LPPROC_THREAD_ATTRIBUTE_LIST)malloc(buf_size);
InitializeProcThreadAttributeList(buf, count, 0, &buf_size);

ドキュメントではdwAttributeCountの意味があいまいですが、0〜3までの値が有効で、おそらく継承させたいハンドルの最大数だと思います。


このバッファにハンドルを設定するにはUpdateProcThreadAttribute()を使います。

BOOL WINAPI UpdateProcThreadAttribute(
    LPPROC_THREAD_ATTRIBUTE_LIST lpAttributeList,
    DWORD dwFlags,          // 常に0
    DWORD_PTR Attribute,    // PROC_THREAD_ATTRIBUTE_HANDLE_LIST
    PVOID lpValue,          // 継承させたいHANDLEの配列へのポインタ
    SIZE_T cbSize,          // lpValueのバイト数
    PVOID lpPreviousValue,  // 常に0
    PSIZE_T lpReturnSize    // 常に0
);

lpValueに継承させたいHANDLEの配列を指定するのですが、この配列にはコンソール入力バッファとコンソールスクリーンバッファのハンドルを含めてはいけません。
おそらく、コンソールを継承するかどうかはCreateProcess()のオプションで指定するからだと思います。
このことに気付くのにかなり時間がかかりました。
コンソールのハンドルを含めていると、lpAttributeListの内容は無視されてしまうようです。
また、継承可能になっていないハンドルが含まれている場合はエラーになります。


Hamigaki.Processではハンドルがコンソールのものかの判定を次のような関数で行っています。

bool is_console(::HANDLE h)
{
    ::DWORD mode;
    return ::GetConsoleMode(h, &mode) != FALSE;
}

これで本当によいのかは不明です。


使い終わったバッファは次の関数で後始末をします。

VOID WINAPI DeleteProcThreadAttributeList(
    LPPROC_THREAD_ATTRIBUTE_LIST lpAttributeList
);

バッファのメモリ自体はfree()などで別途解放する必要があります。
あと、UpdateProcThreadAttribute()で設定した配列はDeleteProcThreadAttributeList()するまで残しておかないといけません。


以上の調査から、Hamigaki.Processに不要なハンドルを継承させない修正を追加しました。
child.cppの差分
もちろん、Windows Vistaで動かさない限り、不要なハンドルが継承される恐れは残ります。