stdout / stderrのインターリーブを妨げるものは何ですか?


13

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

#!/usr/bin/env bash

foo &
bar &
baz &

wait;

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

foobarbaz | cat

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


3
コマンドはどのくらいのデータを出力しますか?それらを数キロバイト出力してみてください。
クサラナナンダ

コマンドの1つが改行の前に数KBを出力する場所を意味しますか?
アレクサンダーミルズ

いいえ、次のようなもの:unix.stackexchange.com/a/452762/70524
muru

回答:


22

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

出力バッファリング

プログラムがどのように出力をバッファリングするかによります。stdioライブラリ彼らが使用するバッファを書いているとき、ほとんどのプログラムは出力をより効率的に使用します。プログラムがライブラリ関数を呼び出してファイルに書き込むとすぐにデータを出力する代わりに、関数はこのデータをバッファに保存し、バッファがいっぱいになるとデータを実際に出力します。これは、出力がバッチで実行されることを意味します。より正確には、3つの出力モードがあります。

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

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

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

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

  • yes aaaaaaaa基本的に行バッファモードと同等の方法で永久に書き込みます。yesユーティリティは、実際には一度に複数の行を書き込み、それは出力を発するたびに、出力は、行の整数です。
  • 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%だけが中断されましたが、それは起こりました。出力はランダム化されているため、割り込みの数は異なりますが、毎回少なくともいくつかの割り込みが発生しました。バッファーあたりの行数が減少すると中断の可能性が高くなるため、行が長くなると中断された行の割合が高くなります。

出力バッファリング調整する方法はいくつかあります。主なものは次のとおりです。

  • stdbuf -o0GNU coreutilsやその他のFreeBSDなどのシステムにあるプログラムで、デフォルト設定を変更せずにstdioライブラリを使用するプログラムのバッファリングをオフにします。または、でラインバッファリングに切り替えることもでき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バッファーフルをコピーします。複数のプログラムが同じパイプに書き込みを行っており、カーネルが選択する最初のプログラムが複数のバッファーフルを書き込みたい場合、カーネルが再度同じプログラムを選択するという保証はありません。たとえば、Pがバッファサイズで、foo2 * Pバイトbarを書き込み、3バイトを書き込みたい場合、可能なインターリーブは、Pバイトfrom foo、次に3バイトfrom barPバイトfrom fooです。

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

何が起こっているかの詳細を確認したい場合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はデフォルトでこれを行います。


興味深いのでcat、catプロセスがfoo / bar / bazから行全体を受け取るが、別の行から半分の行を受け取らないように、すべての行がアトミックに書き込まれることを保証する良い方法かもしれません。 bashスクリプトでできることはありますか?
アレクサンダーミルズ

1
これは、数百のファイルがありawk、同じIDに対して2行(またはそれ以上)の出力find -type f -name 'myfiles*' -print0 | xargs -0 awk '{ seen[$1]= seen[$1] $2} END { for(x in seen) print x, seen[x] }'find -type f -name 'myfiles*' -print0 | xargs -0 cat| awk '{ seen[$1]= seen[$1] $2} END { for(x in seen) print x, seen[x] }'生成されたが、IDごとに1行のみが正しく生成された場合にも当てはまります。
αғsнιη

インターリーブを防止するために、Node.jsのようなプログラミング環境でそれを行うことができますが、bash / shellを使用しますが、その方法はわかりません。
アレクサンダーミルズ

1
@JoLパイプバッファがいっぱいになったことが原因です。ストーリーの2番目の部分を書く必要があることはわかっていました…完了しました。
ジル 'SO-悪であるのをやめる'

1
@OlegzandrDenman TLDRが追加されました:それらはインターリーブを行います。理由は複雑です。
ジル 'SO-悪であるのをやめる'

1

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など)を使用することをお勧めします。


そのセクションは間違っています。xargs echoecho bashビルトインではなく、echoからのユーティリティを呼び出します$PATH。とにかく、bash 4.4でそのbashエコー動作を再現できません。Linuxでは、4Kより大きいパイプ(/ dev / nullではない)への書き込みは、アトミックであるとは限りません。
ステファンシャゼラス
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.