同じパイプラインで同じファイルの読み取りと書き込みを常に「失敗」させる方法は?


9

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

#!/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行が出力されます。このスクリプトが常に0行を出力するように強制できる方法はありますか(そのため、I / Oリダイレクトtmpは常に最初に準備され、データは常に破棄されます)?明確にするために、このスクリプトではなく、システム設定を変更することを意味します。

あなたのアイデアをありがとう。

回答:


2

Gillesの回答は競合状態を説明しています。私はこの部分に答えるつもりです:

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

このためのツールが既に存在する場合はIDKですが、どのように実装できるかについて考えがあります。(ただし、これは常に 0行ではないことに注意してください。このような単純なレースを簡単にキャッチする便利なテスターと、いくつかのより複雑なレースがあります。@ Gillesのコメントを参照してください。) スクリプトが安全あるとは限りませんが、 ARMなどの弱く順序付けされた非x86 CPUを含む、異なるCPUでマルチスレッドプログラムをテストするのと同様に、テストに役立つツールです。

あなたはそれを実行するでしょう racechecker bash foo.sh

施設/トレース傍受同じシステム・コールを使用strace -fしてltrace -f、すべての子プロセスにアタッチするために使用します。(Linuxでは、これは、ptraceGDBおよび他のデバッガーがブレークポイントの設定、シングルステップ、および別のプロセスのメモリ/レジスターの変更に使用するシステムコールと同じです。)

openおよびopenatシステムコールをインストルメント化します。このツールで実行されているプロセスがを使用しopen(2)システムコール(またはopenat)を実行するとO_RDONLY、1/2または1秒間スリープします。他のopenシステムコール(特にを含むO_TRUNC)を遅滞なく実行させます。

これにより、システムの負荷も高くない場合や、他の読み取りが行われるまで切り捨てが発生しない複雑な競合状態である場合を除き、ライターはほぼすべての競合状態で競合に勝てるはずです。したがって、どのopen()s(およびおそらくread()sまたは書き込み)が遅延するをランダムに変化させると、このツールの検出能力が向上しますが、もちろん、最終的に発生する可能性のあるすべての状況をカバーする遅延シミュレーターで無限の時間をテストしなくても現実の世界では、スクリプトを注意深く読んでそうでないことを証明しない限り、スクリプトが人種から解放されていると確信することはできません。


あなたはおそらくホワイトリスト(ない遅延にそれを必要がありますopen内のファイル用)/usr/binおよび/usr/libプロセス起動しては永遠なりません。(ランタイムダイナミックリンクではopen()複数のファイルを参照するstrace -eopen /bin/true必要が/bin/lsあります(ただし、場合によっては))が、親シェル自体が切り捨てを行っている場合は問題ありません。ただし、このツールでスクリプトが不当に遅くならないようにしても問題はありません)。

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


racecheckerそれ自体はシェルではなくCで記述する必要がありますが、straceのコードを開始点として使用でき、実装にそれほどの労力を要しない場合があります。

FUSEファイルシステムでも同じ機能が得られるかもしれません。おそらく純粋なパススルーファイルシステムのFUSEの例があるので、open()関数にチェックを追加して、読み取り専用のオープンのためにスリープさせるが、すぐにトランケーションが発生するようにすることができます。


レースチェッカーのアイデアは実際には機能しません。最初に、タイムアウトが信頼できないという問題があります。ある日、他の人が予想よりも長くかかります(これは、ビルドスクリプトまたはテストスクリプトの古典的な問題であり、しばらくは機能しているように見え、その後、デバッグが難しい方法で失敗します。ワークロードが拡大し、多くのものが並行して実行される場合)。しかし、これを超えて、どのオープンに遅延を追加しますか?興味深いものを検出するには、さまざまな遅延パターンで多くの実行を行い、それらの結果を比較する必要があります。
Gilles「SO-邪悪なことをやめなさい」

@ギルズ:そうです、かなり短い遅延は、トランケートがレースに勝つことを保証しませ(あなたが指摘するように負荷の高いマシン上で)。ここでの考え方は、これを数回スクリプトのテストに使用することでracecheckerあり、常に使用することではありません。そしておそらく、あなたはそれをより高く設定したい非常に負荷の高いマシンの人々のために、10秒のように構成して、読み取り可能なオープンスリープ時間を設定したいと思うでしょう。または再オープンされたファイルという長いまたは非効率的なスクリプトのために0.1秒のように、下にそれを設定たくさん
Peter Cordes

@ギレス:さまざまな遅延パターンについての素晴らしいアイデア。これにより、OPの場合のように、(シェルがどのように機能するかがわかったら)明白になるはずの単純な同じパイプライン内のものよりも多くのレースをキャッチできます。しかし、「どっちが開く?」読み取り専用のオープン、ホワイトリスト、またはプロセスの起動を遅らせない他の方法
Peter Cordes

他のプロセスが完了するまで切り捨てられないバックグラウンドジョブを使用した、より複雑なレースを考えていると思いますか?ええ、それをキャッチするにはランダムなバリエーションが必要かもしれません。または、プロセスツリーを確認して、通常の順序を逆にしようとして、「早期」の読み取りをさらに遅らせます。ますます多くの並べ替えの可能性をシミュレートするためにツールをますます複雑にすることができますが、マルチタスクを実行している場合は、プログラムを正しく設計する必要がある場合があります。自動化されたテストは、起こり得る問題がより限定されている単純なスクリプトに役立つ場合があります。
Peter Cordes

これは、マルチスレッドコードのテスト、特にロックレスアルゴリズムのテストに非常に似ています。それが正しい理由についての論理的な推論は非常に重要です。また、特定のマシンのセットでのテストを信頼してすべての順序変更を生成することはできないためです。すべての抜け穴を塞がないと問題になります。しかし、単にARMまたはPowerPCのような弱命じたアーキテクチャでテストのような人工的なものを公開することができます遅らせるというシステムの下でスクリプトをテストし、実際には良いアイデアですいくつかのことが何もないよりはましだので、レースを。キャッチできないバグはいつでも導入できます!
Peter Cordes

18

なぜ競合状態があるのか

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

time sleep 1 | sleep 1

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

シェルは2つの子プロセスを開始し、両方が完了するのを待ちます。これら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行読み取り、標準出力に書き込みます。

同期の唯一のポイントは、right-3がleft-3が1行を処理するのを待つことです。左2と右1の間には同期がないため、どちらの順序でも発生する可能性があります。それらが発生する順序は予測できません。それは、CPUアーキテクチャ、シェル、カーネル、プロセスがスケジュールされているコア、その時間の周りにCPUが受け取る割り込みなどに依存します。

動作を変更する方法

システム設定を変更して動作を変更することはできません。コンピュータはあなたがそれをするように言うことをします。切り捨てて並列にtmp読み取るように指示したtmpので、2つの処理が並列に行われます。

OK、変更できる「システム設定」が1つあります/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

moreutilsのコレクションはちょうどと呼ばれる、というんプログラムを含んでいますsponge

cat tmp | head -1 | sponge tmp

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

不適切に記述されたスクリプトを取り、どこが壊れているかを自動的に把握することが目的である場合、残念ながら、人生はそれほど単純ではありません。cat切り捨てが発生する前に読み取りが終了することがあるので、ランタイム分析では問題を確実に検出できません。静的分析は原則としてそれを行うことができます。質問の簡略化された例はShellcheckによって捕捉されますが、より複雑なスクリプトでは同様の問題を捕捉しない可能性があります。


それが私の目標でした。スクリプトが適切に記述されているかどうかを判断することです。スクリプトがこの方法でデータを破壊した可能性がある場合、私はそれを毎回破壊したかっただけです。これがほぼ不可能だと聞いてはいけません。おかげさまで、私は問題が何であるかを知っており、解決策を考えようとします。
Karlosss

@karlosss:うーん、同じシステムコールトレース/インターセプトstrace(つまりLinux ptrace)を使用して、すべてopenの読み取り用システムコール(すべての子プロセス内)を0.5秒間スリープさせることができるので、打ち切り、打ち切りはほとんど常に勝つでしょう。
Peter Cordes

@PeterCordes私はこれが初心者です。これを達成する方法を管理し、それを回答として書き込むことができれば、私はそれを受け入れます。
Karlosss

@PeterCordesトランケーションが遅れて勝つことは保証できません。ほとんどの場合は機能しますが、負荷の高いマシンでは、スクリプトがおかしな方法で失敗することがあります。
Gilles「SO-邪悪なことをやめよ」

@ギレス:私の答えの下でこれを議論しましょう。
Peter Cordes
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.