🐙

無名パイプと名前付きパイプ

2024/11/12に公開

パイプとは?

コンピュータ上で動作している異なるプロセス(プログラム)間でデータを受け渡すための仕組み。これを水道管のイメージで考えると、一方のプロセスがデータを「水」として流し込み、もう一方のプロセスがそれを受け取って利用する、というイメージ。
例えば、あるプログラムAの出力を別のプログラムBの入力として使いたい場合、パイプを使うと便利。

パイプには主に「無名パイプ」と「名前付きパイプ」の2種類がある。

無名パイプと名前付きパイプ

無名パイプ

  • 親子関係にあるプロセス間でのみ使用できる。
  • 一時的な通信チャネルで、プロセスが終了すると消える。
  • 単方向の通信のみ可能。
  • ファイルシステム上に実体を持たない。

名前付きパイプ(FIFO)

  • 普通のファイルのように名前を持ち、ファイルシステム上に実体(特殊なファイル)を持つが、実際にはメモリ上でデータをやり取りする。
  • ファイルシステム上に名前を持つため、関係のない複数のプロセス間で使用できる。
  • 永続的に存在し、複数のプロセスで再利用可能。
  • 双方向の通信が可能。

両者の違い

無名パイプは、例えると家族間での会話のようなもので、同じ家に住む家族(親子関係にあるプロセス)同士なら、特別な準備なしですぐに会話(通信)ができる、というイメージ。
名前付きパイプは、電話のようなイメージ。知らない人(関係のないプロセス)とも、電話番号(パイプの名前)さえ分かれば通信できる。また、電話機(ファイルシステム上の実体)が必要。

両者の違いを表でまとめると、以下のようになる。

特徴 無名パイプ 名前付きパイプ
名前 なし あり(ファイルシステム上)
使用可能なプロセス 親子関係のみ 任意のプロセス
存続期間 プロセス終了まで 永続的
通信方向 単方向 双方向
再利用性 不可 可能

無名パイプを使った簡易な親子プロセス間通信

以下に、pythonでのコード例を示す。

無名パイプを使った親子プロセス間通信
import os

# パイプを作成
read_fd, write_fd = os.pipe()

# 子プロセスを作成
pid = os.fork()

if pid > 0:  # 親プロセス
    os.close(read_fd)  # 読み取り側を閉じる
    print("親プロセス: メッセージを送信します")
    message = "こんにちは、子プロセス!"
    os.write(write_fd, message.encode())  # メッセージを送信
    os.close(write_fd)  # 書き込み側を閉じる
else:  # 子プロセス
    os.close(write_fd)  # 書き込み側を閉じる
    print("子プロセス: メッセージを待っています")
    message = os.read(read_fd, 1024).decode()  # メッセージを受信
    print(f"子プロセス: 受け取ったメッセージ: {message}")
    os.close(read_fd)  # 読み取り側を閉じる

上記のプログラムについての説明

  • read_fd, write_fd = os.pipe()でパイプを作成し、読み取り用と書き込み用の2つのファイルディスクリプタを得る。
  • pid = os.fork()で子プロセスを作成する。この時点で、親プロセスと子プロセスの両方がパイプにアクセスできる。
  • 親プロセスは書き込み用、子プロセスは読み取り用のパイプを使う。
  • 以下のように、使わない側は閉じる。
# 親プロセス
os.close(read_fd)  # 読み取り側を閉じる
# 子プロセス
os.close(write_fd)  # 書き込み側を閉じる
  • データの送受信には os.write() と os.read() を使う。Python の文字列をバイト列に変換(encode)したり、その逆(decode)をする必要がある(コードの以下の部分参照)。
#親プロセス
os.write(write_fd, message.encode())  # メッセージを送信
#子プロセス
message = os.read(read_fd, 1024).decode()  # メッセージを受信

上記のプログラムを実行すると、以下のような出力が得られる。

出力結果
親プロセス: メッセージを送信します
子プロセス: メッセージを待っています
子プロセス: 受け取ったメッセージ: こんにちは、子プロセス!


名前付きパイプを使った簡易なクライアント-サーバー間通信

以下に、名前付きパイプを使用する、2つの別Pythonスクリプト(server.pyとserver.py)の例を示す。

スクリプトの使用方法

まず、server.pyを実行する。
次に、別のターミナルウィンドウでclient.pyを実行する。
クライアント側でメッセージを入力すると、サーバー側でそのメッセージが表示される。
これらserver.pyとclient.pyのプログラムを使用することで、異なるプロセス(サーバーとクライアント)間でメッセージをやり取りできる。

設定ファイル (config.json)
{
    "filepath": "data/temp/pipe",
    "max_connections": 5,
    "timeout": 30
}
サーバープログラム (server.py)
import os
import json
import time

# 設定ファイルを読み込む
with open('config.json', 'r') as config_file:
    config = json.load(config_file)

# 名前付きパイプを作成
if not os.path.exists(config['filepath']):
    os.mkfifo(config['filepath'])

print(f"サーバーが起動しました。パイプの場所: {config['filepath']}")

try:
    while True:
        # パイプを開いてメッセージを待つ
        with open(config['filepath'], 'r') as pipe:
            message = pipe.read()
            if message:
                print(f"受信したメッセージ: {message}")
                
                # ここで受信したメッセージを処理する
                # 例: データベースに保存したり、他のクライアントに転送したりする
                
        time.sleep(1)  # CPUの負荷を減らすために少し待つ
except KeyboardInterrupt:
    print("サーバーを終了します。")
finally:
    # プログラム終了時にパイプを削除
    os.remove(config['filepath'])

サーバープログラムについての説明

  • 設定ファイル(config.json)を読み込み、os.mkfifo(config['filepath'])で名前付きパイプを作成する[1]。これはファイルシステム上に特殊なファイルとして現れる。
  • while Trueの無限ループでパイプからのメッセージを待ち受ける。
    ここで無限ループにしているのは、

    • サーバーを常時稼働状態にする
    • 複数のクライアントからの接続を継続的に受け付ける
      ためである。
  • メッセージを受信したら、print(f"受信したメッセージ: {message}")で、メッセージを表示する(実際のアプリケーションではここでメッセージを処理する)。

  • Ctrl+Cを押すと、whileループを抜けてexcept KeyboardInterrupt[2]ブロックに入る。

  • そのあと、finallyブロックでパイプを削除し、プログラムを終了する。サーバーは通常、最後に終了するプロセスなので、他のプロセスがまだパイプを使用している可能性を考慮するため、パイプの削除もサーバー側で行うのが一般的。
クライアントプログラム (client.py)
import os
import json

# 設定ファイルを読み込む
with open('config.json', 'r') as config_file:
    config = json.load(config_file)

print(f"クライアントが起動しました。パイプの場所: {config['filepath']}")

try:
    while True:
        # ユーザーからの入力を受け取る
        message = input("送信するメッセージを入力してください(終了するには'exit'と入力): ")
        
        if message.lower() == 'exit':
            break
        
        # パイプにメッセージを書き込む
        with open(config['filepath'], 'w') as pipe:
            pipe.write(message)
        
        print("メッセージを送信しました。")

except KeyboardInterrupt:
    print("クライアントを終了します。")

サーバープログラムについての説明

  • 設定ファイル(config.json)を読み込む。
  • whileループに入る。
  • message = input("送信するメッセージを入力してください(終了するには'exit'と入力): ")
    の部分で、ユーザーからの入力を受け付ける。
  • 入力されたメッセージをパイプに書き込む。
  • ユーザーが'exit'と入力するまで、この処理を繰り返す。
  • Ctrl+Cを押すと、whileループを抜けてexcept KeyboardInterruptブロックに入り、プログラムは終了する。

プログラムで作成した名前付きパイプについて補足

  • os.mkfifo() で名前付きパイプを作成する。これはファイルシステム上に特殊なファイルとして現れる。
  • 通常のファイル操作(open, read, write)でパイプを扱うことができる。
  • 送信側と受信側は独立したプロセスで、同じパイプ名を使って通信する。
  • 使用後はos.remove()でパイプを削除する。

まとめ

パイプは、異なるプロセス間でデータを効率的に受け渡すための重要な仕組み。無名パイプと名前付きパイプ(FIFO)の2種類があり、それぞれ特徴が異なる。無名パイプは親子プロセス間の一時的な通信に適しており、名前付きパイプはファイルシステム上に実体を持ち、関連のないプロセス間でも通信が可能。両者の違いは、名前の有無、使用可能なプロセス、存続期間、通信方向、再利用性にある。
パイプの特性を理解し、適切に使用することで、効率的なプロセス間通信が実現でき、より柔軟で強力なシステム設計が可能となる。

脚注
  1. サーバーは通常、長時間稼働し続けるプロセスであり、クライアントは、既に存在するパイプに接続することを想定している。このため、名前付きパイプの作成は通常サーバー側で行う。 ↩︎

  2. KeyboardInterruptは、ユーザーがCtrl+Cを押してプログラムを中断しようとした時に発生する例外である。
    この例外をキャッチするのは、

    • プログラムを適切に終了させる
    • 必要なクリーンアップ処理(この場合はパイプの削除)を確実に行う
      ためである。
    ↩︎

Discussion