FIFOへのすべての入力を読んだら、FIFOのすべてのリーダーを閉じますか?

FIFOへのすべての入力を読んだら、FIFOのすべてのリーダーを閉じますか?

すべての入力を読んだ後、FIFOのすべてのリーダーをどのように閉じますか?そのうちの1つだけをオフにしてプログラムが完了しないようです。

以下は、動作するサンプルプログラムです(ファイルに入れるテスト用)。

set -euo pipefail

rm -f todo.pipe
mkfifo todo.pipe

rm -f output.pipe
mkfifo output.pipe

cat todo.pipe | \
    while read line && echo hej $line; do :; done \
            > output.pipe &

echo "adam\n bertil\n carl" > todo.pipe &

cat < output.pipe

出力は予想通りです。

❯ ./test.zsh
hej adam
hej bertil
hej carl

しかし、これらのタスクを処理するために別のスレッドを追加すると、タスクtodo.pipeは永久に中断されます。

set -euo pipefail

rm -f todo.pipe
mkfifo todo.pipe

rm -f output.pipe
mkfifo output.pipe

cat todo.pipe | \
    while read line && echo hej $line; do :; done \
            > output.pipe &
# The below 3 lines is all that's changed
cat todo.pipe | \
    while read line && echo hej $line; do :; done \
            > output.pipe &

echo "adam\n bertil\n carl" > todo.pipe &

cat < output.pipe

これで以前と同じ内容を印刷しますが、決して戻りません。なぜ?この問題をどのように解決できますか?

私は2番目の「ワーカースレッド」が現在EOFまたはそれに似ていることを疑っていますが、ここでは基本的なものを見逃しているようです。

答え1

FIFOブロックを読み取りモードで読み取るのではなく、開くと、他のプロセスも書き込みモードで開くまで(またはその逆)ブロックされ、そのような場合にパイプがインスタンス化されることを認識することが重要です。

パイプがアクティブな間にFIFOを開いて、より多くのプロセスをパイプに接続できます。

どのプロセスでもパイプが開かない場合、パイプは破壊され、その後最初のパーティに戻り、読み取りと書き込みのためにfifoが再び開くと、他のパイプがインスタンス化される可能性があります。

存在する:

[0]
[1] cat todo.pipe |
    [2] while read line && echo hej $line; do :; done \
            > output.pipe &
# The below 3 lines is all that's changed
[3] cat todo.pipe |
    [4] while read line && echo hej $line; do :; done \
            > output.pipe &

[5] echo "adam\n bertil\n carl" > todo.pipe &

[6] cat < output.pipe

メインシェルプロセスは同時に4つのプロセスを作成し、各プロセスは独立して並列に実行されます。最初は最初のパイプを実行し、2番目は2番目のパイプを実行し、3番目は実行しecho、4番目はcat(開いた後output.pipe)実行します。

cat todo.pipeパイププロセスはまた、元のプロセスが同時にループしている間に実行する追加のプロセスを作成しますwhile

したがって、6つ(最後のプロセスを待つメインシェルプロセスを計算すると7つcat)がほとんど同時に開始されます。私は[1]それらを上に..とマークしました[6]

予約方法は、システムのプロセススケジューラによって異なります。外部コマンド(たとえば、cat時間のかかるコマンド)を実行するには、シェル自体で実行される操作が最初に発生する可能性があります。

2、4、5、6はすべてシェルでfifoファイルを開き始めます。 2と4はoutput.pipe書き込みと6読み取りのために開いています。すぐに互いのロックが解除され、パイプがインスタンス化されます。

todo.pipe5は、少なくとも1つのcatプロセスが読み取り専用で開くまで書き込み専用のオープン状態を一時停止します。

これにより、1と3がこの点をめぐって競争するようになります。実行には、プロセスメモリの消去、ディスクから実行可能ファイルのロード、ライブラリ共有、動的接続、動的接続の実行、最後にそのコマンドラインが解析され、fifoファイルが最終的にその中にあるコードの実行がcat含まれます。開く。/bin/catcat

1または3のいずれかがFIFOを開くと(ここでは1と仮定)、5つのロックが解除されます。 1はこのfdに対して作業をread()続けますが、まだパイプに何もないので停止します。

5番は現在予定されているプロセスかもしれません。これはechoシェルの組み込みコマンドを実行しているので、aを実行してwrite("adam...)終了し、fdがそれを閉じますoutput.pipe

その後、read()続行してcat大きな塊を読み取り、小さな出力全体を飲み込み、パイプの書き込みread()の終わりまでfd'ingすることを含めて終了できます。

3がそれまでfifoを開かない場合、パイプは破壊され、3がついにfifoを開くと、他のものが書き込みモードでfifoを開き、ここで配管が発生しない関連のない新しいパイプをインスタンス化するまで停止します。 。

最初に開かないと、output.pipefifoのような問題が発生する可能性があります。

今やっても:

{
   cat | while...done &
   cat | while...done
} < todo.pipe > output.pipe &
echo ... > todo.pipe &
cat < output.pipe

where はtodo.pipe読み取るために一度だけ開かれるので、両方ともcatfd を共有します ( と同じoutput.pipe)、これらの問題を避けるので、おそらくあまり役​​に立ちません。

cat最初のタスクを実行するタスクはread()出力echo全体を飲み込み、他のタスクには何もしません。echo読み取りバッファよりも大きな出力に置き換えて、両方のバッファcatにそれぞれいくつかのフラグメントをキャッチする機会を与えても、cat各フラグメントは見かけ上ランダムな方法で切り捨てられます。

組み込み関数が一度に1バイトずつ読み取るため、sがパイプから直接読み取られるようにcat |sを削除すると、状況はさらに悪化します。したがって、競合する2つのsが順番に1バイトを読み取ることになります。readreadread

これが機能する唯一の方法は、次のジョブが入力される前に最初のジョブがその1つによって読み取られるようにcatプロセスが十分に遅いことを確認し、システムコールを介して一度に1つのToDoエントリを入力することです。 4KiBより大きくないジョブの場合、読み取りバッファサイズよりも大きくはありません。todo.pipecatwrite()cat

より良いアプローチは、1つのプロセスがパイプを読み取り、xargs -PGNUやGNUなどのものを使用してタスクをワーカーに渡すことですparallel

関連情報