次のスクリプトがあるとします。
#!/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行を出力するように強制する方法はありますか(したがって、tmp
へのI/Oリダイレクトは常に最初に準備され、データは常に破棄されます)?明確にするために、このスクリプトではなく、システム設定を変更することを意味します。
あなたのアイデアをありがとう。
Gillesの回答は競合状態を説明しています。私はこの部分に答えるつもりです:
このスクリプトが常に0行を出力するように強制する方法はありますか(そのため、tmpへのI/Oリダイレクトは常に最初に準備され、データは常に破棄されます)?明確にするために、私はシステム設定を変更することを意味します
このためのツールがすでに存在する場合はIDKですが、どのように実装できるかについては考えています。 (ただし、これは always 0行ではなく、このような単純なレースを簡単にキャッチできる便利なテスターであり、 some より複雑です @ Gilles 'comment を参照してください。)スクリプトが安全であることを保証するものではありませんですが、マルチスレッドのテストと同様に、テストに役立つツールとなる可能性がありますARMなどの弱く順序付けされた非x86 CPUを含む、異なるCPU上のプログラム。
_racechecker bash foo.sh
_として実行します
_strace -f
_および_ltrace -f
_がすべての子プロセスにアタッチするために使用するのと同じシステムコールトレース/インターセプト機能を使用します。 (Linuxでは、これは同じです GDBおよび他のデバッガーで使用されるptrace
システムコール ブレークポイントの設定、シングルステップ、および別のプロセスのメモリ/レジスターの変更。)
open
およびopenat
システムコールをインストルメント化します。このツールで実行されているプロセスが an open(2)
システムコール (またはopenat
)_O_RDONLY
_を使用して、1/2秒または1秒スリープします。他のopen
システムコール(特に_O_TRUNC
_を含むもの)を遅滞なく実行させます。
これにより、システムの負荷も高くない場合や、他の読み取りが行われるまで切り捨てが発生しない複雑な競合状態でない限り、ライターはほぼすべての競合状態で競合に勝てるはずです。したがって、ランダムなバリエーションのopen()
s(およびread()
sまたは書き込み)が遅延するこのツールの検出力が向上しますが、もちろん、現実世界で遭遇する可能性のあるすべての可能な状況を最終的にカバーする遅延シミュレーターで無限の時間、確実スクリプトを読んでいない限り、レースから解放されることはできません注意深く、そうでないことを証明してください。
_/usr/bin
_と_/usr/lib
_のファイルをホワイトリストに登録して(open
を遅らせるのではなく)必要とするので、プロセスの起動に時間がかかりません。 (ランタイムダイナミックリンクは、複数のファイルをopen()
する必要があります(_strace -eopen /bin/true
_または_/bin/ls
_を参照)。ただし、親シェル自体が切り捨てを行っている場合は問題ありません。しかし、このツールがスクリプトを不当に遅くしないようにするためには依然として良いでしょう)。
または、呼び出しプロセスが最初に切り捨てる権限を持っていないすべてのファイルをホワイトリストに登録することもできます。つまり、トレースプロセスは、ファイルをaccess(2)
したいプロセスを実際に中断する前にopen()
システムコールを実行できます。
racechecker
自体はシェルではなくCで作成する必要がありますが、strace
のコードを開始点として使用でき、実装にそれほどの労力を要しない場合があります。
おそらく同じ機能 Fuseファイルシステムを使用 を取得できます。おそらく純粋なパススルーファイルシステムのFuseの例があるので、open()
関数にチェックを追加して、読み取り専用のオープンでスリープさせるが、すぐに切り捨てを行わせることができます。
パイプの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
左側のプロセスは次のことを行います(説明に関連する手順のみを記載しています)。
cat
を引数tmp
で実行します。tmp
を開いて読み取ります。右側のプロセスは次のことを行います。
tmp
にリダイレクトし、プロセスでファイルを切り捨てます。-1
を指定して外部プログラムhead
を実行します。同期の唯一のポイントは、right-3がleft-3が1行を処理するのを待つことです。 left-2とright-1の間には同期はないため、どちらの順序でも発生する可能性があります。それらがどの順序で発生するかは予測できません。それは、CPUアーキテクチャ、シェル、カーネル、プロセスがスケジュールされているコア、その時点でCPUが受け取る割り込みなどに依存します。
システム設定を変更して動作を変更することはできません。コンピュータはあなたがそれをするように言うことをします。 tmp
を切り捨ててtmp
から並列に読み取るように指示したので、2つの処理が並列に行われます。
変更できる「システム設定」が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 によって捕捉されますが、より複雑なスクリプトでは同様の問題を捕捉しない可能性があります。