Linuxのパイプ、空なのに読み取りが終わらない理由

淡い緑のガラスの通路に三つの桃色の小片が残り、入口には紫の二つの栓が、一つは浮き、一つは接続された抽象的な構図。

パイプからデータを読み続ける処理が、書き手の仕事が終わったのに終了しないことがあります。原因の一つは、どこかに書き込み側のファイル記述子が残っていることです。Linuxのパイプでは、「いま中身がない」と「これから先も書き込まれない」を区別します。この違いが、読み取りを待つのか、終端を返すのかを決めます。

簡単にまとめると?

  • パイプが空でも、書き込み側を参照する番号が残っていれば、通常の読み取りは新しいデータを待ちます。
  • 書き込み側の参照がすべて閉じられ、残ったデータも読み切った後に、正の長さを要求したreadが0を返してEOFを知らせます。
  • 親子プロセスや複製した番号の閉じ忘れは、終端を届かなくする原因になります。読み取り側だけの待ち時間を調整する前に、両端の所有者を確認します。

パイプは、読み口と書き口を持つバイト列

パイプは、プロセス間で一方向にデータを渡すための仕組みです。作成すると、読み取り端と書き込み端を参照する二つのファイル記述子が得られます。書き口に渡したバイト列を、読み口から受け取ります。

通常ファイルとの違いとして、パイプには、任意の位置へ移動して読み直す仕組みがありません。lseek()で位置を動かすことはできません。また、パイプはメッセージの区切りを保持する通信路ではなく、バイトストリームです。「一回書いた分を一回で読み取れる」とは限らないため、行の区切りや長さ情報などは、利用する側の規則で扱います。

ここでは、通常のブロッキング読み取りを考えます。これは必要なデータがまだなければ待つ方式です。read(fd, buffer, count)の戻り値は、実際に読み取れたバイト数です。要求したcountより少なくても、それだけでエラーとは限りません。本稿でEOFを判定する例は、countに正の長さを指定した場合です。長さ0を要求した読み取りと混同しないようにします。

空のパイプが、すぐEOFにならない理由

読み取り側から見ると、パイプが空である理由は二つあり得ます。書き手がまだ次のデータを準備している場合と、もう誰も書き込まない場合です。前者で毎回EOFを返すと、一時的な空きを通信の終わりと誤認してしまいます。

そこで、カーネルは書き込み端を参照するファイル記述子が残っているかを扱います。空でも書き込み側の参照が一つ以上あれば、ブロッキング読み取りはデータを待ちます。参照がすべて閉じられていれば、残っているデータを読み切った後、read()が0を返して終端、EOF(end-of-file)を知らせます。

ここで数えるのは、プログラム上の「書き込み仕事が終わった」という気持ちではありません。書き口に対する参照が実際に閉じられたかどうかです。もう書かない処理であっても、番号を開いたまま持っていれば、読み取り側は終わりを判断できません。

一つ閉じても、もう一つ残っていれば待つ

書き込み側を参照する番号が二つあり、パイプにABCという3バイトが入っている例を考えます。読み取り側が2バイトを要求すると、まずABを受け取り、Cが残ります。書き込み側の番号を一つ閉じた後でも、もう一つの参照は残っています。

次に2バイトを要求し、利用可能な1バイトCだけを受け取る場合を考えます。この短い読み取りは、EOFではありません。戻り値は1です。さらに読むと、今度は中身が空なので待ちます。残っていた最後の書き込み側参照も閉じると、その待ちが終端として解け、0が返ります。

パイプの読み取り条件。未読データがあれば読み取る。空で書き込み参照が残れば待機。空で書き込み参照がゼロならEOF。正の長さを要求するブロッキング読み取りの例。
正の長さを要求するブロッキング読み取りの整理。参照がなくても未読データがあれば、先にそのデータを受け取る。

逆に、書き込み側をすべて閉じた瞬間に、バッファの中身が捨てられるわけでもありません。未読のABCが残っていれば、読み取り側はそれを受け取った後でEOFへ進みます。「最後の書き口を閉じた」と「最後のデータを読んだ」は、別の時点です。

親子プロセスでは、使わない端をそれぞれ閉じる

パイプを作ってからfork()で親子に分かれる構成では、両者にパイプ端の参照が残ります。たとえば親が書き手、子が読み手になるなら、親は使わない読み口、子は使わない書き口を閉じる必要があります。そのうえで、親はデータを送り終えたら、自分の書き口を閉じます。

もし子が、使わない書き口を持ち続けていたらどうなるでしょうか。親が自分の書き口を閉じても、子自身がもう一つの書き込み側参照を保持しています。子が読み取りでEOFを待つ一方、EOFが届く条件を子自身が妨げることになります。これはデータをもっと早く読むだけでは解消しない問題です。

同じことは、dup()などで番号を複製した場合にも起こります。元の番号だけを閉じ、複製した番号を残すと、書き込み側の参照はなくなりません。処理の開始時、正常終了時、途中の失敗時について、どのプロセスがどの端を閉じるかを対応付けておくと、待ち続ける原因を追いやすくなります。

待たない読み取りと、相手が消えた書き込み

O_NONBLOCKを使う非ブロッキング方式では、空のパイプで書き込み側の参照が残っていると、読み取りは待つ代わりにEAGAINで失敗します。これは「今はデータがない」という状態であり、0が返るEOFとは分けます。再び読み取る機会をどう待つかは、イベント通知などを使う側の設計になります。

反対に、読み取り端を参照する番号がすべて閉じられた後の書き込みでは、SIGPIPEシグナルが発生します。呼び出し側がこのシグナルを無視していれば、write()はEPIPEで失敗します。「読者がいない」と「バッファが満杯」も異なる状態です。シグナルの扱いを含め、通常の再試行で回復する場面なのかを分けて確認します。

終了しない処理を調べる順番

まず、未読データが残っているのか、空なのかを分けます。次に、書き込み端を誰が持っているかを調べます。親だけでなく、子、複製した番号、途中で起動した処理へ残った参照も対象です。その後で、読み取りがブロッキングなのか非ブロッキングなのか、戻り値とエラーをどう扱っているかを確認します。

説明図と数値例は、バイト列と参照数による簡単な状態モデルです。実機のスケジューリング、シグナル割込み、満杯時の書き込み順序や性能は検証していません。パイプの容量を増やす話とも分けています。終了条件を理解するための中心は、バッファが空かだけでなく、これから書ける参照が残っているかを見ることです。

出典・参考資料