ソケット、パイプ、共有メモリ…IPC(プロセス間通信)の最適な手段を模索してみた

ソケット、パイプ、共有メモリ…IPC(プロセス間通信)の最適な手段を模索してみた

先月からプログラマをしているヤスです。

プログラマに転職し、現在携わっている案件が、Linuxで動作する定型データの送受信アプリケーションの開発です。

仕様的には、そんなに難しくない案件ですが、何分プログラマとしての経験値が低いことに加え、Linuxでのプログラミング経験がないため、四苦八苦しながら開発を進めています。

この案件ですが、同一のアプリケーション内で機能毎にスレッドを生成し、スレッド間でスレッドの状態や受信したデータなどをやり取りする仕様になっています。

Linuxプログラミングについて何も知らない私なので、もちろんスレッド間通信の実現方法も分かりませんでした。

そこで、スレッド間通信の実現方法について調べてみました。

IPC(プロセス間通信)

スレッド間通信は、IPC(Inter-Process Communication : プロセス間通信)に該当します。

IPCは、同一のコンピュータ上で動く複数プロセス間(または複数スレッド間)でデータ交換を行う仕組みです。

プロセス間通信の実現には、幾つかの方法があり、私が調べたところ、下記の3種類の方法がよく使われているようです。

  1. ソケット
  2. パイプ
  3. 共有メモリ

ソケット、パイプは、ファイルと同じようにディスクリプタで管理されます。そのため、writeやreadなどの関数にディスクリプタを渡せば、ファイルと同じように使用できます。

共有メモリは、正確には通信ではありません。もちろん、ディスクリプタも使用しません。

ソケットやパイプとは、多少異なるプロセス間通信と言えます。

ところで、上記3種類の手法には、どんな違いがあるのでしょうか?

このままでは、どの手法を使って実装すれば良いのか分からないので、各手法の特徴と差異について調べました。

ソケット

ソケットは、1つのディスクリプタで双方向通信を実現します。

各プロセスでソケットを1つずつ作成し、そのソケットを介してプロセス間通信を行います。

先に述べたように、ファイルと同様にディスクリプタで各ソケットを管理されており、このディスクリプタを用いてバイトストリームの送受信を行います。

受信したバイトストリームは、ソケット作成時に同時に作成されたバッファに一度格納され、受信側のソケットで読み込みが行われる度にバッファからデータが取り出されます。

バッファがあるため、送信側と受信側のタイミングが異なっても通信を行うことができます。

また、ソケットは、プロセス間通信だけではなく、通常の外部機器とのネットワーク通信にも使用されます。

プロセス間通信およびネットワーク通信のプロトコルとして、UDP/IPとTCP/IPが使用できます。

TCP/IPの場合、クライアント/サーバモデルになるため、ソケット通信においてもクライアント側とサーバ側に分かれます。

UDP/IPと違い、TCP/IPの場合、ソケットの使い方が多少煩雑になりますが、その代わりにプロセス間通信においても信頼性のある通信を確立できます。

パイプ

パイプは、ソケットが現れる前から使われているプロセス間通信の1つです。

基本的には、ソケットと同じようにディスクリプタで接続を管理し、データの送受信を行います。

ソケットと異なる点は、1つのディスクリプタで提供可能な通信が単方向のみであることです。そのため、双方向通信を行うためには、2つのディスクリプタを用意する必要があります。

しかし、パイプの使い方は、ソケットよりも簡素になっており、ソケットに比べて簡単に扱えるため、プロセス間通信でよく使われているようです。

パイプもソケットと同様に受信バッファを持っており、これにより、送受信のタイミングが一致しなかったとしても問題なく通信が行えます。

しかし、バッファが満杯になってしまった時へのバッファへの書き込み、バッファが空の時のバッファからの読み込みを行うと、読み書きができるまでプロセスをブロックしてしまいます。

共有メモリ

共有メモリは、ソケットやパイプと異なり、通信を行うものではありません。

複数のプロセス間で共有可能なメモリを用意し、そのメモリを介してプロセス間でのデータ交換を実現します。

通信を行うわけではなく、共有のメモリを参照するだけなので、ソケットやパイプ生成のためのシステムコールを読み出したり、通信のためのデータのシリアル化が不要になります。不要になった処理を排除できる分、処理を軽くできます。

また、ソケットやパイプと違い、共有メモリ上のデータは意図的に削除しない限り、永続的に残ります。

しかし、共有メモリは取り扱いが難しく、アクセス時のミューテックスや、メモリ書き込み時のオーバーフロー、アドレス管理など、実装時の注意が多くあります。

実装が簡易、処理が軽いというメリットがありますが、取り扱いを間違えるとセグメンテーション違反などの致命的なエラーを起こしやすいというデメリットにもよく注意が必要です。

各プロセス間通信の比較

ここまでに各プロセス間通信の特徴を述べましたが、最適なプロセス間通信はどの方法になるのでしょうか?

プロセス間通信の選択に役立つ情報として、下記のような資料があります。

Evaluation of Inter-Process Communication Mechanisms

この資料では、各プロセス間通信のレイテンシやスループットが送受信データ量によってどう変化するかを確認しています。

これによると、レイテンシ、スループットともに優れている手法が共有メモリだということが分かります。

一方、ソケットとパイプは、送受信データ量が多いほどレイテンシが大きくなり、スループットは、ある送受信データ量から変化がないことが分かります。

パイプとソケットを比較すると、若干ですがソケットの方がレイテンシが大きく、スループットが低いことが確認できます。

以上の情報から最適なプロセス間通信は何かを考えると、どれが最適かというのは、ケースバイケースになるでしょう。

例えば、処理速度が最優先のプログラムの場合、共有メモリを用いることが最適でしょうし、処理速度よりも安全性を求めるならば、ソケットやパイプになるでしょう。

経験値の高いプログラマであれば、共有メモリで実装するのがベストでしょう。

仕事であれば、チームで実装することが多いと思いますので、メンバーの実力を考慮して決める必要があります。

このように、状況を鑑みて、どれを使うのかを選択しましょう。