stdout / stderrがインターリーブされるのを防ぐには?

stdout / stderrがインターリーブされるのを防ぐには?

いくつかのプロセスを実行するとしましょう。

#!/usr/bin/env bash

foo &
bar &
baz &

wait;

上記のスクリプトを次のように実行します。

foobarbaz | cat

私が知る限り、すべてのプロセスがstdout / stderrに書き込むとき、出力はインターリーブされません。 stdioのすべての行は原子的なように見えます。どのように動作しますか?各行の原子性を制御するユーティリティは何ですか?

答え1

彼らは交差します!短い出力バーストだけを試みたが、分割されていないが、実際には特定の出力が分割されていないことを保証することは困難である。

出力バッファ

プログラムがどうなるかによって異なりますが、バッファー彼らの出力。これ標準入出力ライブラリほとんどのプログラムは、出力効率を向上させるために書き込み時にバッファを使用します。この関数は、プログラムがファイルに書き込むためにライブラリ関数を呼び出すとすぐにデータを出力せず、代わりにバッファにデータを保存し、バッファがいっぱいになった後にのみデータを出力します。これは、出力が一括して実行されることを意味します。より正確には、3つの出力モードがある。

  • バッファリングされていません:データはすぐに書き込まれ、バッファは使用されません。プログラムが出力を小さな塊(文字単位など)として記録すると、速度が遅くなる可能性があります。これは標準エラーの基本モードです。
  • 完全バッファリング:バッファがいっぱいになった場合にのみデータが書き込まれます。これは、パイプまたは通常のファイル(stderrを除く)に書き込むときのデフォルトモードです。
  • 行バッファリング:各改行文字の後、またはバッファがいっぱいになったときにデータが書き込まれます。これは端末に書き込むときのデフォルトモードです(stderrを除く)。

プログラムは、各ファイルが異なる動作をするように再プログラムでき、バッファを明示的にフラッシュできます。プログラムがファイルを閉じるか正常に終了すると、バッファは自動的にフラッシュされます。

同じパイプに書き込むすべてのプログラムがラインバッファモードを使用するか、バッファリングされていないモードを使用し、出力関数への単一呼び出しで各ラインを書き込む場合、ラインが単一ブロックを書き込むのに十分短い場合、出力はライン全体になります。インターレースの。ただし、プログラムの1つが完全バッファリングモードを使用する場合、または行が長すぎる場合は、混合行が表示されます。

以下は、2つのプログラムの出力をインターリーブした例です。私はLinuxでGNU coreutilsを使用しています。これらのユーティリティのバージョンによって異なる動作があります。

  • yes aaaaaaaa本質的に、行バッファモードと同じ方法で永久に作成します。ユーティリティyesは実際には一度に複数行を書きますが、出力をエクスポートするたびに出力は整数行です。
  • while true; do echo bbbb; done | grep bbbbb完全バッファリングモードで永久に書き込みます。バッファサイズは8192、行長は5バイトです。 5 は 8192 に分割できないため、書き込み間の境界は通常行の境界にありません。

それらを一つにまとめてみよう。

$ { yes aaaa & while true; do echo bbbb; done | grep b & } | head -n 999999 | grep -e ab -e ba
bbaaaa
bbbbaaaa
baaaa
bbbaaaa
bbaaaa
bbbaaaa
ab
bbbbaaa

ご覧のとおり、grepが時々中断され、その逆もあります。回線が切断される場合は約0.001%に過ぎませんが、そのようなことが発生します。出力はランダムであるため割り込みの数は異なりますが、毎回少なくともいくつかの割り込みが表示されます。行が長いと、バッファあたりの行数が減るほど、壊れる可能性が高くなり、切断された行の割合が高くなります。

これを行う方法はいくつかあります。出力バッファリングの調整。以下があります:

  • プログラムのデフォルト設定を変更せずに、stdioライブラリを使用するプログラムでバッファリングをオフにします。stdbuf -o0GNU coreutilsや他のシステム(FreeBSDなど)にあります。ラインバッファリングに切り替えを使用することもできますstdbuf -oL
  • この目的で生成されたターミナルブートローダの出力を介してラインバッファリングに切り替えます。unbuffer。一部のプログラムは、grep出力が端末の場合はデフォルトで色を使用するなど、他の方法で異なる動作をする場合があります。
  • 設定プログラム(例:--line-bufferedGNU grepに渡される)

上記のコードをもう一度見てみましょう。今回は両側にラインバッファリングがあります。

{ stdbuf -oL yes aaaa & while true; do echo bbbb; done | grep --line-buffered b & } | head -n 999999 | grep -e ab -e ba
abbbb
abbbb
abbbb
abbbb
abbbb
abbbb
abbbb
abbbb
abbbb
abbbb
abbbb
abbbb
abbbb

したがって、今回はyesはgrepを中断しませんが、grepは時々yesを中断します。理由は後で説明します。

パイプインターリーブ

各プログラムが一度に1行ずつ出力し、その行が十分に短い場合、出力行はきれいに分離されます。ただし、これを達成するにはキュー時間が制限されます。パイプ自体には転送バッファがあります。プログラムがパイプに出力されると、データはライターからパイプの転送バッファにコピーされ、パイプの転送バッファからリーダにコピーされます。 (少なくとも概念的には、カーネルは時々それを単一のコピーに最適化することができます。)

パイプ転送バッファが保持できるものよりもコピーするデータが多い場合、カーネルはバッファを一度に1つずつコピーします。複数のプログラムが同じパイプに書き込んでいて、カーネルが選択した最初のプログラムが複数のバッファに書き込もうとしている場合、カーネルが2番目に同じプログラムを再選択するという保証はありません。例えば、バッファサイズです。foo2*を書きたいです。バイトを書き込んでbar3バイトを書きたい場合は、可能なインターリーブの1つは次のとおりです。のバイトfoo、の3バイトbar、およびfoo.

上記のyes + grepの例に戻り、私のシステムはyes aaaa8192バイトのバッファに入ることができるだけ多くの行を一度に作成しました。 5バイトが記録されるため(4つの印刷可能文字と改行文字)、これは毎回8190バイトが記録されることを意味します。パイプバッファサイズは4096バイトです。したがって、yesで4096バイトを取得し、grepからいくつかの出力を取得し、yesから残りの書き込みを取得できます(8190 - 4096 = 4094バイト)。 819ラインの4096バイトとaaaaシングルaaabbbb

何が起こっているのかを詳しく知りたい場合は、システムのパイプバッファgetconf PIPE_BUF .サイズを教えてください。

strace -s9999 -f -o line_buffered.strace sh -c '{ stdbuf -oL yes aaaa & while true; do echo bbbb; done | grep --line-buffered b & }' | head -n 999999 | grep -e ab -e ba

きれいなラインインターリーブを保証する方法

ラインバッファリングは、ライン長がパイプバッファサイズより小さい場合、出力に混合ラインが現れないようにします。

行の長さが長い場合、複数のプログラムが同じパイプに書き込むときにランダムな混合を回避する方法はありません。分離を確実にするには、各プログラムが別のパイプに書き込まれ、1つのプログラムを使用して行を結合する必要があります。例えばGNUパラレルこれは基本的に行われます。

答え2

http://mywiki.wooledge.org/BashPitfalls#Non-atomic_writes_with_xargs_-Pこれは研究されました:

GNU xargs は複数のタスクを並列に実行することをサポートします。 -P n ここで、n は並列に実行するジョブの数です。

seq 100 | xargs -n1 -P10 echo "$a" | grep 5
seq 100 | xargs -n1 -P10 echo "$a" > myoutput.txt

これはほとんどの場合うまく機能しますが、1つの詐欺的な欠陥があります。 $aが1000文字を超えると、エコーがアトミックではなく(複数のwrite()呼び出しに分割される可能性があります)、2つのギルドが混在しています。

$ perl -e 'print "a"x2000, "\n"' > foo
$ strace -e write bash -c 'read -r foo < foo; echo "$foo"' >/dev/null
write(1, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"..., 1008) = 1008
write(1, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"..., 993) = 993
+++ exited with 0 +++

明らかに、echoまたはprintfが複数回呼び出されると、同じ問題が発生します。

slowprint() {
  printf 'Start-%s ' "$1"
  sleep "$1"
  printf '%s-End\n' "$1"
}
export -f slowprint
seq 10 | xargs -n1 -I {} -P4 bash -c "slowprint {}"
# Compare to no parallelization
seq 10 | xargs -n1 -I {} bash -c "slowprint {}"
# Be sure to see the warnings in the next Pitfall!

各ジョブは2つ(またはそれ以上)の別々のwrite()呼び出しで構成されているため、並列ジョブの出力は一緒に混在します。

したがって、混在していない出力が必要な場合は、出力が直列化されることを保証するツール(GNU Parallelなど)を使用することをお勧めします。

関連情報