bashスクリプトはすべてのスリープコマンドを実行しません

bashスクリプトはすべてのスリープコマンドを実行しません

以下のコードはLinuxシステムで実行されている単純なbashスクリプトで、各出力間の時間間隔が次の理由を知りたいと思います。4つ 第二変える

$ for test in test1 test2 test3; do (echo ${test}; sleep 4s; echo hop2; sleep 4s; echo hop3) | date;  done
Sun 11 Apr 2021 12:42:27 AM +07
Sun 11 Apr 2021 12:42:31 AM +07
Sun 11 Apr 2021 12:42:35 AM +07

後者の時間値を少し増やしても、各出力間の時間はまだ4秒です。

$ for test in test1 test2 test3; do (echo ${test}; sleep 4s; echo hop2; sleep 50s; echo hop3) | date;  done
Sun 11 Apr 2021 12:42:44 AM +07
Sun 11 Apr 2021 12:42:48 AM +07
Sun 11 Apr 2021 12:42:52 AM +07

これはとても混乱していますが、誰もがこれを説明できればとても感謝します。

より混乱するのは、コマンドを前に置くとdateスリープコマンドが実行されないように見えることです。

$ for test in test1 test2 test3; do (date; echo ${test}; sleep 50s; echo hop2; sleep 50s; echo hop3) | date;  done
Sun 11 Apr 2021 01:22:35 AM +07
Sun 11 Apr 2021 01:22:35 AM +07
Sun 11 Apr 2021 01:22:35 AM +07

答え1

これを明確にするために「hop2」をエコーする前と後(パイプバイパス)stderrにデバッグ出力を追加します。

$ for test in test1 test2 test3; do 
      (echo ${test}; sleep 4s; echo before hop2 >&2;
       echo hop2; echo after hop2 >&2; sleep 4s; echo hop3) | date;
  done
Sat Apr 10 11:29:46 PDT 2021
before hop2
Sat Apr 10 11:29:50 PDT 2021
before hop2
Sat Apr 10 11:29:54 PDT 2021
before hop2

気づくecho after hop2 >&2 絶対に実行しないでください、そしてそれに続くコマンド:2番目sleepecho hop3

私が理解したのは、これが起こったことです。ループ内では、2つの別々のプロセスが並列に実行され、最初のプロセスの出力が2番目のプロセスの入力に接続されます。 2つのプロセスが実行されます。

echo ${test}
sleep 4s
echo before hop2 >&2
echo hop2
echo after hop2 >&2
sleep 4s
echo hop3

そして

date

おおよその実行順序は次のとおりです(最初のステップ3の正確な順序とステップ4の開始はややランダムです)。

  1. プロセス1が実行されますecho ${test}。これは、「test1」(および改行文字)をパイプに書き込んで後で読み取れるようにバッファリングされます。
  2. プロセス2を実行しdate、現在の日付を端末に印刷します。
  3. プロセス2が終了し、パイプの端が閉じられる。
  4. プロセス1が実行されますsleep 4s
  5. 4秒後、プロセス1が実行され、echo before hop2 >&2「before hop2」が端末に印刷されます。
  6. プロセス1努力する実行がecho hop2パイプの唯一のリーダーがパイプを閉じたため、SIGPIPE エラーが発生します。これにより、明らかに完全なサブシェルプロセス(echoコマンドだけでなく)が終了します。

これはechoシェルが組み込まれているために発生します。/bin/echo hop2(シェルの組み込みechoコマンドではなく外部コマンド)を使用すると、sleep期待どおりに2番目のコマンドが実行されます。

ちなみに、これは他のシェルで比較的一貫しています。 bash、zsh、dash、ksh(対話型)で同じ結果を得ました。スクリプトのkshは、続行する前にプロセス1が終了するのを待たないという点で若干異なります。したがって、すべてのdatesがすぐに実行され、その後に一連の「hop2の前」行(4秒後)が続きます。

答え2

(echo ${test}; ...) | date

ここで何をしたいですか?データは標準入力に転送されていますが、date入力dateを読みません。日付のみ印刷して終了します。

終了後、dateパイプが閉じて、後で印刷されたすべてのデータにはもう行きません。パイプ(サブシェル(echo; sleep; echo; sleep))に書き込むプロセスはSIGPIPE信号を受信して​​終了します。

これはパイプラインが一般的に機能する方法です。左側が大量の出力を生成できる場合、信号は右側が興味を失った後に停止するように指示することです。

たとえば、同様の状況では、cat /dev/zero | head -c128 > /dev/null信号は最終的に終了し、cat無期限に維持されません。シェルもシェルもcatエラーメッセージを出力しません。このように動作するパイプは通常の動作の一部に過ぎません。今の状況も同じだ。 (場合によってはエラーメッセージが表示されますが、常にエラーメッセージが表示されるとは思わないでください。)

(外部ループは結果に影響を与えないため、省略しました。これを使用してtime ( ... ) | ...パイプラインにかかる時間を測定できます。)

Bashでは、配列のパイプラインにあるコマンドの終了ステータスを確認できますPIPESTATUS

$ ( echo ${test}; sleep 4s; echo hop2; sleep 50s; echo hop3; ) |
    date;  declare -p PIPESTATUS
Sat Apr 10 21:34:56 EEST 2021
declare -a PIPESTATUS=([0]="141" [1]="0")

SIGPIPEは少なくともLinuxでは13回なので、表示されている終了ステータス141 = 128 +のシグナル番号と一致します。 (プロセスはステータス> = 128で正常に終了する可能性がありますが、ここではそうではありません。)

あるいは、strace何が起こっているのかを確認できます。

$ strace -f bash -c '( echo ${test}; sleep 4s; echo hop2; 
                       sleep 50s; echo hop3; ) | date'
...
[pid 31647] write(1, "hop2\n", 5)       = -1 EPIPE (Broken pipe)
[pid 31647] --- SIGPIPE {si_signo=SIGPIPE, si_code=SI_USER, si_pid=31647, si_uid=1000} ---
[pid 31647] +++ killed by SIGPIPE +++
...

一方、パイプの右側にある入力を読み取ると、SIGPIPEはなく、パイプ全体が合計54秒間スリープモードになります。

$ time ( echo ${test}; sleep 4s; echo hop2; sleep 50s; echo hop3; ) |
       cat > /dev/null 

real    0m54.005s
user    0m0.000s
sys     0m0.000s

または、書き込み側でSIGPIPEを無視する場合:

$ time ( trap '' PIPE; echo foo; sleep 4s; echo hop2; sleep 10s; 
         echo hop3; ) | date 
Sat Apr 10 21:57:07 EEST 2021
bash: echo: write error: Broken pipe
bash: echo: write error: Broken pipe

real    0m14.006s
user    0m0.004s
sys     0m0.000s

SIGPIPEが無視されると、シェルは閉じたパイプに書き込むと定期的にエラーを返し、この目的でエラーが印刷されます。


左側のパイプへの書き込みと右側のパイプのクローズの間にはタイミングの問題もあります。

私が次のようにした場合:

( echo foo; echo bar; sleep 4s; ) | date;

それはsleep逃げた。ただし、両方を交換すると、LHSが到達する前に終了しechoます。 (ここでの競争はシステムによって異なります。)for i in {1..100}; do echo foo; done;sleep

そして、dateLHSで異なるケースを最初に検討してください。

(date; echo ${test}; sleep 50s; echo hop2; sleep 50s; echo hop3) | date

これもタイミング問題のためだ。date外部コマンドであるため、これを実行するシェルは内部で処理するよりも遅くなる可能性がありますecho(最終的にはすべてのシェルの隣に組み込まれたコマンドです)。これにより、date右側がレースで勝利し、最初の書き込み前にパイプを閉じる可能性が高くなりますecho


一般的に言えば、パイプに書き込むことは、一部のデータを相手に渡すことが目的ではなく、他のことがある場合に可能なSIGPIPEを処理する必要がある場合にはあまり役に立ちません。

答え3

date標準入力でデータを受信して​​いないため、送信するものはありません。

したがって、aを使用する|ことは完全に間違っています。私はあなたが使うべきだと思います&&

しかし、問題は次のとおりです。

各出力間の時間間隔は何ですか?4秒変える

それでも動作しますが、解釈するのは混乱しています。つまり、後続のコマンドecho hop2は実行されません。 2番目の睡眠は使用されませんでした。

$ (echo test; sleep 4s; echo hop2; sleep 4s; echo hop3) | date; date
Sun 11 Apr 2021 05:30:28 AM UTC
Sun 11 Apr 2021 05:30:32 AM UTC

stderr代わりに、以下stdoutを使用してすべてのコマンドを実行する場合:

$ (echo test >&2; sleep 4s; echo hop2 >&2; sleep 4s; echo hop3 >&2) | date; date

test
Sun 11 Apr 2021 05:32:36 AM UTC
hop2
hop3
Sun 11 Apr 2021 05:32:44 AM UTC

その理由は、datestdinから入力を受け取らず、ここに書き込もうとするとSIGPIPE応答を受け取り、全体(bash)シェル(左側のもの|)が閉じられるからです。リスト内の他のすべてのコマンドを削除します。

例では、最初の項目が成功した理由はecho時間によって異なります。

  1. 最初は、実行中のシェルがパイプを設定します|
  2. dateこれを行うには、launchコマンドのサブコマンドを開きます。
  3. 何も待たずに、シェルはパイプのもう一方の端に戻り(他のサブプロセスで)実行を要求しますecho ${test}
  4. パイプがまだ開いているため(date閉じていない)、echo出力がバッファリングされ、コマンドがsleep 4s実行されます。
  5. このdateコマンドには、パイプを完了して閉じるのに十分な時間があります。
  6. パイプが閉じていると、次のパイプechoに書き込むことができません。 SIGPIPEが発生します。
  7. SIGPIPEを受け取った後、左側のサブシェルは|きちんと終了します(エコーが組み込まれており、組み込みエラーが原因でシェル全体が失敗します)。
  8. パイプからコマンドは実行されなくなりました。

だから:

$ (sleep 1; echo test; sleep 4s; echo hop2 >&2; ) | date ; date 

Sun 11 Apr 2021 06:10:04 AM UTC
Sun 11 Apr 2021 06:10:05 AM UTC

最初のエコーも印刷しません。時間は0.1秒程度と短くできます。それは閉鎖を可能にするのに十分でしたdate

echo遅延するのに十分な外部コマンドへの呼び出しでもあります。

(/usr/bin/true; echo test; sleep 4s; echo confirm >&2 ) | date; date
Sun 11 Apr 2021 06:27:01 AM UTC
Sun 11 Apr 2021 06:27:01 AM UTC

ループforは上記を3回繰り返します。したがって、各ループ間にはわずか4秒しかありません。

関連情報