web-dev-qa-db-ja.com

stdout / stderrがインターリーブするのを防ぐものは何ですか?

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

#!/usr/bin/env bash

foo &
bar &
baz &

wait;

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

foobarbaz | cat

私の知る限り、プロセスのいずれかがstdout/stderrに書き込む場合、それらの出力はインターリーブすることはありません。stdioの各行はアトミックなようです。それはどのように機能しますか?各行のアトミックを制御するユーティリティは何ですか?

14
Alexander Mills

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

出力バッファリング

プログラム buffer の出力方法によって異なります。ほとんどのプログラムが書き込み時に使用する stdioライブラリ は、出力をより効率的にするためにバッファを使用します。プログラムがライブラリ関数を呼び出してファイルに書き込むとすぐにデータを出力する代わりに、関数はこのデータをバッファーに格納し、バッファーがいっぱいになると実際にデータを出力します。つまり、出力はバッチで行われます。より正確には、3つの出力モードがあります。

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

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

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

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

  • yes aaaaは、本質的にラインバッファモードと同等の機能でaaaaを永久に書き込みます。 yesユーティリティは実際には一度に複数の行を書き込みますが、出力を出力するたびに、出力は整数の行になります。
  • echo bbbb; done | grep bは、完全にバッファリングされたモードでbbbbを永久に書き込みます。 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 -o0 GNU coreutilsおよびFreeBSDなどの他のシステムにあります。または、stdbuf -oLを使用して行バッファリングに切り替えることができます。
  • unbuffer を使用して、この目的のために作成されたターミナルを介してプログラムの出力を送信することにより、ラインバッファリングに切り替えます。一部のプログラムは、他の方法で異なる動作をする場合があります。たとえば、出力がターミナルの場合、grepはデフォルトで色を使用します。
  • たとえば、--line-bufferedをGNU 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回目に同じプログラムを再度選択する保証はありません。たとえば、[〜#〜] p [〜#〜]がバッファサイズの場合、fooは2 *[〜#〜] p [〜#〜]バイトとbarが3バイトの書き込みを希望している場合、1つの可能なインターリーブは[〜#〜] p [〜#〜]fooからのバイト、次にbarからの3バイト、および[〜#〜] p [〜#〜]fooからのバイト。

上記のyes + grepの例に戻ると、私のシステムでは、yes aaaaがたまたま8192バイトのバッファーに収まるだけの行を一度に書き込みます。書き込むバイト数は5バイト(印刷可能な4文字と改行)であるため、毎回8190バイトを書き込むことになります。パイプバッファーサイズは4096バイトです。したがって、yesから4096バイト、次にgrepから一部の出力、そしてyesから残りの書き込みを取得することができます(8190-4096 = 4094バイト)。 4096バイトは、aaaaと1つのaで819行分のスペースを残します。したがって、このaが1つある行に続いて、grepからの1つの書き込みがあり、abbbbが1行あります。

何が起こっているかの詳細を確認したい場合は、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

クリーンなラインインターリーブを保証する方法

行の長さがパイプのバッファーサイズよりも小さい場合、行のバッファリングにより、出力に行が混在しないことが保証されます。

行の長さが長くなる可能性がある場合、複数のプログラムが同じパイプに書き込むときに任意の混合を回避する方法はありません。確実に分離するには、各プログラムを異なるパイプに書き込み、プログラムを使用して行を結合する必要があります。たとえば GNU Parallel はデフォルトでこれを行います。

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

これは多くの状況で問題なく機能しますが、不正な欠陥があります。$ 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など)を使用することをお勧めします。

1
Ole Tange