Linuxの名前付きパイプ:思ったほどFIFOではありません

5
2022.01.06

要するに:

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

私が期待したこと:

 a
b

最初のecho … > fifoが最初にファイルを開いたはずなので、そのプロセスが最初にファイルに書き込むことを期待します(最初に開いたブロックを解除します)。

私が得るもの:

 b
a

驚いたことに、この動作は、2つの別々の端末を開いて、完全に独立したプロセスで書き込みを行うときにも発生しました。

名前付きパイプの先入れ先出しのセマンティクスについて何か誤解していますか?

スティーブンは遅延を追加することを提案しました:

 #!/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ループ自体のコピーと貼り付けでさえ、約半分の時間で失敗します。私はここで何が起こっているのか非常に混乱しています。

回答
15
2022.01.06

パイプは先入れ先出しです。あなたの問題は、「イン」がいつ起こるかを誤解することです。 「in」イベントは書き込みであり、開くことではありません。

不要な句読点を削除すると、コードは次のようになります。

 echo a > fifo & echo b > fifo &

これにより、コマンドecho a > fifoecho b > fifoが並行して実行されます。最初に入るものは何でも最初に出ますが、どちらが最初に入るかについてはほぼ同じレースです。

あなたはab前に必ずお読みになりたい場合は、bの前にそれを書くように手配しなければなりません。この手段あなたはecho a > fifoが完了するまで、あなたがecho b > fifoを開始する前に待たなければなりません。

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

さらに掘り下げたい場合は、内部で行われる基本的な操作を区別する必要があります。 echo a > fifoは、次の3つの操作を組み合わせています。

  1. 書き込み用にfifoを開きます。
  2. ファイルに2文字( aと改行)を書き込みます。
  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コマンドは、端末への書き込みにcat >&2に対して競争している、とあなたはopening for writingwrote acatからaが表示される場合がありますので。タイミングをより正確に把握したい場合は、システムコールを追跡できます。たとえば、Linuxでは次のようになります。

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

これで、2人のライターを配置すると、リーダーが到着するまで、両方のライターがオープニングステップでブロックされます。誰が最初にopen呼び出しを開始するかは関係ありません。パイプは、オープンな試行ではなく、データの先入れ先出しです。最初に書く人が重要です。これを試すためのスクリプトを次に示します。

 #!/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
15
2022.01.06

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

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

バックグラウンドでプロセスを開始するということは、実際にいつスケジュールされるかわからないことを意味します。したがって、最初のバックグラウンドプロセスが2番目のプロセスの前にその作業を実行するという保証はありません。同じことが、ブロックされたプロセスのブロック解除にも当てはまります。

2番目のプロセスを人為的に遅らせることにより、バックグラウンドプロセスを使用しながら、オッズを改善できます。

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

長い遅延は、より良いオッズ:fifoを開く終えるのを待ってecho a > fifoブロック、catが始まるとecho aのブロックを解除fifoを開き、その後、echo bが実行されます。

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

最初にcatを実行すると、異なる結果が得られます。

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

そうすれば、書き込み用にfifoを開くとブロックされない傾向があるため(それでも、保証なしで)、最初のセットアップよりも高い頻度でaが最初に表示されます。また、すなわちだけaが出力され、echo bの実行前にcat仕上げが表示されます。