同じパイプで同じファイルを読み書きすることを常に「失敗」させる方法は何ですか?

同じパイプで同じファイルを読み書きすることを常に「失敗」させる方法は何ですか?

次のスクリプトがあるとしましょう。

#!/bin/bash
for i in $(seq 1000)
do
    cp /etc/passwd tmp
    cat tmp | head -1 | head -1 | head -1 > tmp  #this is the key line
    cat tmp
done

重要な行で同じファイルを読み書きすることtmpは時々失敗します。

(これは競合状態のためであることを読んでいます。パイプラインのプロセスが並列に実行されるため、なぜなのか理解できませんhead。可能です。)

スクリプトを実行すると約200行が出力されます。このスクリプトが常にゼロ行を出力するように強制する方法はありますかtmp? (したがって、I / Oリダイレクトは常に最初に準備され、データは常に削除されます。)明らかに、これはスクリプトではなくシステム設定を変更することを意味します。

あなたの考えに感謝します。

答え1

競争条件が存在する理由

パイプラインの両方が順番に実行されるのではなく、並列に実行されます。これを示す非常に簡単な方法があります。

time sleep 1 | sleep 1

これには2秒ではなく1秒かかります。

シェルは2つのサブプロセスを開始し、完了するのを待ちます。両方のプロセスは並列に実行されます。どちらか一方が他のプロセスと同期される唯一の理由は次のとおりです。必要お互いを待ってください。最も一般的な同期点は、右側が標準入力からのデータの読み込みを待つのをブロックし、左側がより多くのデータを書き込むときにブロックを解除する場所です。逆の場合も発生する可能性があります。右側がデータを読み取る速度が遅く、右側がより多くのデータを読み取るまで、左側が書き込み操作をブロックします(パイプ自体にはパイプによって管理されるバッファがあります)、カーネルの最大サイズは小さくなります。 )。

同期点を観察するには、次のコマンドに従ってください(sh -x各コマンドが実行されると印刷)。

time sh -x -c '{ sleep 1; echo a; } | { cat; }'
time sh -x -c '{ echo a; sleep 1; } | { cat; }'
time sh -x -c '{ echo a; sleep 1; } | { sleep 1; cat; }'
time sh -x -c '{ sleep 2; echo a; } | { cat; sleep 1; }'

観察した結果に満足するまで、さまざまなバリエーションを試してください。

複合コマンドが与えられると

cat tmp | head -1 > tmp

左側のプロセスは次のことを行います(説明に関連するステップのみをリストしました)。

  1. catパラメータを使用して外部プログラムを実行しますtmp
  2. 読書用に開いていますtmp
  3. ファイルの終わりに達していない場合は、ファイルからブロックを読み取り、標準出力に書き込みます。

右側のプロセスは次のことを行います。

  1. 標準出力にリダイレクトtmpし、プロセスからファイルを切り捨てます。
  2. headパラメータを使用して外部プログラムを実行します-1
  3. 標準入力から1行を読み、それを標準出力に書き込みます。

唯一の同期点は、left-3がフルライン処理を完了するのを待つright-3です。 left-2 と right-1 の間に同期がないため、どの順序でも発生する可能性があります。発生順序は予測できません。 CPU アーキテクチャ、シェル、カーネル、プロセスが予約されるコア、その時点で CPU が受け取る割り込みなどによって異なります。

行動を変える方法

システム設定を変更して動作を変更することはできません。コンピュータはユーザーの指示に従います。トリミングしてtmp並列に読み取るように指示するtmpと、2つの操作が並列に実行されます。

まあ、変更できる「システム設定」があります。/bin/bashこれをbash以外のプログラムに置き換えることができます。これが良い考えではないということは言うまでもないことでありますように。

パイプの左前にカットが発生するようにするには、パイプの外側に配置する必要があります。たとえば、次のようになります。

{ cat tmp | head -1; } >tmp

または

( exec >tmp; cat tmp | head -1 )

なぜあなたがこれをしたいのかわかりません。空であることを知っているファイルから読むのはなぜですか?

代わりに、読み取りが完了した後に出力リダイレクト(切り捨てを含む)が発生するようにするには、catメモリにデータを完全にバッファリングする必要があります。

line=$(cat tmp | head -1)
printf %s "$line" >tmp

または別のファイルに書き込み、その場所に移動します。これは通常、スクリプトで作業を実行する安定した方法であり、ファイルが元の名前で表示される前に完全に作成されるという利点があります。

cat tmp | head -1 >new && mv new tmp

これその他のユーティリティコレクションには、これを行うように特別に設計されたというプログラムが含まれていますsponge

cat tmp | head -1 | sponge tmp

問題を自動的に検出する方法

あなたの目標が間違って書かれたスクリプトをインポートし、自動的に何が間違っているのかを見つけることであれば申し訳ありません。人生はそれほど簡単ではありません。場合によってはcat、切り捨てが発生する前に読み取りが完了するため、ランタイム分析では問題を確実に見つけることができません。静的分析は、原則として質問キャプチャの単純化された例を介して実行できます。住宅検査しかし、より複雑なスクリプトでは、同様の問題を捉えることができない可能性があります。

答え2

Gilesの答えは競争条件を説明します。この部分だけ答えたいです。

このスクリプトが常にゼロ行を出力するように強制する方法はありますか? (したがって、tmpへのI / Oリダイレクトは常に最初に準備されるため、データは常に破損します。)明確に言うと、システム設定を変更することを意味します。

そのようなツールがすでにあるかどうかはわかりませんが、実装方法はわかっています。 (ただし、そうでないことを参考にしてください。いつも0行、このような簡単な一致項目を簡単にキャッチできる便利なテスターであり、一部より複雑なマッチ。バラより@gilesでコメント.) スクリプトが安全であることを保証するものではありません。しかし、ARMのような弱い順序の非x86 CPUを含む、さまざまなCPUでマルチスレッドプログラムをテストするのと同様に、テストに役立つツールになる可能性があります。

これは次のように実行できます。racechecker bash foo.sh

strace -fltrace -f各サブプロセスに接続されているのと同じシステムコールトレース/インターセプトツールを使用します。 (Linuxでも同じだ。ptraceGDBやその他のデバッガで使用されるシステムコールブレークポイントを設定し、単一ステップを実行し、他のプロセスのメモリ/レジスタを変更します。 )

検出openopenatシステムコール:このツールで実行されているプロセスシステムopen(2)コール(またはopenat)を使ってO_RDONLY約1/2〜1秒間眠ります。別のopenシステムコール(特にインクルードO_TRUNC)がすぐに実行されるようにします。

これにより、システムの負荷も高くないか、他の読み取りが終了するまで切り捨てが発生しない複雑な競合状態でない限り、ほとんどすべての競合条件で作成者が勝利することができます。だからopen()s(sまたはwriteのいずれかread())はランダムな変更のために遅れます。このツールの検出機能は向上しますが、実際には現実の世界で遭遇する可能性のあるすべてのシナリオを最終的にカバーする遅延時間シミュレータを使用して、無制限の時間テストなしではそうすることはできません。確かにスクリプトを注意深く読み、そうでないことを証明しない限り、スクリプトには競合はありません。


プロセスの開始に時間がかからないように、遅延の代わりにopenファイルをホワイトリストに追加する必要があります。 (動的接続は実行時に複数のファイルに接続する必要があります(表示または時折)。ただし、親シェル自体が切り捨てを行ってもかまいません。ただし、スクリプトを非合理的に遅くしない方がまだこのツールに適しています。)/usr/bin/usr/libopen()strace -eopen /bin/true/bin/ls

または、呼び出しプロセスが最初に切り捨てる権限を持たないすべてのファイルをホワイトリストに追加することもできます。つまり、トレースプロセスは、ファイルがaccess(2)必要なプロセスを実際に中断する前にシステムコールを実行できます。open()


racecheckerコード自体はシェルではなくCで書かなければなりませんが、straceコードを始点として使用することができ、実装に多くの作業は必要ありません。

同じ機能を得ることもできます。FUSEファイルシステムの使用。純粋なパススルーファイルシステムのFUSEの例があるため、open()読み取り専用オープンでスリープ機能にチェックを追加できますが、すぐに切り捨てが発生します。

関連情報