🐈

【Python入門】パイプで親子プロセス間通信をしてみる(親↔︎子)

に公開

はじめに

前回の記事で、親子関係にあるプロセスのプロセス間通信を実現する方法としてパイプを解説し、親プロセスから子プロセスへのデータの渡し方について解説しました。

前回の記事はこちら
https://zenn.dev/mabo23/articles/f829f541b9e427

この記事では、親プロセスと子プロセスが相互にパイプを使ってプロセス間通信を実装する方法を、実体験に基づき解説していきます。
ここでは、実際に遭遇したエラーの意味と原因についてもまとめていきます。

開発環境

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

パイプを使って双方向通信を実装

最終的な実装コード
import os

r1,w1 = os.pipe()
r2,w2 = os.pipe()
pid = os.fork()

if pid > 0:
    os.close(r1)
    os.close(w2)

    # 親プロセスから子プロセスへ
    with os.fdopen(w1, 'wb') as parent_to_child_pipe:
        parentMessage = "Parent PID: {}".format(os.getpid())
        print("Send this message for child: '{}'".format(parentMessage))
        parent_to_child_pipe.write(parentMessage.encode('utf-8'))

    # 子プロセスから親プロセスへ
    with os.fdopen(r2) as child_to_parent_pipe:
        print("Incoming message from child: ", child_to_parent_pipe.read())

else:
    os.close(w1)
    os.close(r2)

    # 親プロセスから子プロセスへ
    with os.fdopen(r1) as parent_to_child_pipe:
        print("Incoming message from parent: ", parent_to_child_pipe.read())
    
    print("Child PID: {}".format(os.getpid()))

    # 子プロセスから親プロセスへ
    with os.fdopen(w2, 'wb') as child_to_parent_pipe:
        childMessage = "Child PID: {}".format(os.getpid())
        print("Send this message for parent: '{}'".format(childMessage))
        child_to_parent_pipe.write(childMessage.encode('utf-8'))

「with 構文を使わない」 → 「with 構文を使う」という順番で実装しました。

実装のポイント

1. pipe()システムコールを2回実行

これによって、親プロセスから子プロセスにデータを流すためのパイプ、子プロセスから親プロセスにデータを流すためのパイプを作成することができます。
パイプは単方向にしかデータが流れないため、この方法によって双方向のデータ通信を実現します。

2. 使用しない FD は閉じることで一方向性を担保

3. with 構文で FD を自動的に閉じることができ、忘れによるブロックを防止

with 構文使用前に発生したエラー

with 構文使用前の実装コード
import os

r1,w1 = os.pipe()
r2,w2 = os.pipe()
pid = os.fork()

if pid > 0:
    os.close(r1)
    os.close(w2)

    parentMessage = "Parent PID: {}".format(os.getpid())
    print("Send this message: '{}'".format(parentMessage))
    os.write(w1, parentMessage.encode('utf-8'))

    pipe2 = os.fdopen(r2)
    print("Incoming message from child: ", pipe2.read())

else:
    os.close(w1)
    os.close(r2)

    print("Child PID: {}".format(os.getpid()))
    pipe1 = os.fdopen(r1)
    print("Incoming message from parent: ", pipe1.read())

    childMessage = "Send my PID: {}".format(os.getpid())
    print("Send this message: '{}'".format(childMessage))
    os.write(w2, childMessage.encode('utf-8'))
エラーメッセージ
Send this message: 'Parent PID: 82079'
Child PID: 82080
^CTraceback (most recent call last):
Traceback (most recent call last):
  File "/Users/mavo/practice/python/pipe/pipe.py", line 24, in <module>
    print("Incoming message from parent: ", pipe1.read())
                                            ~~~~~~~~~~^^
KeyboardInterrupt
  File "/Users/mavo/practice/python/pipe/pipe.py", line 16, in <module>
    print("Incoming message from child: ", pipe2.read())
                                           ~~~~~~~~~~^^

原因

書き込み用のファイルディスクリプタ(FD)が閉じられていないことによる

read()の挙動

パイプは、書き込み側からデータが来なくなるまで、read()がブロック(停止)するように設計されています。
これは、read()がEOF(End Of File)を認識して、初めてパイプからデータを読み込むことができるようになっています。
そのため、書き込み用のFDが閉じられて初めてEOFを read() 側に通知し、データを読み込めるようになります。

read()の後は os.close(pipe)しなくていいのか?

  • read() 後でも FD を閉じる必要はある場合とない場合がある
  • 安全策としては明示的に閉じるのが推奨

1. パイプの EOF と read() の関係

パイプの read() は、書き込み側FDが全て閉じられるまでブロック(停止)します。
書き込み側が閉じられることで EOF が通知され、読み取り側はデータを受け取れるようになります。
つまり、書き込み側を閉じていないと read() は永遠に待ち続ける可能性があるということです。

2. os.fdopen() とファイルオブジェクトの自動クローズ

os.fdopen()で作成したファイルオブジェクトは、プログラム終了時やガベージコレクション時に自動で閉じられます。
そのため、最終的には os.close() で閉じる必要はないこともあるということです。

3. なぜ明示的に閉じるべきか

ガベージコレクションのタイミングは予測できません。
長時間動作するプロセスや複雑な通信では、閉じ忘れによるブロックのリスクがあります。
そこで、with 構文を使うことによって、オブジェクトのスコープを抜けると自動で close() されるので手動管理よりも安全です。

修正後の実装コード

修正後のコード
if pid > 0:
    os.close(r1)
    os.close(w2)

    parentMessage = "Parent PID: {}".format(os.getpid())
    print("Send this message: '{}'".format(parentMessage))
    os.write(w1, parentMessage.encode('utf-8'))
    os.close(w1) #追加

    pipe2 = os.fdopen(r2)
    print("Incoming message from child: ", pipe2.read())

else:
    os.close(w1)
    os.close(r2)

    print("Child PID: {}".format(os.getpid()))
    pipe1 = os.fdopen(r1)
    print("Incoming message from parent: ", pipe1.read())

    childMessage = "Send my PID: {}".format(os.getpid())
    print("Send this message: '{}'".format(childMessage))
    os.write(w2, childMessage.encode('utf-8'))
    os.close(w2) # 追加

得られた学び

  • read()システムコールは、パイプの書き込み側がすべて閉じられるまで、データの到着を無限に待ち続けること
    read()がパイプからのデータの読み込みを完了して処理を続けることができるようになるのは、通信の相手方がos.close()を使って、パイプの書き込み側を閉じたとき

  • os.close(w)は単にFDを閉じるだけでなく、パイプの読み取り側に「これ以上データは来ない」という通知を送る重要な役割を果たすこと

  • os.fdopen() で作成されたファイルオブジェクトは、プログラムが正常に終了するか、ガベージコレクションによってオブジェクトが破棄されるときに自動的に close() されること

  • with文が使えるのは、os.fdopen() で作成されたファイルオブジェクトに対してであること

まとめ

この記事では、

  • 親プロセスから子プロセスにデータを渡す流れ
  • 子プロセスから親プロセスにデータを渡す流れ

について解説しました。
パイプが単方向通信を実現する手段であることから、双方向の通信を実現するには、パイプが2つ必要だということがお分かりいただけたと思います。

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

参考URL

https://docs.python.org/ja/3/reference/compound_stmts.html

Discussion