Linux 名前付きパイプ: 思ったほど先入選出ではない

Linux 名前付きパイプ: 思ったほど先入選出ではない

簡単に言うと:

mkfifo fifo; (echo a > fifo) &; (echo b > fifo) &; cat fifo

私が期待したもの:

a
b

最初のプロセスはecho … > fifoファイルを開く最初のプロセスである必要があるため、そのプロセスがファイルに書き込む最初のプロセスになりたい(最初に開いてロック解除)。

私が得るもの:

b
a

驚くべきことに、この動作は、完全に別々のプロセスに書き込むために2つの別々の端末を開くときにも発生します。

名前付きパイプの先入れ先出しの意味を誤って理解しているのでしょうか?

Stephenは遅延の追加を提案した。

#!/usr/bin/zsh
delay=$1
N=$(( $2 - 1 ))
out=$(for n in {00..$N}; do
  mkfifo /tmp/fifo$n
  (echo $n > /tmp/fifo$n) &
  sleep $delay
  (echo $(( $n + 1000 )) > /tmp/fifo$n )&
  # intentionally using `cat` here to not step into any smartness
  cat /tmp/fifo$n | sort -C || echo +1
  rm /tmp/fifo$n
done)
echo "$(( $res )) inverted out of $(( $N + 1 ))"

今、この言葉は100%です(delay = 0.1, N = 100)。

それでもmkfifo fifo; (echo a > fifo) &; sleep 0.1 ; (echo b > fifo) &; cat fifo手動で実行ほぼいつも逆順を生成します。

実際、コピーと貼り付けforループ自体も半分ほど失敗します。はい非常にここで何が起こっているのか混乱しています。

答え1

これは、パイプのFIFOセマンティクスとは何の関係もなく、いかなる方法でもパイプについて何も証明しません。これは、FIFOが書き込みおよび読み取りのために開くまでブロックされるため、読み取りのcatために開くfifoまで何も起こらないという事実に関連しています。

最初のものがecho最初のものでなければならないからです。

バックグラウンドでプロセスを開始することは、そのプロセスが実際にいつ予約されたかを知ることができないことを意味します。約束はありません。最初のバックグラウンドプロセスは、2番目のバックグラウンドプロセスの前にタスクを完了します。同じように適用されますプロセスのブロック解除

バックグラウンドプロセスを引き続き使用しながら、2番目のプロセスを人為的に遅らせることで確率を上げることができます。

rm fifo; mkfifo fifo; echo a > fifo & (sleep 0.1; echo b > fifo) & cat fifo

遅延時間が長くなるほど機会は大きくなります。echo a > fifo開くが完了するまでブロックしfifocat開始して開き、fifoブロックを解除しecho aてからecho b実行します。

ただし、ここでの主な要因は、catFIFOがオンになっているときです。それまで、シェルブロックはリダイレクトを設定しようとします。表示される出力の順序は、最終的に書き込みプロセスがロック解除される順序に依存します。

最初に実行すると、他の結果が表示されますcat

rm fifo; mkfifo fifo; cat fifo & echo a > fifo & echo b > fifo

これはfifo書き込みオープンをブロックしないため(まだ保証できません)、a最初は最初の設定よりも頻度が高くなります。cat事前実行完了も確認できますecho bつまり出力のみ可能ですa

答え2

パイプは最初に入って来る方法です。問題は、「in」が発生すると誤解することです。 「in」イベントは次のとおりです。書く、開けません。

役に立たない句読点を削除すると、コードは次のようになります。

echo a > fifo & echo b > fifo &

これにより、コマンドが並列echo a > fifoに実行されますecho b > fifo。先に入ってくる人が先に出るが、誰が先に入るかはほぼ同等の競争だ。

a他の人が最初に読みたい場合は、まずb書き込みを準備する必要がありますb。これはecho a > fifo、開始する前に作業が完了するのを待つ必要があることを意味しますecho b > fifo

{ echo a > fifo; echo b > fifo; } & cat fifo

詳しくは、後で行われる基本的な作業を区別する必要があります。echo a > fifo3つのタスクを組み合わせます。

  1. 書き込みにオープンですfifo
  2. aファイルに2文字(および改行文字)を書き込みます。
  3. ファイルを閉じます。

これらのタスクが異なる時間に発生するようにスケジュールできます。

(
    exec >fifo     # 1. open
    sleep 1
    echo a         # 2. write
    sleep 1
)                  # 3. close

同様にcat foo、オープン、読み取り、およびクローズ操作が結合されます。次のように区別できます。

(
    exec <fifo     # 1. open
    sleep 1
    read line      # 2. read
    echo $line
    sleep 1
)                  # 3. close

readシェル組み込みは実際に複数のreadシステムコールを行うことができますが、今は問題ではありません。)

Fifoは実際にはパイプではありません。これは潜在的なパイプラインに似ています。 fifo はディレクトリエントリです。プロセスが読み取るためにfifoを開くと、パイプオブジェクトが作成されます。パイプが存在しないときにプロセスが書き込み用にFIFOを開くと、openパイプが作成されるまで呼び出しはブロックされます。また、プロセスが読み取り用にfifoを開くと、プロセスが書き込み用にfifoを開くまでこの操作もブロックされます(リーダーがシェルに不便な非ブロックモードでパイプを開かない限り)。したがって、名前付きパイプの最初のオープン読み取りと最初のオープン書き込みが同時に返されます。

以下は、この知識を実際に適用するシェルスクリプトです。

#!/bin/sh
tick () { sleep 0.1; echo tick; echo 0.1; }
mkfifo fifo
{
    exec <fifo; echo >&2 opened for reading;
    echo a; echo >&2 wrote a
} & writer=$!
tick
{
    exec >fifo; echo >&2 opened for writing;
    exec cat >&2;
} & reader=$!
wait $writer
kill $reader
rm fifo

両方の開口部がどのように同時に発生するかを確認してください(観察できる限りできるだけ近い)。そして、書き込みはそれ以降にのみ可能です。

注:上記のスクリプトには実際に競合条件がありますが、パイプとは何の関係もありません。これらのコマンドは、端末に書き込まれるためにスクランブルされているため、以前と表示できますecho >&2。時間をより正確に知りたい場合は、システムコールを追跡してください。たとえば、Linuxでは次のようになります。cat >&2acatopening for writingwrote a

mkfifo fifo
strace -f -P fifo sh -c '…'

今、2人の作家を配置すると、両方の作家がリーダーが到着するまで、開始段階でブロックされます。誰が最初に呼び出すかは問題ではありませんopen。パイプは、公開試行ではなくデータに対して先入れ先出しされます。 WHO書くまず最も重要です。これを実験するためのスクリプトは次のとおりです。

#!/bin/sh
mkfifo fifo
{
    exec >fifo; echo >&2 opened for writing a
    sleep $1
    echo a; echo >&2 wrote a
} & writer_a=$!
{
    exec >fifo; echo >&2 opened for writing b
    sleep $2
    echo b; echo >&2 wrote b
} & writer_b=$!
sleep 0.2
cat fifo & reader=$!
wait $writer_a
wait $writer_b
kill $reader
rm fifo

スクリプトは、リーダーaの待機時間と作成者bの待機時間の2つのパラメーターで呼び出されます。リーダーは0.2秒以内にオンラインになります。両方のレイテンシが0.2秒未満の場合、ライターがオンラインになるとすぐに両方のライターが書き込みを試みます。これは競争です。他方、待ち時間が0.2より大きい場合、最初に来た人が最初に出力を得る。

$ ./c 0.1 0.1
# Order of "opened for writing": random
# Order of "a"/"b": random
# Order of "wrote": random, might not match a/b due to echo racing against each other
$ ./c 0.3 0.4
# Order of "opened for writing": random
# Order of "wrote": a then b
# Order of "a"/"b": a then b

関連情報