🦁

【Python入門】名前付きパイプを使ってプロセス間通信を実装してみる

に公開

はじめに

プロセス間通信(IPC)において、データを単方向に渡す方法としてパイプがあります。
パイプは単方向通信に使用され、片方のプロセスが書き込み、もう片方のプロセスが読み込むことによってデータ送信を実現しています。

詳しくはこちらの記事をご参照ください
https://qiita.com/mabo23/items/d461387d4a2555803dac

ただし、os.pipe() で作られる 無名パイプ は、基本的に親子関係のあるプロセス間でしか利用できません。
そこで登場するのが 名前付きパイプ(FIFO) です。

この記事では、名前付きパイプの概要と実際の簡易プログラムを紹介し、その処理の流れを解説します。

名前付きパイプとは

名前付きパイプはファイルシステム上に「名前」だけ存在し、ディスク上にデータを保存しない特殊なファイルです。
データ自体はディスクに保存されず、プロセス間でやり取りされる際に一時的にカーネル内で扱われます。
無名パイプとは異なり、親子関係にないプロセス間でも同じパスを通じて通信できるのが特徴です。

名前付きパイプの仕組み

名前付きパイプは、ファイルシステム上の共通パスを通じて通信を行ないます。

  • 親子関係のあるプロセス → ファイル記述子を共有することで通信
  • 親子関係のないプロセス → ファイル記述子を共有できないため、共通のパスにアクセスして通信

異なるプロセス間でのアクセス方法

複数のプロセスが同じ名前付きパイプを利用するには、共通のパスを共有する必要があります。
その方法としては次のようなものがあります。

  1. 環境変数
    親プロセスが設定し、子プロセスが継承して利用

  2. コマンドライン引数
    別プロセスを起動するときにパスを引数として渡す

  3. ハードコーディング
    あらかじめ決めた同じパスをソースコード内に書いておく

開発環境

  • OS: macOS Sequoia 15.6.1
  • PC: MacBook Pro(M2チップ)
  • エディタ: VS Code
  • 言語: Python 3.13.5
  • 使用ライブラリ: 標準ライブラリのみ(os, json)

ディレクトリ構成

実際に、サーバからクライアントに文字列を送信する簡易プログラムを実装しました。
以下は、そのディレクトリ構成と実装コードです。

ディレクトリ構成
named_pipe/
├── data/
│   └── temp/
├── client.py
├── config.json
└── server.py

実装内容

名前付きパイプへのファイルパス
{
    "filepath" : "data/temp/named-pipe"
}
server.py(データ送信側)
import os
import json

# open('config.json') がファイルを開き、ファイルオブジェクトを返す
# json.load() がそのファイルオブジェクトを読み込み、JSONデータをPythonの辞書に変換
config = json.load(open('config.json'))

# 辞書にアクセスしてファイルパスを取得
named_pipe_path = config['filepath']

# remove: メモリからではなく、ディスク(ストレージ)上のファイルを削除する処理
# 新しいセッションをクリーンな状態から開始するため、既に同じ名前のパイプが存在する場合はそれを削除する
if os.path.exists(named_pipe_path):
    os.remove(named_pipe_path)

# 指定したパスに名前付きパイプを作成する
# 数値モード(8進数表記)でパーミッションを指定
os.mkfifo(named_pipe_path, 0o600)

print(f"FIFO named {named_pipe_path} is created successfully.")
print("Type what you would like to send to clients.")

# ユーザーからの入力を取得し、それを名前付きパイプに書き込み
# 'exit'が入力されるまでこの操作を繰り返す
flag = True

while flag:
    # ユーザー入力を促す
    inputstr = input("Input your message: ")

    if inputstr == 'exit':
        flag = False
    else:
        with open(named_pipe_path, 'w') as named_pipe:
            named_pipe.write(inputstr)

os.remove(named_pipe_path)
print("Program exit!")
client.py(データ受信側)
import os
import json

config = json.load(open('config.json'))

named_pipe_path = config['filepath']

# 読み込みモードでキーで指定したファイルを読み取り
# 名前付きパイプに関するファイルオブジェクトを変数に格納
named_pipe = open(named_pipe_path, 'r')

flag = True

while flag:
    if not os.path.exists(named_pipe_path):
        flag = False

    data = named_pipe.read()

    if len(data) != 0:
        print('Received data from named pipe: {}'.format(data))

named_pipe.close()

処理の流れ

  1. サーバ起動(python3 server.py
  2. クライアント起動(python3 client.py
  3. サーバ側でメッセージ入力(例 Hello!
  4. クライアントで確認
  5. サーバ側でexit終了
  6. クライアントも終了

主要メソッドの意味

open('config.json')

Pythonの組み込み関数 open() が、引数に指定されたファイルパス config.json を探し、そのファイルを読み込みモードで開きます。
そして、その開かれたファイルを表すファイルオブジェクトを返します。

fdopen()との違い
os.fdopen()はファイル記述子(整数)を使い、open()はファイルのパス(文字列)を使います。この関数は、すでに開かれているファイル(またはソケットなど)のファイル記述子を引数として受け取り、それに対応するファイルオブジェクトを作成します。

json.load(open('config.json'))

JSON形式のデータをファイルから読み込むための関数で、ファイルオブジェクトを引数として受け取り、ファイルオブジェクトから読み込みます。
この関数は、JSON形式のテキストデータを読み込み、それをPythonのオブジェクト(主に辞書やリスト)に変換します。これにより、プログラム内でデータを簡単に操作できるようになります。

os.path.exists()

指定されたパスがファイルシステム上に存在するかどうかを確認するためのメソッドです。
このメソッドは、引数として受け取ったパスが、ファイル、ディレクトリ、または特殊なファイル(名前付きパイプなど)として実際に存在すれば True を返し、存在しなければ False を返します。
ここでは、名前付きパイプがすでに存在するかどうかを確かめています。

ファイル権限の設定

名前付きパイプは特殊なファイルです。ファイル(名前付きパイプ)には誰でもアクセスして読み取り・書き込み・実行ができるわけではありません。
アクセスしようとしている人がどんなグループに属しているのかなどによってできることが異なります。
ファイルの権限とパーミッションについては以下の記事に簡単にまとめましたので、よろしければご参照ください。
https://zenn.dev/mabo23/articles/3cf1e4d5643e5e

ここでは、所有者のみ読み書きができるように設定しました。

ファイルを閉じる方法

1. ファイル記述子(FD)を閉じる

これは低レベルな操作で、os.close()関数を使います。

2. ファイルオブジェクトのclose()メソッドを使う

open()関数で得られたファイルオブジェクトには、close()メソッドが用意されています。
これは、ファイルオブジェクトを明示的に閉じるための標準的な方法です。
※この方法は、途中で例外が発生した場合に close() が呼び出されない可能性があるため、注意が必要です。

3. with構文を使う

これがPythonでファイルを扱う際に最も推奨される方法とされています。
withブロックを抜けるときに、たとえ例外が発生したとしても、ファイルオブジェクトは自動的に閉じられるためです。

実際の挙動を確かめる

1. サーバ起動


入力ができる状態です。

2. 新たなターミナルを開き、クライアント起動


サーバからのメッセージを待っている状態

3. サーバ側でメッセージ入力


ここでは、「Hello!」と入力してみます。

4. クライアント側を確認・メッセージ取得


確かにサーバプロセスからメッセージが届きました。

5. 「exit」を入力してサーバプログラムを終了

6. クライアントプログラムも強制終了


自動的に終了したことが確認できました。

※名前付きパイプ(ファイル)が一時的に生成される

名前付きパイプは特殊なファイルで、ファイルシステム上のファイルとして存在しますが、ディスクに実際のデータを書き込みはしません。

サーバプロセスで送信したメッセージは一時的に named-pipe を経由してクライアントプロセスに渡されます。

その様子がこちら

このファイルは一時的なものであるため、通信が終了すると自動的になくなります。

実際に遭遇したエラー

クライアントプロセスが起動できない

原因

サーバがまだ起動していないため、名前付きパイプが存在しない。

サーバ側で os.mkfifo() によってパイプが作られて初めてクライアントは接続できるので、必ずサーバを先に起動する必要があります。

したがって、サーバから起動しない場合、以下のようにクライアントプロセスが起動できないため注意が必要です。

注意点

  • 名前付きパイプのファイルそのものは、自動で削除されない
    明示的に os.remove() を呼ばない限りファイルシステム上に残ります。

  • 送信されたデータは揮発的で、読み込まれると消える

まとめ

この記事では、名前付きパイプを使ったプロセス間通信について、実装例を交えて解説しました。

  • 無名パイプは基本的に親子プロセス専用
  • 名前付きパイプは、共通のパスを介して親子関係のないプロセス間でも通信可能
  • 名前付きパイプのファイルは明示的に削除しないと残る点に注意

簡易プログラムを通して、

  • 実際にパイプが作られるのは os.mkfifo() 実行時であること
  • サーバを先に起動する必要があること

を確認しました。
プロセス間通信の実現手段としての名前付きパイプがどのようにデータを送信するのか、理解の参考になれば幸いです。

最後までお読みいただき、ありがとうございました。

参考URL

https://recursionist.io/dashboard/course/31/lesson/1094

https://docs.python.org/ja/3/library/json.html

https://qiita.com/richmikan@github/items/bb660a58690ac01ec295

Discussion