hozugawa.net · A neutral informational page

CreateNamedPipeで実現する名前付きパイプによるプロセス間通信

Windows環境でプロセス間通信を実装する場合、名前付きパイプは非常に有力な選択肢です。ローカルマシン上での動作に特化しており、ACLやImpersonationといったWindows固有の仕組みと高い親和性があります。CreateNamedPipe関数は、サーバー側エンドポイントを作成し、続く接続要求を待機する役割を担う中心的なAPIです。

名前付きパイプは \\.\pipe\<パイプ名> という形式で識別され、CreateFile、CallNamedPipe、TransactNamedPipeなどのクライアント向けAPIと組み合わせることで双方向通信を構築できます。バイトストリームとメッセージの両モードをサポートし、用途に応じて使い分けられる柔軟性があります。本記事では、基本的な仕組みから同期・非同期I/Oの選択基準、運用時の注意点までを順に整理します。

名前付きパイプの基本概念と仕組み

名前付きパイプはUNIX系OSのFIFOに由来する仕組みですが、Windows APIではさらに多くの拡張が提供されています。ACLによる厳密なアクセス制御、サーバー側・クライアント側での非対称な権限設定、トランザクション単位のI/O、メッセージ境界の保持といった機能を備えます。サービスとログオン中ユーザーのGUIアプリのように、セッションが異なるプロセス間でも問題なく通信できる点は大きな利点です。

CreateNamedPipeを呼び出すたびに1つのインスタンスが生成されます。1つのパイプ名に対して複数のインスタンスを並べることで、複数のクライアントからの同時接続を処理できます。インスタンス数の上限はPIPE_UNLIMITED_INSTANCESで事実上無制限にできますが、スレッド数やハンドルリソースの制約を受けるため、想定同時接続数に応じた適切な値を設定することが推奨されます。

通信モードにはバイトストリームとメッセージがあります。バイトストリームはTCPと同様に単なるバイト列として扱われ、書き込みの境界が保持されません。メッセージモードは1回のWriteFileがそのまま1回のReadFileに対応し、長さも保持されるため、独自プロトコルのパース処理を簡潔に書けます。クライアント側のCreateFile時に指定するフラグの組み合わせによって、サーバー側と異なる読み取りモードを要求できる点にも注意が必要です。

CreateNamedPipe関数のシグネチャとパラメータ

CreateNamedPipeはwinbase.hで定義されており、引数は全部で8つあります。lpNameには必ず \\.\pipe\ プレフィックスを付け、続いて任意のパイプ名を指定します。パイプ名はASCII英数字とアンダースコアで構成しておくと、サービス運用時にも扱いやすいでしょう。dwOpenModeでは、PIPE_ACCESS_INBOUND/OUTBOUND/DUPLEXのいずれかを指定し、必要に応じてFILE_FLAG_OVERLAPPEDやFILE_FLAG_FIRST_PIPE_INSTANCEを組み合わせます。

FIRST_PIPE_INSTANCEはそのパイプ名でインスタンスがすでに存在するかを保証するため、シングルトンが必要なサービスプロセスで威力を発揮します。dwPipeModeでは、PIPE_TYPE_BYTE/MESSAGE、PIPE_READMODE_BYTE/MESSAGE、PIPE_WAIT/NOWAITをビットORで渡します。nOutBufferSizeとnInBufferSizeに0を渡すと、システムは既定のバッファサイズを採用します。想定リクエストサイズに対してやや大きめに設定しておくと、書き込みが分割されにくくなります。

lpSecurityAttributesにNULLを指定すると、デフォルトのセキュリティ記述子が適用され、Administratorsと作成ユーザー自身のみが接続可能になります。複数ユーザーが同一マシンにログオンしている環境や、サービスからユーザーアプリへの通知を受け付けるようなケースでは、SECURITY_ATTRIBUTESを構築して明示的にACLを緩める、もしくは特定のSIDのみを許可する形に書き換える必要があります。

サーバー側とクライアント側の接続シーケンス

サーバー側は、CreateNamedPipeでインスタンスを生成し、ConnectNamedPipeでクライアントの接続を待ち、ReadFileとWriteFileで送受信を行い、切断後にDisconnectNamedPipeでクリーンアップ、再びConnectNamedPipeで次の接続を待つ、というループ構造になります。ConnectNamedPipeは既定では接続が確立するまでスレッドをブロックしますが、FILE_FLAG_OVERLAPPED付きで作成した場合はOVERLAPPED構造体を渡して非同期に呼び出せます。

クライアント側は、\\.\pipe\myPipe をCreateFileで開くだけで接続が完了します。アクセスモードには0(読み取り)、GENERIC_WRITE(書き込み)、GENERIC_READ|GENERIC_WRITE(読み書き)のいずれかを指定します。サーバーがFILE_FLAG_OVERLAPPEDを使っている場合、クライアント側でもFILE_FLAG_OVERLAPPEDを付ける必要があり、どちらか一方が欠けると接続時にエラーになる点に注意してください。

ConnectNamedPipeがERROR_PIPE_CONNECTEDを返すケースがあります。これはクライアントがすでに接続を完了しており、サーバー側が後から「接続を待つ」操作に入った状態で発生します。エラー扱いせず正常系のひとつとして処理し、ループの先頭でFALSEが返った場合はGetLastErrorで値を確認するように実装しておくと、確実な動作を期待できます。アイドル状態のインスタンスを長時間保持するとハンドルがリークするため、タイムアウトを設けてDisconnectNamedPipeで明示的に切断するのが安全です。

セキュリティ記述子とImpersonation

CreateNamedPipeで生成したハンドルは、CreateFileで取得したクライアントハンドルと対になり、双方のプロセスから独立したカーネルオブジェクトとして扱われます。インスタンスの一覧は \\.\pipe\ 以下のオブジェクトディレクトリで確認でき、デバッグ時の手掛かりにもなります。サードパーティ製のツールでも列挙可能な点から、ACL設計は慎重に行うべきです。

セキュリティ記述子を細かく制御するには、InitializeSecurityDescriptorとSetSecurityDescriptorDaclでLPSECURITY_ATTRIBUTESを構築します。サービスからユーザーアプリへ通知を送りたい場合はWorld権限にFILE_GENERIC_READを許可し、逆にユーザーがサービスへ書き込むだけ許可したい場合はService SIDのみにFILE_GENERIC_WRITEを与えるといった形で、方向ごとにACLを分けるのが定石です。

Impersonationも重要な機能です。サーバー側でImpersonateNamedPipeClientを呼び出すと、その時点以降のアクセスが接続元クライアントのユーザー権限で行われたかのように振る舞います。ファイル操作やレジストリ操作をクライアント権限で行えるため、特権昇格を防ぎつつ強力なサンドボックスを構築できます。不要になった時点でRevertToSelfを呼ぶことを忘れてはなりません。重い描画処理を子プロセスに分離するアーキテクチャでは、CreateCompatibleDCでのビットマップバッファ作成を担当するワーカーと、GUI側をCreateNamedPipeで結線する形が定番です。

同期・非同期とオーバーラップI/O

ファイルAPIとしての性格が強い名前付きパイプでは、ReadFileとWriteFileをブロッキングモードで使うのが最も手軽です。ただし、サーバー側で複数の接続を同時に捌きたい場合は、スレッドごとにインスタンスを1つずつ割り当てるモデルか、オーバーラップI/OとI/O完了ポート(IOCP)を組み合わせるモデルのいずれかを選ぶことになります。前者は単純ですがスレッド数に比例してスタックを消費し、後者は複雑ですが高いスケーラビリティを発揮します。

オーバーラップI/Oを使うには、CreateNamedPipe時にFILE_FLAG_OVERLAPPEDを指定し、ReadFileとWriteFileにOVERLAPPED構造体へのポインターを渡します。完了通知にはイベントシグナル、コールバック関数、Alertable I/O、I/O完了ポートの4通りがありますが、パイプの場合はIOCPとの相性が最も良好です。CreateIoCompletionPortで関連付けた後、GetQueuedCompletionStatusで完了を待機すれば、少数のワーカースレッドで多数のパイプを捌けます。

非同期特有の落とし穴として、CancelIoとFlushFileBuffersの使い分けがあります。読み取りを中断したい場合はCancelIo、ハンドルを閉じる前に未書き込みデータをすべて送り出したい場合はFlushFileBuffersを使います。CloseHandleを呼び出した瞬間にERROR_BROKEN_PIPE相当の状態になるクライアントもあるため、サーバー側ではDisconnectNamedPipe→CloseHandleの順で必ず処理する流れを徹底してください。

実装時の注意点とデバッグ手法

運用に入った段階で遭遇しやすいのが、ERROR_BROKEN_PIPE(対向プロセスが予期せず終了)、ERROR_PIPE_BUSY(全インスタンス使用中)、ERROR_ACCESS_DENIED(ACL不一致)です。原因切り分けには、クライアント側でWaitNamedPipeを短いタイムアウトで呼ぶポーリングロジックが有効です。サーバー側ではPIPE_NOWAIT+ポーリング、ConnectNamedPipeのリトライ、タイムアウトの3つを状況に応じて使い分けます。

GUIデバッグでは、パイプの通信内容とウィンドウ状態を同時に確認したい場面がよくあります。クライアント領域の座標取得を実施し、その矩形内にデバッグ用のオーバーレイ描画を行うと、通信ログと画面表示を並べて検証できます。プロファイラやETWトレースと併用すれば、ボトルネックの特定が格段に速くなります。

プロトコルの複雑さが増してきた段階で初めてTCPやUnixドメインソケットへの移行を検討し、それまでは名前付きパイプでシンプルに作り込む、という戦略が現実的です。デバッグビルドでは通信内容をテキストログにダンプし、リリースビルドではバイナリに切り替える二段構えのロギングは、開発と運用の両面で効きます。

項目 同期I/O 非同期I/O
実装の容易さ 高い 中程度
スケーラビリティ スレッド数に依存 IOCPで多数同時処理
デバッグ ブレークポイントと好相性 コールスタックが複雑化
リソース消費 スレッドスタックを消費 ハンドルとオーバーラップ構造体
適用シーン 管理ツール、少数接続 常駐サービス、デーモン

推奨されるパラメータ設定の指針

運用中に発生しやすいエラーと対処

名前付きパイプは、適切に使えばローカルIPCの信頼性とパフォーマンスを大きく引き上げます。ソケットほどの汎用性はありませんが、ACLやImpersonationといったWindows固有のセキュリティ機構と直結しているため、権限分離の厳しいエンタープライズ用途でこそ真価を発揮するAPIです。まずはローカルIPCをCreateNamedPipeで構成し、要件が拡大してきた段階で初めて上位の通信方式へ移行を検討する、という段階的な設計が落とし所になります。