場合によっては、シェルスクリプトの1つのインスタンスのみが同時に実行されていることを確認する必要があります。
たとえば、それ自体ではロックを提供しないcrondを介して実行されるcronジョブ(デフォルトのSolaris crondなど)。
ロックを実装する一般的なパターンは、次のようなコードです。
_#!/bin/sh
LOCK=/var/tmp/mylock
if [ -f $LOCK ]; then # 'test' -> race begin
echo Job is already running\!
exit 6
fi
touch $LOCK # 'set' -> race end
# do some work
rm $LOCK
_
もちろん、そのようなコードには競合状態があります。 2つのインスタンスの実行がどちらも3行目から進んでから、_$LOCK
_ファイルにアクセスできるようになるまでの時間枠があります。
Cronジョブの場合、2つの呼び出しの間に分単位の間隔があるため、これは通常問題にはなりません。
しかし、問題が発生する可能性があります-たとえば、ロックファイルがNFSサーバーにある場合-ハングします。その場合、いくつかのcronジョブが3行目でブロックしてキューに入る可能性があります。 NFSサーバーが再びアクティブになると、並列実行中のジョブが thundering herd になります。
Webで検索したところ、ツール lockrun が見つかりました。これは、その問題に対する適切な解決策のようです。それを使用して、次のようなロックが必要なスクリプトを実行します。
_$ lockrun --lockfile=/var/tmp/mylock myscript.sh
_
これをラッパーに入れるか、crontabから使用できます。
可能な場合はlockf()
(POSIX)を使用し、flock()
(BSD)にフォールバックします。また、NFSでのlockf()
サポートは比較的広範囲に及ぶはずです。
lockrun
に代わるものはありますか?
他のcronデーモンはどうですか?健全な方法でロックをサポートする一般的なcrondはありますか? Vixie Crond(Debian/Ubuntuシステムのデフォルト)のmanページをざっと見ても、ロックについては何も表示されません。
lockrun
のようなツールを coreutils に含めることは良い考えでしょうか?
私の意見では、それは timeout
、Nice
および友人と非常によく似たテーマを実装しています。
上記の競合状態を防ぐことができるシェルスクリプトでロックを行う別の方法を次に示します。2つのジョブが両方とも3行目を通過する場合があります。noclobber
オプションはkshとbashで機能します。 csh/tcshでスクリプトを作成するべきではないため、set noclobber
を使用しないでください。 ;)
lockfile=/var/tmp/mylock
if ( set -o noclobber; echo "$$" > "$lockfile") 2> /dev/null; then
trap 'rm -f "$lockfile"; exit $?' INT TERM EXIT
# do stuff here
# clean up after yourself, and release your trap
rm -f "$lockfile"
trap - INT TERM EXIT
else
echo "Lock Exists: $lockfile owned by $(cat $lockfile)"
fi
NFSでロックされたYMMV(NFSサーバーに到達できない場合)ですが、一般に、以前よりもはるかに堅牢です。 (10年前)
複数のサーバーから同時に同じことを行うcronジョブがあり、実際に実行する必要があるのは1つのインスタンスだけである場合、このようなものがうまくいく場合があります。
私はロックランの経験はありませんが、スクリプトが実際に実行される前に事前に設定されたロック環境があると役立つ場合があります。またはそうでないかもしれません。あなたはラッパーのスクリプトの外でロックファイルのテストを設定しているだけであり、理論的には、2つのジョブがlockrunによってまったく同時に呼び出された場合、「inside-スクリプトのソリューション?
とにかく、ファイルのロックはシステムの動作をかなり尊重し、実行前にロックファイルの存在を確認しないスクリプトは、実行しようとしていることを何でも実行します。ロックファイルテストと適切な動作を設定するだけで、100%ではないにしても、潜在的な問題の99%を解決できます。
ロックファイルの競合状態が頻繁に発生する場合は、ジョブのタイミングが適切でないなど、より大きな問題のインジケータである可能性があります。あるいは、間隔がジョブの完了ほど重要でない場合は、ジョブがデーモン化されるのに適している可能性があります。
以下の@Clint Pachlのコメントに基づいて、ksh88を使用する場合は、mkdir
ではなくnoclobber
を使用してください。これにより、潜在的な競合状態が緩和されますが、完全に制限されるわけではありません(リスクはごくわずかです)。詳細については Clintが以下に掲載したリンク を参照してください。
lockdir=/var/tmp/mylock
pidfile=/var/tmp/mylock/pid
if ( mkdir ${lockdir} ) 2> /dev/null; then
echo $$ > $pidfile
trap 'rm -rf "$lockdir"; exit $?' INT TERM EXIT
# do stuff here
# clean up after yourself, and release your trap
rm -rf "$lockdir"
trap - INT TERM EXIT
else
echo "Lock Exists: $lockdir owned by $(cat $pidfile)"
fi
また、追加の利点として、スクリプトでtmpfilesを作成する必要がある場合は、lockdir
ディレクトリを使用して、スクリプトの終了時にクリーンアップされることを確認できます。
よりモダンなbashの場合、上部にあるnoclobberメソッドが適しています。
私はハードリンクを使用することを好みます。
_lockfile=/var/lock/mylock
tmpfile=${lockfile}.$$
echo $$ > $tmpfile
if ln $tmpfile $lockfile 2>&-; then
echo locked
else
echo locked by $(<$lockfile)
rm $tmpfile
exit
fi
trap "rm ${tmpfile} ${lockfile}" 0 1 2 3 15
# do what you need to
_
ハードリンクはNFSを介してアトミックです 、そして大部分は mkdirも同様です 。 mkdir(2)
またはlink(2)
の使用は、実用レベルではほぼ同じです。 NFSの実装では、アトミックmkdir
よりもアトミックハードリンクが許可されているため、私はハードリンクの使用を好みます。 NFSの最新リリースでは、どちらを使用するかを心配する必要はありません。
mkdir
はアトミックであることを理解しているので、おそらく:
lockdir=/var/tmp/myapp
if mkdir $lockdir; then
# this is a new instance, store the pid
echo $$ > $lockdir/PID
else
echo Job is already running, pid $(<$lockdir/PID) >&2
exit 6
fi
# then set traps to cleanup upon script termination
# ref http://www.shelldorado.com/goodcoding/tempfiles.html
trap 'rm -r "$lockdir" >/dev/null 2>&1' 0
trap "exit 2" 1 2 3 13 15
簡単な方法は lockfile
を使用することです。通常はprocmail
パッケージに付属しています。
LOCKFILE="/tmp/mylockfile.lock"
# try once to get the lock else exit
lockfile -r 0 "$LOCKFILE" || exit 0
# here the actual job
rm -f "$LOCKFILE"
GNU sem
の一部として提供されるparallel
ツールは、あなたが探しているものかもしれません:
sem [--fg] [--id <id>] [--semaphoretimeout <secs>] [-j <num>] [--wait] command
のように:
sem --id my_semaphore --fg "echo 1 ; date ; sleep 3" &
sem --id my_semaphore --fg "echo 2 ; date ; sleep 3" &
sem --id my_semaphore --fg "echo 3 ; date ; sleep 3" &
出力:
1
Thu 10 Nov 00:26:21 UTC 2016
2
Thu 10 Nov 00:26:24 UTC 2016
3
Thu 10 Nov 00:26:28 UTC 2016
順序は保証されていないことに注意してください。また、出力は終了するまで表示されません(イライラする!)。しかし、それでも、ロックファイルや再試行、クリーンアップを心配せずに、同時実行を防ぐ最も簡単な方法です。
私は dtach
を使用します。
$ dtach -n /tmp/socket long_running_task ; echo $?
0
$ dtach -n /tmp/socket long_running_task ; echo $?
dtach: /tmp/socket: Address already in use
1
ファイルを使用しないでください。
スクリプトが次のように実行された場合:
bash my_script
次のコマンドを使用して、実行中かどうかを検出できます。
running_proc=$(ps -C bash -o pid=,cmd= | grep my_script);
if [[ "$running_proc" != "$$ bash my_script" ]]; do
echo Already locked
exit 6
fi
ただし、ps
を使用したいくつかの壊れた半実用的なアプローチと、それらが持つ多くの警告について説明したいと思います。
この答えは「シェルでのロックの処理にps
とgrep
を使用しないのはなぜですか?」に対する答えです。
最初に、 別の答え で与えられたアプローチは、機能せず(また、実行できず)、明らかにテストされなかったという事実にもかかわらず、いくつかの賛成票を持っています。
running_proc=$(ps -C bash -o pid=,cmd= | grep my_script);
if [[ "$running_proc" != "$$ bash my_script" ]]; do
echo Already locked
exit 6
fi
構文エラーと壊れたps
引数を修正して取得しましょう:
running_proc=$(ps -C bash -o pid,cmd | grep "$0");
echo "$running_proc"
if [[ "$running_proc" != "$$ bash $0" ]]; then
echo Already locked
exit 6
fi
このスクリプトは、実行方法に関係なく、毎回always 6を終了します。
./myscript
を指定して実行すると、ps
の出力は12345 -bash
になり、必要な文字列12345 bash ./myscript
と一致しないため、失敗します。
bash myscript
を指定して実行すると、より興味深いものになります。 bashプロセスforksはパイプラインを実行し、childシェルはps
およびgrep
を実行します。元のシェルと子シェルの両方がps
出力に次のように表示されます。
25793 bash myscript
25795 bash myscript
これは予想される出力$$ bash $0
ではないため、スクリプトは終了します。
壊れたアプローチ#1を書いたユーザーに公平に言えば、私が最初にこれを試したとき、私は自分と同じようなことをしました。
if otherpids="$(pgrep -f "$0" | grep -vFx "$$")" ; then
echo >&2 "There are other copies of the script running; exiting."
ps >&2 -fq "${otherpids//$'\n'/ }" # -q takes about a tenth the time as -p
exit 1
fi
これはほぼで機能します。しかし、パイプを実行するためにフォークするという事実は、これを捨てます。したがって、これも常に終了します。
pids_this_script="$(pgrep -f "$0")"
if not_this_process="$(echo "$pids_this_script" | grep -vFx "$$")"; then
echo >&2 "There are other copies of this script running; exiting."
ps -fq "${not_this_process//$'\n'/ }"
exit 1
fi
このバージョンでは、最初にコマンドライン引数に現在のスクリプトが含まれるすべてのPIDを取得し、then個別にそのpidlistをフィルタリングして現在のスクリプトのPIDを省略することで、アプローチ#2のパイプラインフォークの問題を回避します。 。
これはうまくいくかもしれません...他のプロセスに$0
に一致するコマンドラインがない場合、スクリプトは常に同じ方法で呼び出されます(たとえば、相対パスと絶対パスで呼び出された場合、後者のインスタンス前者には気づかないでしょう)。
では、スクリプトが実際に実行されていることを示していない可能性があるため、コマンドライン全体のチェックをスキップし、代わりにlsof
をチェックして、このスクリプトが開いているすべてのプロセスを見つけるとどうなるでしょうか。
ええ、はい、このアプローチは実際にはそれほど悪くありません:
if otherpids="$(lsof -t "$0" | grep -vFx "$$")"; then
echo >&2 "Error: There are other processes that have this script open - most likely other copies of the script running. Exiting to avoid conflicts."
ps >&2 -fq "${otherpids//$'\n'/ }"
exit 1
fi
もちろん、スクリプトのcopyが実行されている場合、新しいインスタンスは正常に起動し、2つのcopiesが実行されます。
または、実行中のスクリプトが変更された場合(例:Vimまたはgit checkout
)、Vimとgit checkout
の両方が新しいものになるため、スクリプトの「新しい」バージョンは問題なく起動します古いものの代わりにファイル(新しいiノード)。
ただし、スクリプトを変更したりコピーしたりしない場合、このバージョンは非常に優れています。チェックに達する前にスクリプトファイルを開いておく必要があるため、競合状態はありません。
別のプロセスがスクリプトファイルを開いている場合でも誤検知が発生する可能性がありますが、Vimで編集のために開いている場合でも、vimはスクリプトファイルを開いたままにしないため、誤検知は発生しません。
ただし、スクリプトが編集またはコピーされる可能性がある場合は、このアプローチを使用しないでください。falseになるためですnegativesつまり、一度に複数のインスタンスが実行されるため、Vimで編集してもfalseにならないという事実ポジティブなことはあなたにとって重要ではありません。ただし、Vimでスクリプトを開いている場合、アプローチ#3 doesは誤検知(つまり、開始を拒否)するため、これについて触れます。
この質問に対するトップ投票の回答 は、しっかりとしたアプローチを提供します。
たぶん、あなたはより良いものを書くことができます...しかし、あなたがすべての問題と上記のアプローチすべての警告を理解していなければ、それらすべてを回避するロックメソッドを書くことはおそらくありません。
FLOM(Free LOck Manager) ツールを使用すると、コマンドのシリアル化が実行するのと同じくらい簡単になります
flom -- command_to_serialize
FLOMでは、ここで説明するように、より洗練されたユースケース(分散ロック、リーダー/ライター、数値リソースなど)を実装できます。 http://sourceforge.net/p/flom/wiki/FLOM%20by %20examples /
これは、マシン上の任意のジョブの競合状態を簡単に処理するために、サーバーに追加することがあります。これはTim Kennedyの投稿に似ていますが、この方法では、必要な各bashスクリプトに1行追加するだけでレースを処理できます。
以下の内容を/ opt/racechecker/racecheckerに入れます:
ZPROGRAMNAME=$(readlink -f $0)
EZPROGRAMNAME=`echo $ZPROGRAMNAME | sed 's/\//_/g'`
EZMAIL="/usr/bin/mail"
EZCAT="/bin/cat"
if [ -n "$EZPROGRAMNAME" ] ;then
EZPIDFILE=/tmp/$EZPROGRAMNAME.pid
if [ -e "$EZPIDFILE" ] ;then
EZPID=$($EZCAT $EZPIDFILE)
echo "" | $EZMAIL -s "$ZPROGRAMNAME already running with pid $EZPID" [email protected] >>/dev/null
exit -1
fi
echo $$ >> $EZPIDFILE
function finish {
rm $EZPIDFILE
}
trap finish EXIT
fi
使い方は次のとおりです。シバンの後の行に注意してください:
#/bin/bash
. /opt/racechecker/racechecker
echo "script are running"
sleep 120
それが動作する方法は、メインのbashscriptファイル名を把握し、 "/ tmp"の下にpidfileを作成することです。また、リスナーを終了信号に追加します。メインスクリプトが適切に終了すると、リスナーはpidfileを削除します。
代わりに、インスタンスの起動時にpidfileが存在する場合、2番目のifステートメント内のコードを含むifステートメントが実行されます。この場合、私はこれが起こったときに警報メールを出すことにしました。
スクリプトがクラッシュした場合
さらなる演習は、クラッシュを処理することです。理想的には、何らかの理由でメインスクリプトがクラッシュした場合でも、pidfileを削除する必要があります。これは、上記の私のバージョンでは行われません。つまり、スクリプトがクラッシュした場合、pidfileを手動で削除して機能を復元する必要があります。
システムクラッシュの場合
たとえば/ tmpの下にpidfile/lockfileを保存することをお勧めします。この方法では、pidfileは常に起動時に削除されるため、システムクラッシュ後もスクリプトは確実に実行を継続します。