プロセス間通信の概要
オペレーティングシステムにおいて、プロセス間通信(IPC)とは、異なるプロセス間での情報共有、データ転送、メッセージ通知などのインタラクションを指します。各プロセスは生成時に独自の仮想アドレス空間を持ちますが、カーネル空間は共有します。そのため、プロセス間の通信を実現するには、カーネルを仲介する必要があります。
Linuxシステムでは、パイプ、メッセージキュー、共有メモリ、セマフォ、シグナル、ソケットなど、多様なIPCメカニズムが提供されています。これらのメカニズムにより、プロセス間でのデータ共有、メッセージ転送、同期・通信が可能になります。
パイプ通信
パイプはIPCメカニズムの一つであり、あるプロセスの出力を直接別のプロセスの入力として接続します。Linuxでは、コマンドの出力を別のコマンドに渡して処理するために使用されます。
docker ps -a | grep nginx
Linuxを操作する際、このようなコマンドでnginxコンテナを確認する機会は多いでしょう。コマンド内の「|」がパイプ命令ですが、これは匿名パイプであり、使用後即座に破棄されます。匿名パイプは親子関係にあるプロセス間でのみ通信可能です。あるプロセスの出力を直接別のプロセスの入力とするため、nginxプロセスのみが表示されるのです。この方式は単方向の通信です。
匿名パイプに対し、名前付きパイプ(FIFO)も存在します。これは先入れ先出し(First In First Out)のデータ転送方式に由来します。名前付きパイプは読み書き2つのポートを持ち、プロセスはパイプファイルを開いて読み書きを行えます。あるプロセスがパイプにデータを書き込むと、別のプロセスがそこからデータを読み取れます。
名前付きパイプを使用するには、まず「mkfifo」コマンドでパイプを作成し、名前を指定します:
$ mkfifo channel_pipe
channel_pipeはパイプ名です。Linuxの「すべてはファイルである」という原則により、パイプもファイルとして存在します。lsコマンドで確認すると、このファイルのタイプはp(pipe)と表示されます。
次に、channel_pipeという名前のパイプにデータを書き込みます:
$ echo "Hello IPC" > channel_pipe
書き込み操作を実行後、コマンドがそこで停止するかもしれません。これはパイプ内のデータがまだ読み取られていないためです。パイプ内のデータが完全に読み取られると、コマンドは正常に終了します。したがって、別のコマンドを実行してパイプ内のデータを読み取る必要があります:
$ cat channel_pipe
Hello IPC
パイプの内容が正常に読み取られ、ターミナルに表示され、echoコマンドも正常終了したことがわかります。
匿名パイプの通信範囲は親子関係にあるプロセスに限定されます。パイプ自体に実体(パイプファイル)がないため、forkによる親プロセスのファイル記述子のコピーを通じてのみプロセス間通信を実現できます。
シェルでA | Bコマンドを実行する際、AプロセスとBプロセスは両方ともシェルが生成した子プロセスです。AとBの間には親子関係がなく、両方の親プロセスはシェルです。
一方、名前付きパイプは関係のないプロセス間での通信を可能にします。これは名前付きパイプが特定のタイプのデバイスファイルを事前に作成し、プロセスがそのデバイスファイルを使用するだけでプロセス間通信を実現できるためです。
メッセージキュー
メッセージキューはIPCメカニズムの一つであり、パイプよりも高い効率と柔軟性を提供します。メッセージキューはカーネル内にメッセージリンクリストを作成することで実現され、プロセスはデータをキューに格納し、他のプロセスがそこからデータを読み取れます。
例えば、プロセスXがプロセスYにメッセージを送信する必要がある場合、プロセスXはデータをYプロセスに対応するメッセージキューに格納した後、正常に戻ることができます。プロセスYは必要な時にデータを読み取れます。同様に、プロセスYがプロセスXにメッセージを送信する必要がある場合も、同じ方式で操作できます。
パイプとは異なり、メッセージキューはフォーマットされており、各メッセージは固定サイズのストレージブロックです。プロセスがデータを読み取る際には、メッセージのデータ形式を事前に合意しておく必要があります。メッセージキューの利点はプロセス間の非同期通信をサポートすることにあり、送信側と受信側が同時に実行されている必要がありません。メッセージはキュー内で相手の読み取りを待機できます。パイプがフォーマットされていないバイトストリームであるのとは異なります。プロセスがメッセージキューからメッセージを読み取ると、カーネルはそのメッセージを削除します。
メッセージキューのライフサイクルはカーネルに関連しており、明示的に解放したりオペレーティングシステムをシャットダウンしたりしない限り、メッセージキューは永続的に存在します。一方、パイプのライフサイクルはプロセスの生成と終了に伴って動的に確立・破棄されます。
しかし、メッセージキューには欠点もあります。データがユーザーモードとカーネルモード間でコピーされるため、メッセージキュー通信プロセスには一定のオーバーヘッドが存在します。プロセスがメッセージキューにデータを書き込む際、データをユーザーモードからカーネルモードにコピーする必要があり、別のプロセスがメッセージキューからデータを読み取る際、データをカーネルモードからユーザーモードにコピーする必要があります。このデータコピーのオーバーヘッドが通信効率に影響します。
共有メモリ
共有メモリは効率的なIPCメカニズムであり、複数のプロセスが同じメモリ領域を共有できるようにし、データコピーのプロセスを回避して通信速度を向上させます。
共有メモリメカニズムでは、オペレーティングシステムが共有メモリ領域を複数のプロセスの仮想アドレス空間にマッピングし、それらが同じ物理メモリに直接アクセスできるようにします。こうすることで、あるプロセスが共有メモリに書き込んだ操作を、他のプロセスは即座に更新されたデータとして確認でき、データのコピー転送を行う必要がありません。
共有メモリはデータコピーを行わないため、プロセス間通信プロセスにおいて低いオーバーヘッドと高い転送速度を持ちます。しかし、共有メモリメカニズムでは、競合状態やデータ不整合の問題を避けるために、同期メカニズムを使用して複数プロセス間のデータ一貫性を保証する必要があります。