シェルスクリプトの正しいロック?


66

シェルスクリプトの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サーバーが再びアクティブになった場合、並列実行ジョブの雷の群れがあります。

Webで検索すると、この問題の良い解決策のように思えるツールロックランが見つかりました。これを使用して、次のようなロックが必要なスクリプトを実行します。

$ lockrun --lockfile=/var/tmp/mylock myscript.sh

これをラッパーに入れるか、crontabから使用できます。

これは、使用lockf()可能な場合(POSIX)をバックにフォールflock()(BSD)。またlockf()、NFSを介したサポートは比較的広く普及しているはずです。

に代わるものはありlockrunますか?

他のcronデーモンはどうですか?適切な方法でロックをサポートする一般的なcrondはありますか?Vixie Crond(Debian / Ubuntuシステムのデフォルト)のマニュアルページをざっと見ても、ロックについては何も表示されません。

のようなツール含めることが良いでしょうlockruncoreutilsのを

私の意見では、それは非常に類似したテーマを実装timeoutniceおよび友人。


4
接線方向で、そしてあなたの初期パターンGood Enough(tm)を考慮するかもしれない他の人のために、シェルコードはkilledのときにロックファイルを削除するためにTERMをトラップする可能性があります。また、ロックファイルに触れるだけでなく、自分のPIDをロックファイルに保存することをお勧めします。
ウルリッヒ・シュワルツ


@Shawnは、実際にはcrondとNFSについて言及していません。
maxschlepzig


1
@Ulrichは遅かれ早かれ、NFSロックファイルにPIDを保存してもほとんど価値がありません。ホスト名を追加しても、実際のプロセスの確認にはまだ役立ちません
-roaima

回答:


45

上記の競合状態を防ぐことができるシェルスクリプトでロックを行う別の方法を次に示します。2つのジョブが両方とも行3を渡すnoclobber場合があります。このオプションはkshとbashで機能します。set noclobbercsh / tcshでスクリプトを作成するべきではないため、使用しないでください。;)

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つだけの場合、このようなことがうまくいく可能性があります。

lockrunの経験はありませんが、スクリプトを実際に実行する前に事前設定されたロック環境を用意しておくと役立つ場合があります。またはそうではないかもしれません。ラッパーでスクリプトの外部のロックファイルのテストを設定しているだけで、理論的には、2つのジョブがlockrunによってまったく同時に呼び出された場合、「inside-スクリプトのソリューション?

ファイルのロックはとにかくシステムの振る舞いを大いに尊重し、実行前にロックファイルの存在をチェックしないスクリプトは、何をしようと何でもします。ロックファイルのテストと適切な動作を行うだけで、潜在的な問題の100%ではなくても99%を解決できます。

ロックファイルの競合状態が頻繁に発生する場合は、ジョブのタイミングが合っていないなど、より大きな問題の指標である可能性があります。 。


以下で編集-2016-05-06(KSH88を使用している場合)


以下の@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メソッドが適しているはずです。


1
いいえ、lockrunを使用しても問題はありません。NFSサーバーがハングすると、lockf()システムコールですべてのlockrun呼び出しが(少なくとも)ハングします。競合状態はありません。私はcronジョブでこのような問題に頻繁に遭遇することはありません-反対は事実です-しかし、これはあなたにぶつかったときに大きな痛みを引き起こす可能性がある問題です。
maxschlepzig

1
この方法は安全で、これまでで最もエレガントな方法であるため、私はこの答えを受け入れました。小さな変形をお勧めしset -o noclobber && echo "$$" > "$lockfile"ます。シェルがnoclobberオプションをサポートしていない場合に安全なフォールバックを取得します。
maxschlepzig

3
良い答えですが、ロックを作成したプロセスがまだ存在していることを確認するには、lockfileの値を「kill -0」する必要もあります。
ナイジェルホーン14

1
noclobberオプションが競合状態に陥りやすいかもしれません。考えられる食べ物については、mywiki.wooledge.org / BashFAQ / 045を参照してください。
クリントPachl

2
注意:使用してnoclobber(または-Cksh88では、使用しないので、動作しませんksh88ではで)O_EXCLのためにnoclobber。新しいシェルで実行している場合は、大丈夫かもしれません
...-jrw32982

14

ハードリンクを使用することを好みます。

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の実装の多くはatomicよりもアトミックハードリンクを許可しているため、ハードリンクを使用することを好みますmkdir。NFSの最新リリースでは、どちらを使用しても心配する必要はありません。


12

私はそれ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

わかりましたがmkdir()、NFS(> = 3)がアトミックに標準化されているかどうかについての情報は見つかりませんでした。
maxschlepzig

2
@maxschlepzig RFC 1813mkdir、アトミックであることを明示的に呼び出していません(を呼び出していますrename)。実際には、一部の実装はそうではないことが知られています。関連:GNU archの作者による貢献を含む興味深いスレッド
ジル「悪さSO-ストップ」

8

簡単な方法は、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"

5

semparallelあなたが探しているのはGNU ツールの一部として来るものかもしれません:

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

順序は保証されないことに注意してください。また、出力は完了するまで表示されません(いらいらします!)。それでも、ロックファイルや再試行、クリーンアップを心配することなく、同時実行を防止するための最も簡潔な方法です。


semハンドルによって提供されるロックは、実行中に撃downされますか?
ロアイマ

2

を使用します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

1

ここここで説明するように、コマンドラインツール「flock」を使用して、bashスクリプトのロックを管理します。私はflockマンページからこの簡単な方法を使用して、サブシェルでいくつかのコマンドを実行しました...

   (
     flock -n 9
     # ... commands executed under lock ...
   ) 9>/var/lock/mylockfile

その例では、ロックファイルを取得できない場合、終了コード1で失敗します。ただし、flockは、コマンドをサブシェルで実行する必要のない方法でも使用できます:-)


3
flock()システムコールは、NFS上では動作しません
maxschlepzig

1
BSDには同様のツール「lockf」があります。
-dubiousjim

2
@ dubiousjim、BSD lockfも呼び出すflock()ため、NFS経由で問題が発生します。一方、Linuxのflock()fcntl()は、ファイルがNFSマウントにあるときにフォールバックするようになりました。したがって、LinuxのみのNFS環境でflock()は、NFSで動作するようになりました。
maxschlepzig

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チェックコードは内部から実行されますmy_scriptか?別のインスタンスが実行されている場合running_proc-2つの一致する行が含まれていませんか?私はアイデアが好き、もちろん-別のユーザーが同じ名前のスクリプトを実行しているときに、誤った結果が得られます...
maxschlepzig

3
また、競合状態も含まれます。2つのインスタンスが最初の行を並行して実行すると、「ロック」を取得せず、両方ともステータス6で終了します。これは、1ラウンドの相互飢starのようなものです。ところで、なぜあなたがあなたの例の$!代わりに使うのか分かりません$$
maxschlepzig

@maxschlepzigは確かに間違った$でごめんなさい!vs. $$
frogstarr78

@maxschlepzigでスクリプトを実行する複数のユーザーを処理するには、euser =を-o引数に追加します。
frogstarr78

@maxschlepzigを使用すると、複数行を防ぐことができます。引数をgrepに変更したり、「フィルター」を追加したりすることもできます(例:)grep -v $$。基本的に、私は問題に対して異なるアプローチを提供しようとしていました。
frogstarr78

1

実際に使用する場合は、上位の回答を使用する必要があります

しかし、私はps人々がそれらを使用しているのを見続けているので、いくつかのさまざまな壊れた半実行可能なアプローチとそれらが持つ多くの警告について説明したいと思います。

この答えは、シェルでロックを使用て処理しない理由」に対する答えです。psgrep

壊れたアプローチ#1

まず、別の回答で示されているアプローチで、機能しない(および機能しない可能性があり、明らかにテストされていないという事実にもかかわらず、いくつかの賛成票がある):

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

このスクリプトは、実行方法に関係なく、常に 6で終了します。

で実行すると./myscriptps出力はになりますが12345 -bash、これは必要な文字列12345 bash ./myscriptと一致しないため、失敗します。

で実行するとbash myscript、さらに面白くなります。bashプロセスはパイプラインを実行するために分岐し、シェルはpsandを実行しgrepます。元のシェルと子シェルの両方がps出力に表示されます。次のようなものです。

25793 bash myscript
25795 bash myscript

これは期待される出力$$ bash $0ではないため、スクリプトは終了します。

壊れたアプローチ#2

壊れたアプローチ#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

これはほとんど機能します。しかし、パイプを実行するために分岐するという事実はこれをスローします。したがって、これも常に終了します。

信頼性の低いアプローチ#3

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を取得し、次にそのpidlistを個別にフィルタリングして現在のスクリプトのPIDを省略することにより、アプローチ#2のパイプラインフォークの問題を回避します。

これは機能する可能性があります。他のプロセスにに一致するコマンドラインがない場合$0、スクリプトは常に同じ方法で呼び出されます(たとえば、相対パスと絶対パスで呼び出された場合、後者のインスタンスは前者に気付かないでしょう) )。

信頼性の低いアプローチ#4

それでは、実際に実行されているスクリプトを示していない可能性があるため、コマンドライン全体のチェックをスキップし、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

もちろん、スクリプトのコピーが実行されている場合、新しいインスタンスは正常に起動し、2つのコピーが実行されます。

または、実行中のスクリプトが変更された場合(Vimまたはaなどgit checkout)、Vimとgit checkout結果の両方が新しいファイル(新しいiノード)になるので、スクリプトの「新しい」バージョンは問題なく起動します。古いもの。

ただし、スクリプトが変更されたりコピーされたりしないのあれば、このバージョンはかなり良いです。チェックに到達する前にスクリプトファイルを既に開いている必要があるため、競合状態はありません。

別のプロセスがスクリプトファイルを開いている場合、依然として誤検出が発生する可能性がありますが、Vimで編集のために開いていても、vimは実際にスクリプトファイルを開いたままにしないため、誤検出は発生しません。

しかし、覚えて、スクリプトを編集またはコピーされる可能性があります場合はfalseを取得しますので、このアプローチを使用していないネガは一度に複数のインスタンスを実行-すなわちこれはVimで編集する種類の偽陽性を与えていないという事実は問題ないはずあなたへ。ただし、Vimでスクリプトを開いている場合、アプローチ3 誤検知(つまり、起動を拒否)を行うためです。

それでは、何をすべきでしょうか?

この質問へのトップ投票答えが良いソリッドなアプローチを提供します。

おそらくもっと良いものを書くことができます...しかし、上記のすべてのアプローチですべての問題と警告を理解していない場合、それらをすべて回避するロック方法を書くことはできません。



0

マシン上のジョブの競合状態を簡単に処理するためにサーバーに追加することがあります。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"  alarms@someemail.com >>/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を手動で削除する必要があります。

システムクラッシュの場合

pidfile / lockfileを/ tmpなどに保存することをお勧めします。これにより、システムのクラッシュ後もスクリプトは定義どおりに実行を継続します。これは、起動時に常にpidfileが削除されるためです。


ティムケネディの仮説とは異なり、スクリプトには競合状態が含まれています。これは、PIDFILEの存在とその条件付き作成のチェックがアトミック操作で行われないためです。
maxschlepzig

その上で+1!これを考慮して、スクリプトを変更します。
ziggestardust

-2

スクリプトを確認してください...

あなたは可能LOVEそれを....

[rambabu@Server01 ~]$ sh Prevent_cron-OR-Script_against_parallel_run.sh
Parallel RUN Enabled
Now running
Task completed in Parallel RUN...
[rambabu@Server01 ~]$ cat Prevent_cron-OR-Script_against_parallel_run.sh
#!/bin/bash
#Created by RambabuKella
#Date : 12-12-2013

#LOCK file name
Parallel_RUN="yes"
#Parallel_RUN="no"
PS_GREP=0
LOCK=/var/tmp/mylock_`whoami`_"$0"
#Checking for the process
PS_GREP=`ps -ef |grep "sh $0" |grep -v grep|wc -l`
if [ "$Parallel_RUN" == "no" ] ;then
echo "Parallel RUN Disabled"

 if [ -f $LOCK ] || [ $PS_GREP -gt 2   ] ;then
        echo -e "\nJob is already running OR LOCK file exists. "
        echo -e "\nDetail are : "
        ps -ef |grep  "$0" |grep -v grep
        cat "$LOCK"
  exit 6
 fi
echo -e "LOCK file \" $LOCK \" created on : `date +%F-%H-%M` ." &> $LOCK
# do some work
echo "Now running"
echo "Task completed on with single RUN ..."
#done

rm -v $LOCK 2>/dev/null
exit 0
else

echo "Parallel RUN Enabled"

# do some work
echo "Now running"
echo "Task completed in Parallel RUN..."
#done

exit 0
fi
echo "some thing wrong"
exit 2
[rambabu@Server01 ~]$

-3

「flocktest」という名前のスクリプトで、次のソリューションを提供します

#!/bin/bash
export LOGFILE=`basename $0`.logfile
logit () {
echo "$1" >>$LOGFILE
}
PROGPATH=$0
(
flock -x -n 257
(($?)) && logit "'$PROGPATH' is already running!" && exit 0
logit "'$PROGPATH', proc($$): sleeping 30 seconds"
sleep 30
)257<$PROGPATH
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.