web-dev-qa-db-ja.com

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

場合によっては、シェルスクリプトの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 に含めることは良い考えでしょうか?

私の意見では、それは timeoutNiceおよび友人と非常によく似たテーマを実装しています。

67
maxschlepzig

上記の競合状態を防ぐことができるシェルスクリプトでロックを行う別の方法を次に示します。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%を解決できます。

ロックファイルの競合状態が頻繁に発生する場合は、ジョブのタイミングが適切でないなど、より大きな問題のインジケータである可能性があります。あるいは、間隔がジョブの完了ほど重要でない場合は、ジョブがデーモン化されるのに適している可能性があります。


以下を編集-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メソッドが適しています。

45
Tim Kennedy

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

_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の最新リリースでは、どちらを使用するかを心配する必要はありません。

14
Arcege

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
12
glenn jackman

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

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

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

5
Partly Cloudy

私は 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
2
AndresVia

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

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

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

1
dru8274

ファイルを使用しないでください。

スクリプトが次のように実行された場合:

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
1
frogstarr78

実際に使用する場合は、 トップ投票の回答 を使用してください。

ただし、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

このスクリプトは、実行方法に関係なく、毎回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ではないため、スクリプトは終了します。

壊れたアプローチ#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を取得し、then個別にその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

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

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

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

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

ただし、スクリプトが編集またはコピーされる可能性がある場合は、このアプローチを使用しないでください。falseになるためですnegativesつまり、一度に複数のインスタンスが実行されるため、Vimで編集してもfalseにならないという事実ポジティブなことはあなたにとって重要ではありません。ただし、Vimでスクリプトを開いている場合、アプローチ#3 doesは誤検知(つまり、開始を拒否)するため、これについて触れます。

それでは、どうしたらいいですか?

この質問に対するトップ投票の回答 は、しっかりとしたアプローチを提供します。

たぶん、あなたはより良いものを書くことができます...しかし、あなたがすべての問題と上記のアプローチすべての警告を理解していなければ、それらすべてを回避するロックメソッドを書くことはおそらくありません。

1
Wildcard

FLOM(Free LOck Manager) ツールを使用すると、コマンドのシリアル化が実行するのと同じくらい簡単になります

flom -- command_to_serialize

FLOMでは、ここで説明するように、より洗練されたユースケース(分散ロック、リーダー/ライター、数値リソースなど)を実装できます。 http://sourceforge.net/p/flom/wiki/FLOM%20by %20examples /

0
tiian

これは、マシン上の任意のジョブの競合状態を簡単に処理するために、サーバーに追加することがあります。これは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は常に起動時に削除されるため、システムクラッシュ後もスクリプトは確実に実行を継続します。

0
ziggestardust