シェルスクリプトのインスタンスが一度に1つだけ実行されるようにする簡単な方法


回答:


109

以下は、ロックファイルを使用してPIDをエコーする実装です。これは、pidfileを削除する前にプロセスが強制終了された場合の保護として機能します。

LOCKFILE=/tmp/lock.txt
if [ -e ${LOCKFILE} ] && kill -0 `cat ${LOCKFILE}`; then
    echo "already running"
    exit
fi

# make sure the lockfile is removed when we exit and then claim it
trap "rm -f ${LOCKFILE}; exit" INT TERM EXIT
echo $$ > ${LOCKFILE}

# do stuff
sleep 1000

rm -f ${LOCKFILE}

ここでのトリックは、kill -0シグナルを配信せず、指定されたPIDのプロセスが存在するかどうかを確認するだけです。また、を呼び出すと、プロセスが強制終了された場合でもロックファイルtrap確実に削除されます(を除く)。kill -9


73
もう1つの回答に関するコメントですでに述べたように、これには致命的な欠陥があります。チェックとエコーの間に他のスクリプトが起動すると、トーストします。
ポールトンブリン

1
シンボリックリンクのトリックはすばらしいですが、ロックファイルの所有者が-9でkillされた場合、またはシステムがクラッシュした場合でも、シンボリックリンクを読み取るための競合状態があり、所有者がいなくなったことを確認してから削除します。私は自分の解決策に固執しています。
bmdhacks 2008年

9
アトミックチェックと作成は、flock(1)またはlockfile(1)を使用してシェルで使用できます。他の回答を参照してください。
dmckee ---元モデレーターの子猫

3
アトミックチェックを実行し、flockやlockfileなどのユーティリティに依存することなく作成するための移植可能な方法については、私の返信を参照してください。
lhunath 2009

2
これはアトミックではないため、役に立ちません。テストと設定にはアトミックなメカニズムが必要です。
K Richard Pixley 2017年

214

flock(1)排他的スコープ付きロックをオンファイル記述子にするために使用します。このようにして、スクリプトのさまざまな部分を同期することもできます。

#!/bin/bash

(
  # Wait for lock on /var/lock/.myscript.exclusivelock (fd 200) for 10 seconds
  flock -x -w 10 200 || exit 1

  # Do stuff

) 200>/var/lock/.myscript.exclusivelock

コードの間で、この性を保証(しては、)一度に1つのプロセスによってのみ実行され、プロセスが長すぎるロックのために待機していないこと。

警告:この特定のコマンドはの一部ですutil-linux。Linux以外のオペレーティングシステムを実行している場合、利用できない場合があります。


11
200は何ですか?マヌルネコでは「fd」と書いてありますが、どういう意味かわかりません。
揺れ動く

4
@chovy「ファイル記述子」、開いているファイルを指定する整数ハンドル。
Alex B

6
他の誰かが疑問に思っている場合:構文( command A ) command Bはのサブシェルを呼び出しますcommand Atldp.org/LDP/abs/html/subshel​​ls.htmlに文書化されています。私はサブシェルとコマンドBの呼び出しのタイミングではないことを確認、まだ午前
博士ジャン-フィリップ・Gehrcke

1
サブシェル内のコードはもっと似ているべきだと思います:if flock -x -w 10 200; then ...Do stuff...; else echo "Failed to lock file" 1>&2; fiタイムアウトが発生した場合(他のプロセスがファイルをロックしている場合)、このスクリプトは先に進んでファイルを変更しません。おそらく...反論は「10秒かかってもロックがまだ利用できない場合、決して利用可能になることはありません」、おそらくロックを保持しているプロセスが終了していないためです(おそらく実行されています)デバッガーの下で?)
ジョナサンレフラー2013

1
リダイレクト先のファイルは、ロックが機能するための単なるプレースフォルダーであり、そこに意味のあるデータはありません。exit内側の部分からです( )。サブプロセスが終了すると、ロックを保持しているプロセスがないため、ロックは自動的に解放されます。
2015年

158

「ロックファイル」の存在をテストするすべてのアプローチには欠陥があります。

どうして?ファイルが存在するかどうかを確認し、1つのアトミックアクションで作成する方法がないためです。このため; 競合状態が存在しWILL相互排他ブレークであなたの試みを作るには。

代わりに、を使用する必要がありますmkdirmkdirディレクトリがまだ存在しない場合は作成し、存在する場合は終了コードを設定します。さらに重要なことは、このすべてを1つのアトミックアクションで実行し、このシナリオに最適です。

if ! mkdir /tmp/myscript.lock 2>/dev/null; then
    echo "Myscript is already running." >&2
    exit 1
fi

詳細については、優れたBashFAQを参照してください。 http://mywiki.wooledge.org/BashFAQ/045

古いロックを処理したい場合は、fuser(1)が便利です。ここでの唯一の欠点は、操作に約1秒かかるため、瞬時ではないことです。

フューザーを使用して問題を解決するために私が一度書いた関数は次のとおりです。

#       mutex file
#
# Open a mutual exclusion lock on the file, unless another process already owns one.
#
# If the file is already locked by another process, the operation fails.
# This function defines a lock on a file as having a file descriptor open to the file.
# This function uses FD 9 to open a lock on the file.  To release the lock, close FD 9:
# exec 9>&-
#
mutex() {
    local file=$1 pid pids 

    exec 9>>"$file"
    { pids=$(fuser -f "$file"); } 2>&- 9>&- 
    for pid in $pids; do
        [[ $pid = $$ ]] && continue

        exec 9>&- 
        return 1 # Locked by a pid.
    done 
}

次のようなスクリプトで使用できます。

mutex /var/run/myscript.lock || { echo "Already running." >&2; exit 1; }

移植性を気にしない場合(これらのソリューションはほとんどすべてのUNIXボックスで動作するはずです)、Linuxのfuser(1)はいくつかの追加オプションを提供し、flock(1)もあります。


1
このif ! mkdir部分を組み合わせて、lockdir内に(正常な起動時に)保存されたPIDを持つプロセスが実際に実行されており、stalenes保護用のスクリプト同一であるかどうかを確認できます。これは、再起動後にPIDを再利用することからも保護しますfuser
Tobias Kienzler 2012

4
アトミック操作とmkdir定義されておらず、「副作用」がファイルシステムの実装の詳細であることは確かに当てはまります。NFSはそれをアトミックな方法で実装しないと彼が言ったなら、私は彼を完全に信じています。私はあなた/tmpがNFS共有であり、mkdirアトミックに実装するfsによって提供される可能性があるとは思いませんが。
lhunath 2012

5
ただし、通常のファイルの存在を確認し、存在しない場合はアトミックlnに作成する方法があります。別のファイルからハードリンクを作成するために使用します。それを保証しない奇妙なファイルシステムがある場合は、後で新しいファイルのiノードをチェックして、元のファイルと同じかどうかを確認できます。
フアンチェスペデス2014

4
そこ「ファイルが存在するかどうかをチェックし、単一のアトミックアクションでそれを作成するための方法は、」 -それはですopen(... O_CREAT|O_EXCL)lockfile-create(in lockfile-progs)やdotlockfile(in liblockfile-bin)などの適切なユーザープログラムが必要です。そして、適切にクリーンアップする(例trap EXIT:)か、古くなったロックをテストする(例:で)ことを確認してください--use-pid
Toby Speight 2016年

5
「「ロックファイル」の存在をテストするすべてのアプローチに欠陥があります。なぜですか?ファイルが存在するかどうかを確認して単一のアトミックアクションで作成する方法がないためです。」-アトミックにするためには、カーネルレベル-カーネルレベルで行われます。flock(1)linux.die.net/man/1/flockは、2006年以降、男性の著作権の日付から存在すると思われます。 1)個人的なことではなく、カーネル開発者によって提供されたカーネル実装ツールを使用することが正しいという強い確信があるだけです。
クレイグヒックス

42

想像を超えてflock(1)と呼ばれるflock(2)システムコールのラッパーがあります。これにより、クリーンアップなどを気にすることなく、確実に排他ロックを比較的簡単に取得できます。man ページに、シェルスクリプトでの使用方法に関する例があります。


3
flock()システムコールは、POSIXではなく、NFSマウント上のファイルでは動作しません。
maxschlepzig

17
私が使用するCronジョブからの実行では、flock -x -n %lock file% -c "%command%"1つのインスタンスのみが実行されていることを確認しています。
Ryall

ああ、想像を絶するflock(1)の代わりに、flock(U)のようなものを使うべきでした。..。それにある程度の親しみがあります。。。少し前に聞いたことがあるようです。
Kent Kruckeberg、2016

flock(2)のドキュメントではファイルのみの使用を指定していますが、flock(1)のドキュメントではファイルまたはディレクトリのいずれかでの使用を指定しています。flock(1)のドキュメントは、作成中の違いを示す方法については明確ではありませんが、最後の「/」を追加することによって行われると思います。とにかく、flock(1)がディレクトリを処理できるがflock(2)が処理できない場合、flock(1)はflock(2)にのみ実装されません。
クレイグヒックス

27

flockのようなアトミック操作が必要です。そうしないと、最終的に失敗します。

しかし、flockが利用できない場合の対処方法。mkdirがあります。これもアトミック操作です。1つのプロセスのみがmkdirを成功させ、他のすべてのプロセスは失敗します。

したがって、コードは次のとおりです。

if mkdir /var/lock/.myscript.exclusivelock
then
  # do stuff
  :
  rmdir /var/lock/.myscript.exclusivelock
fi

古くなったロックを処理する必要があります。そうしないと、クラッシュが発生してスクリプトが再び実行されることはありません。


1
これを数回同時に実行します( "./a.sh&./a.sh&./a.sh&./a.sh&./a.sh&./a.sh&./a.sh & ")とスクリプトは数回リークします。
Nippysaurus

7
@Nippysaurus:このロック方法はリークしません。あなたが目にしたのは、すべてのコピーが起動される前に終了する最初のスクリプトでした。そのため、別のスクリプトが(正しく)ロックを取得できました。この誤検知を回避するには、sleep 10before rmdirを追加して再度カスケードを試行します。「リーク」するものはありません。
アトス卿2013年

他のソースは、mkdirがNFSのようないくつかのファイルシステムではアトミックでないと主張しています。また、NFSで同時再帰的なmkdirを実行すると、jenkinsマトリックスジョブでエラーが発生する場合がありました。だから私はそれが事実だと確信しています。しかし、mkdirは、それほど要求されないユースケースIMOにはかなり適しています。
akostadinov 2014年

通常のファイルでBashのnoclobberオプションを使用できます。
Palec 2014年

26

ロックの信頼性を高めるには、アトミック操作が必要です。上記の提案の多くはアトミックではありません。提案されたlockfile(1)ユーティリティは、マンページで述べられているように、その「NFS耐性」があることを期待しています。OSがlockfile(1)をサポートしておらず、ソリューションがNFSで動作する必要がある場合、多くのオプションはありません...

NFSv2には2つのアトミック操作があります。

  • シンボリックリンク
  • 名前を変更

NFSv3では、作成呼び出しもアトミックです。

NFSv2とNFSv3では、ディレクトリ操作はアトミックではありません(Brent Callaghan著の「NFS Illustrated」の本、ISBN 0-201-32570-5を参照してください。Brentは、SunのNFSベテランです)。

これを知っていれば、ファイルとディレクトリのスピンロックを実装できます(PHPではなくシェルで)。

現在のディレクトリをロック:

while ! ln -s . lock; do :; done

ファイルをロックする:

while ! ln -s ${f} ${f}.lock; do :; done

現在のディレクトリのロックを解除します(仮定、実行中のプロセスが実際にロックを取得):

mv lock deleteme && rm deleteme

ファイルのロックを解除します(実行中のプロセスが実際にロックを取得したと想定)。

mv ${f}.lock ${f}.deleteme && rm ${f}.deleteme

削除もアトミックではないため、最初に名前変更(アトミック)してから削除します。

symlinkとrenameの呼び出しでは、両方のファイル名が同じファイルシステムに存在する必要があります。私の提案:単純なファイル名のみ(パスなし)を使用し、ファイルとロックを同じディレクトリに配置します。


NFS Illustratedのどのページが、mkdirがNFS上でアトミックではないというステートメントをサポートしていますか?
maxschlepzig

このテクニックに感謝します。シェルミューテックスの実装は、新しいシェルライブラリgithub.com/Offirmo/offirmo-shell-libで利用できます。「mutex」を参照してください。使用lockfile可能な場合は使用し、使用できsymlinkない場合はこのメソッドにフォールバックします。
Offirmo 2012

いいね。残念ながら、この方法は古いロックを自動的に削除する方法を提供していません。
Richard Hansen 2013年

2段階のロック解除(mvrm)では、2つのプロセスP1、P2が競合している場合rm -fではなく、使用する必要がありrmますか?たとえば、P1はでロック解除を開始しmv、次にP2がロックし、次にP2がロック解除(mvおよびの両方rm)し、最後にP1が試行rmして失敗します。
マットウォリス

1
@MattWallis最後の問題は$$${f}.deletemeファイル名に含めることで簡単に軽減できます。
Stefan Majewsky、2014

23

別のオプションはnoclobber、を実行してシェルのオプションを使用することset -Cです。その後>、ファイルがすでに存在する場合は失敗します。

簡単に言えば:

set -C
lockfile="/tmp/locktest.lock"
if echo "$$" > "$lockfile"; then
    echo "Successfully acquired lock"
    # do work
    rm "$lockfile"    # XXX or via trap - see below
else
    echo "Cannot acquire lock - already locked by $(cat "$lockfile")"
fi

これにより、シェルは以下を呼び出します。

open(pathname, O_CREAT|O_EXCL)

これは、ファイルをアトミックに作成するか、ファイルがすでに存在する場合は失敗します。


BashFAQ 045のコメントによると、これは失敗する可能性ksh88がありますが、私のすべてのシェルで機能します:

$ strace -e trace=creat,open -f /bin/bash /home/mikel/bin/testopen 2>&1 | grep -F testopen.lock
open("/tmp/testopen.lock", O_WRONLY|O_CREAT|O_EXCL|O_LARGEFILE, 0666) = 3

$ strace -e trace=creat,open -f /bin/zsh /home/mikel/bin/testopen 2>&1 | grep -F testopen.lock
open("/tmp/testopen.lock", O_WRONLY|O_CREAT|O_EXCL|O_NOCTTY|O_LARGEFILE, 0666) = 3

$ strace -e trace=creat,open -f /bin/pdksh /home/mikel/bin/testopen 2>&1 | grep -F testopen.lock
open("/tmp/testopen.lock", O_WRONLY|O_CREAT|O_EXCL|O_TRUNC|O_LARGEFILE, 0666) = 3

$ strace -e trace=creat,open -f /bin/dash /home/mikel/bin/testopen 2>&1 | grep -F testopen.lock
open("/tmp/testopen.lock", O_WRONLY|O_CREAT|O_EXCL|O_LARGEFILE, 0666) = 3

興味深いことにフラグがpdksh追加されますO_TRUNCが、明らかに冗長
です。空のファイルを作成するか、何もしていません。


どのように行うかは、rm不潔な出口をどのように処理するかによって異なります。

クリーン終了時に削除

クリーンでない出口の原因となった問題が解決され、ロックファイルが手動で削除されるまで、新しい実行は失敗します。

# acquire lock
# do work (code here may call exit, etc.)
rm "$lockfile"

出口で削除

スクリプトがまだ実行されていなければ、新しい実行は成功します。

trap 'rm "$lockfile"' EXIT

非常に新しいアプローチ...これは、ロックディレクトリではなくロックファイルを使用して原子性を実現する1つの方法のようです。
Matt Caldwell、

素敵なアプローチ。:-) EXITトラップでは、ロックファイルをクリーンアップできるプロセスを制限する必要があります。例:trap 'if [[$(cat "$ lockfile")== "$$"]]; 次にrm "$ lockfile"; fi 'EXIT
Kevin Seifert

1
ロックファイルは、NFSではアトミックではありません。それが人々がロックディレクトリを使うようになった理由です。
K Richard Pixley 2017年

20

GNU Parallelとして呼び出されsemたときにミューテックスとして機能するため、これに使用できます。したがって、具体的には、次のように使用できます。

sem --id SCRIPTSINGLETON yourScript

タイムアウトも必要な場合は、次を使用します。

sem --id SCRIPTSINGLETON --semaphoretimeout -10 yourScript

<0のタイムアウトは、セマフォがタイムアウト内に解放されない場合にスクリプトを実行せずに終了することを意味し、> 0のタイムアウトはとにかくスクリプトを実行することを意味します。

名前を(で--id)付ける必要があることに注意してください。そうしないと、デフォルトで制御端末に設定されます。

GNU Parallel ほとんどのLinux / OSX / Unixプラットフォームでの非常にシンプルなインストールです。これは単なるPerlスクリプトです。


あまりにも悪い人々は、役に立たない答えに反対票を投じることに消極的です:これは、ジャンクの山に埋もれている新しい関連する答えにつながります。
Dmitry Grigoryev

4
多くの賛成票が必要です。これは、このような整然としたほとんど知られていない答えです。(しかし、これは手っ取り早いOPでしたが、これは手っ取り早く簡単です!)詳細はsem、関連する質問unix.stackexchange.com/a/322200/199525を参照してください
曇りのち

16

シェルスクリプトの場合、ロックをより移植しやすくするため、mkdirover flockを使用する傾向があります。

どちらにしても、使用set -eするだけでは不十分です。コマンドが失敗した場合にのみスクリプトを終了します。ロックはまだ残ります。

適切なロッククリーンアップを行うには、トラップを次の擬似コードのようなものに設定する必要があります(リフトされ、単純化され、テストされていませんが、アクティブに使用されているスクリプトから)。

#=======================================================================
# Predefined Global Variables
#=======================================================================

TMPDIR=/tmp/myapp
[[ ! -d $TMP_DIR ]] \
    && mkdir -p $TMP_DIR \
    && chmod 700 $TMPDIR

LOCK_DIR=$TMP_DIR/lock

#=======================================================================
# Functions
#=======================================================================

function mklock {
    __lockdir="$LOCK_DIR/$(date +%s.%N).$$" # Private Global. Use Epoch.Nano.PID

    # If it can create $LOCK_DIR then no other instance is running
    if $(mkdir $LOCK_DIR)
    then
        mkdir $__lockdir  # create this instance's specific lock in queue
        LOCK_EXISTS=true  # Global
    else
        echo "FATAL: Lock already exists. Another copy is running or manually lock clean up required."
        exit 1001  # Or work out some sleep_while_execution_lock elsewhere
    fi
}

function rmlock {
    [[ ! -d $__lockdir ]] \
        && echo "WARNING: Lock is missing. $__lockdir does not exist" \
        || rmdir $__lockdir
}

#-----------------------------------------------------------------------
# Private Signal Traps Functions {{{2
#
# DANGER: SIGKILL cannot be trapped. So, try not to `kill -9 PID` or 
#         there will be *NO CLEAN UP*. You'll have to manually remove 
#         any locks in place.
#-----------------------------------------------------------------------
function __sig_exit {

    # Place your clean up logic here 

    # Remove the LOCK
    [[ -n $LOCK_EXISTS ]] && rmlock
}

function __sig_int {
    echo "WARNING: SIGINT caught"    
    exit 1002
}

function __sig_quit {
    echo "SIGQUIT caught"
    exit 1003
}

function __sig_term {
    echo "WARNING: SIGTERM caught"    
    exit 1015
}

#=======================================================================
# Main
#=======================================================================

# Set TRAPs
trap __sig_exit EXIT    # SIGEXIT
trap __sig_int INT      # SIGINT
trap __sig_quit QUIT    # SIGQUIT
trap __sig_term TERM    # SIGTERM

mklock

# CODE

exit # No need for cleanup code here being in the __sig_exit trap function

これがどうなるかです。すべてのトラップは終了を生成するため、__sig_exitロックがクリーンアップされる関数が常に実行されます(SIGKILLを除く)。

注:私の終了値は低い値ではありません。どうして?さまざまなバッチ処理システムは、0から31までの数値を想定または想定しています。それらを別の値に設定すると、スクリプトとバッチストリームを前のバッチジョブまたはスクリプトに応じて反応させることができます。


2
あなたのスクリプトは非常に冗長すぎて、私は思うにずっと短くなる可能性がありますが、全体として、そうです、これを正しく行うにはトラップを設定する必要があります。また、SIGHUPを追加します。
mojuba

これは$ LOCK_DIRをチェックするように見えるが$ __ lockdirを削除することを除いて、うまく機能します。たぶん、ロックを解除するときにrm -r $ LOCK_DIRを実行することをお勧めしますか?
bevada 2015

提案ありがとうございます。上記はコードを持ち上げて疑似コードの形式で配置したため、ユーザーの使用状況に基づいて調整する必要があります。RMDIRが安全にディレクトリを削除しかし、私は故意に私の場合は、rmdirと一緒に行ったonly_if彼らは空です。PIDファイルなどのリソースを配置している場合は、ロックのクリーンアップをより積極的に変更するrm -r $LOCK_DIRか、必要に応じて強制的に実行する必要があります(相対スクラッチファイルを保持するなどの特殊なケースでも同様です)。乾杯。
Mark Stinson、2015

テストしましたexit 1002か?
Gilles Quenot

13

本当に迅速かつ本当に汚いですか?スクリプトの上部にあるこのワンライナーは動作します:

[[ $(pgrep -c "`basename \"$0\"`") -gt 1 ]] && exit

もちろん、スクリプト名が一意であることを確認してください。:)


これをシミュレーションしてテストするにはどうすればよいですか?スクリプトを1行で2回開始して、すでに実行されている場合に警告を表示する方法はありますか?
rubo77 2016

2
これはまったく機能しません!なぜチェックするの-gt 2ですか?grepは、psの結果で常にそれを見つけるとは限りません!
rubo77 2016

pgrepPOSIXにはありません。これを移植可能に動作させたい場合は、POSIX psとその出力を処理する必要があります。
Palec 2017

OSX -cが存在しない場合は、を使用する必要があります| wc -l。数の比較について:-gt 1最初のインスタンスがそれ自体を見ているのでチェックされます。
ベンジャミンピーター

6

これは、アトミックディレクトリロックとPIDによる古いロックのチェックを組み合わせ、古い場合は再起動するアプローチです。また、これはいかなるバシズムにも依存しません。

#!/bin/dash

SCRIPTNAME=$(basename $0)
LOCKDIR="/var/lock/${SCRIPTNAME}"
PIDFILE="${LOCKDIR}/pid"

if ! mkdir $LOCKDIR 2>/dev/null
then
    # lock failed, but check for stale one by checking if the PID is really existing
    PID=$(cat $PIDFILE)
    if ! kill -0 $PID 2>/dev/null
    then
       echo "Removing stale lock of nonexistent PID ${PID}" >&2
       rm -rf $LOCKDIR
       echo "Restarting myself (${SCRIPTNAME})" >&2
       exec "$0" "$@"
    fi
    echo "$SCRIPTNAME is already running, bailing out" >&2
    exit 1
else
    # lock successfully acquired, save PID
    echo $$ > $PIDFILE
fi

trap "rm -rf ${LOCKDIR}" QUIT INT TERM EXIT


echo hello

sleep 30s

echo bye

5

既知の場所にロックファイルを作成し、スクリプトの開始時に存在を確認しますか?ファイルにPIDを入れると、スクリプトの実行を妨げている誤ったインスタンスを誰かが追跡しようとした場合に役立つことがあります。


5

この例はman flockで説明されていますが、バグと終了コードを管理する必要があるため、いくつかの改善が必要です。

   #!/bin/bash
   #set -e this is useful only for very stupid scripts because script fails when anything command exits with status more than 0 !! without possibility for capture exit codes. not all commands exits >0 are failed.

( #start subprocess
  # Wait for lock on /var/lock/.myscript.exclusivelock (fd 200) for 10 seconds
  flock -x -w 10 200
  if [ "$?" != "0" ]; then echo Cannot lock!; exit 1; fi
  echo $$>>/var/lock/.myscript.exclusivelock #for backward lockdir compatibility, notice this command is executed AFTER command bottom  ) 200>/var/lock/.myscript.exclusivelock.
  # Do stuff
  # you can properly manage exit codes with multiple command and process algorithm.
  # I suggest throw this all to external procedure than can properly handle exit X commands

) 200>/var/lock/.myscript.exclusivelock   #exit subprocess

FLOCKEXIT=$?  #save exitcode status
    #do some finish commands

exit $FLOCKEXIT   #return properly exitcode, may be usefull inside external scripts

別の方法を使用して、過去に使用したプロセスをリストすることもできます。しかし、これは上記の方法よりも複雑です。プロセスをpsでリストし、その名前でフィルタリングし、追加のフィルターgrep -v grepで寄生虫を削除し、最終的にgrep -cでカウントします。と比較します。その複雑で不確実な


1
ln -sを使用できます。これは、mkdirと同じように、ファイルまたはシンボリックリンクが存在しない場合にのみシンボリックリンクを作成できるためです。多くのシステムプロセスが過去にシンボリックリンクを使用していました(たとえば、initまたはinetd)。synlinkはプロセスIDを保持しますが、実際には何も指していません。長年の間、この振る舞いは変更されました。プロセスは群れとセマフォを使用します。
Znik 2013

5

投稿された既存の回答は、CLIユーティリティに依存しているかflock、ロックファイルを適切に保護していません。flockユーティリティは、Linux以外のすべてのシステム(FreeBSDなど)では使用できず、NFSでは正しく機能しません。

システム管理とシステム開発の初期の頃、ロックファイルを作成する安全で比較的移植性の高い方法は、mkemp(3)またはを使用して一時ファイルを作成し、一時ファイルにmkemp(1)識別情報(つまりPID)を書き込んでからハードリンクすることでした一時ファイルからロックファイルへ。リンクが成功した場合、ロックは正常に取得されています。

シェルスクリプトでロックを使用するときは、通常、obtain_lock()関数を共有プロファイルに配置して、スクリプトからソースを取得します。以下は私のロック機能の例です:

obtain_lock()
{
  LOCK="${1}"
  LOCKDIR="$(dirname "${LOCK}")"
  LOCKFILE="$(basename "${LOCK}")"

  # create temp lock file
  TMPLOCK=$(mktemp -p "${LOCKDIR}" "${LOCKFILE}XXXXXX" 2> /dev/null)
  if test "x${TMPLOCK}" == "x";then
     echo "unable to create temporary file with mktemp" 1>&2
     return 1
  fi
  echo "$$" > "${TMPLOCK}"

  # attempt to obtain lock file
  ln "${TMPLOCK}" "${LOCK}" 2> /dev/null
  if test $? -ne 0;then
     rm -f "${TMPLOCK}"
     echo "unable to obtain lockfile" 1>&2
     if test -f "${LOCK}";then
        echo "current lock information held by: $(cat "${LOCK}")" 1>&2
     fi
     return 2
  fi
  rm -f "${TMPLOCK}"

  return 0;
};

以下は、ロック機能の使用例です。

#!/bin/sh

. /path/to/locking/profile.sh
PROG_LOCKFILE="/tmp/myprog.lock"

clean_up()
{
  rm -f "${PROG_LOCKFILE}"
}

obtain_lock "${PROG_LOCKFILE}"
if test $? -ne 0;then
   exit 1
fi
trap clean_up SIGHUP SIGINT SIGTERM

# bulk of script

clean_up
exit 0
# end of script

clean_upスクリプトのすべての終了ポイントで呼び出すことを忘れないでください。

LinuxとFreeBSDの両方の環境で上記を使用しました。


4

Debianマシンを対象とする場合、lockfile-progsパッケージは良い解決策であると思います。ツールprocmailも付属していlockfileます。しかし時々私はこれらのどちらにも行き詰まっています。

これがmkdir、アトミック性とPIDファイルを使用して古いロックを検出する私のソリューションです。このコードは現在Cygwinセットアップで運用されており、正常に動作します。

それを使用するにはexclusive_lock_require、何かへの排他的なアクセスを取得する必要があるときに呼び出します。オプションのロック名パラメーターを使用すると、異なるスクリプト間でロックを共有できます。さらに複雑なものが必要な場合はexclusive_lock_try、2つの下位レベル関数(およびexclusive_lock_retry)もあります。

function exclusive_lock_try() # [lockname]
{

    local LOCK_NAME="${1:-`basename $0`}"

    LOCK_DIR="/tmp/.${LOCK_NAME}.lock"
    local LOCK_PID_FILE="${LOCK_DIR}/${LOCK_NAME}.pid"

    if [ -e "$LOCK_DIR" ]
    then
        local LOCK_PID="`cat "$LOCK_PID_FILE" 2> /dev/null`"
        if [ ! -z "$LOCK_PID" ] && kill -0 "$LOCK_PID" 2> /dev/null
        then
            # locked by non-dead process
            echo "\"$LOCK_NAME\" lock currently held by PID $LOCK_PID"
            return 1
        else
            # orphaned lock, take it over
            ( echo $$ > "$LOCK_PID_FILE" ) 2> /dev/null && local LOCK_PID="$$"
        fi
    fi
    if [ "`trap -p EXIT`" != "" ]
    then
        # already have an EXIT trap
        echo "Cannot get lock, already have an EXIT trap"
        return 1
    fi
    if [ "$LOCK_PID" != "$$" ] &&
        ! ( umask 077 && mkdir "$LOCK_DIR" && umask 177 && echo $$ > "$LOCK_PID_FILE" ) 2> /dev/null
    then
        local LOCK_PID="`cat "$LOCK_PID_FILE" 2> /dev/null`"
        # unable to acquire lock, new process got in first
        echo "\"$LOCK_NAME\" lock currently held by PID $LOCK_PID"
        return 1
    fi
    trap "/bin/rm -rf \"$LOCK_DIR\"; exit;" EXIT

    return 0 # got lock

}

function exclusive_lock_retry() # [lockname] [retries] [delay]
{

    local LOCK_NAME="$1"
    local MAX_TRIES="${2:-5}"
    local DELAY="${3:-2}"

    local TRIES=0
    local LOCK_RETVAL

    while [ "$TRIES" -lt "$MAX_TRIES" ]
    do

        if [ "$TRIES" -gt 0 ]
        then
            sleep "$DELAY"
        fi
        local TRIES=$(( $TRIES + 1 ))

        if [ "$TRIES" -lt "$MAX_TRIES" ]
        then
            exclusive_lock_try "$LOCK_NAME" > /dev/null
        else
            exclusive_lock_try "$LOCK_NAME"
        fi
        LOCK_RETVAL="${PIPESTATUS[0]}"

        if [ "$LOCK_RETVAL" -eq 0 ]
        then
            return 0
        fi

    done

    return "$LOCK_RETVAL"

}

function exclusive_lock_require() # [lockname] [retries] [delay]
{
    if ! exclusive_lock_retry "$@"
    then
        exit 1
    fi
}

ありがとう、cygwinで試してみましたが、簡単なテストに合格しました。
ndemou 2017年

4

このスレッドの別の場所ですでに説明されているflockの制限が問題ではない場合、これは機能するはずです。

#!/bin/bash

{
    # exit if we are unable to obtain a lock; this would happen if 
    # the script is already running elsewhere
    # note: -x (exclusive) is the default
    flock -n 100 || exit

    # put commands to run here
    sleep 100
} 100>/tmp/myjob.lock 

3
-x(書き込みロック)は既にデフォルトで設定されていることを指摘しました。
Keldon Alleyne 2013

そして、-nなりexit 1、すぐにそれがロックを取得できない場合
Anentropic

@KeldonAlleyneに感謝します。デフォルトの「-x」を削除するようにコードを更新しました。
presto8 '20 / 10/20

3

いくつかのunixはlockfileすでに述べたものと非常によく似ていflockます。

マンページから:

lockfileを使用して、1つ以上のセマフォファイルを作成できます。lock- fileが指定されたすべてのファイルを(指定された順序で)作成できない場合、sleeptime(デフォルトは8)秒待機し、成功しなかった最後のファイルを再試行します。失敗が返されるまでの再試行回数を指定できます。再試行回数が-1(デフォルト、つまり-r-1)の場合、lockfileは永久に再試行します。


どうすればlockfileユーティリティを入手できますか?
Offirmo

lockfileとともに配布されprocmailます。またdotlockfileliblockfileパッケージに対応する代替手段もあります。どちらもNFSで確実に機能すると主張しています。
デスレス氏2014

3

実際、bmdhacksの答えはほぼ良好ですが、ロックファイルを最初にチェックしてから書き込む前に、2番目のスクリプトが実行される可能性がわずかにあります。したがって、両方がロックファイルを書き込み、両方が実行されます。確実に機能させる方法は次のとおりです。

lockfile=/var/lock/myscript.lock

if ( set -o noclobber; echo "$$" > "$lockfile") 2> /dev/null ; then
  trap 'rm -f "$lockfile"; exit $?' INT TERM EXIT
else
  # or you can decide to skip the "else" part if you want
  echo "Another instance is already running!"
fi

このnoclobberオプションは、ファイルがすでに存在する場合にリダイレクトコマンドが失敗することを確認します。したがって、リダイレクトコマンドは実際にはアトミックです。1つのコマンドでファイルを作成して確認します。ファイルの最後にあるロックファイルを削除する必要はありません-トラップによって削除されます。これが後で読む人に役立つことを願っています。

PS私はMikelがすでに質問に正しく回答したことを確認しませんでしたが、たとえばCtrl-Cでスクリプトを停止した後にロックファイルが残る可能性を減らすためのトラップコマンドは含まれていませんでした。これが完全なソリューションです


3

私はロックファイル、ロックディレクトリ、特別なロックプログラムを廃止したいと思っていpidofました。また、可能な限り最も単純なコード(または、少なくとも可能な限り少ない行)が必要でした。if1行で最も簡単なステートメント:

if [[ $(ps axf | awk -v pid=$$ '$1!=pid && $6~/'$(basename $0)'/{print $1}') ]]; then echo "Already running"; exit; fi

1
これは 'ps'出力に敏感です。私のマシン(Ubuntu 14.04、procps-ngバージョン3.3.9の/ bin / ps)では、 'ps axf'コマンドはフィールド番号を乱すASCIIツリー文字を出力します。これは私のために働きました: /bin/ps -a --format pid,cmd | awk -v pid=$$ '/'$(basename $0)'/ { if ($1!=pid) print $1; }'
qneill

2

古いロックファイルを処理するシンプルなアプローチを使用しています。

pidを格納する上記のソリューションの一部は、pidがラップアラウンドできるという事実を無視していることに注意してください。したがって、特に長時間実行されるスクリプトの場合、保存されたpidを使用して有効なプロセスがあるかどうかを確認するだけでは十分ではありません。

noclobberを使用して、一度に1つのスクリプトのみが開いてロックファイルに書き込むことができることを確認しています。さらに、プロセスを一意に識別するのに十分な情報をロックファイルに保存します。pid、ppid、lstartとなるプロセスを一意に識別するためのデータセットを定義します。

新しいスクリプトが起動したときに、ロックファイルの作成に失敗した場合は、ロックファイルを作成したプロセスがまだ残っていることを確認します。そうでない場合は、元のプロセスが不本意な死を遂げ、古いロックファイルが残ったと想定します。その後、新しいスクリプトはロックファイルの所有権を取得し、すべてが再び世界に戻ります。

複数のプラットフォームにわたる複数のシェルで動作するはずです。高速、ポータブル、シンプル。

#!/usr/bin/env sh
# Author: rouble

LOCKFILE=/var/tmp/lockfile #customize this line

trap release INT TERM EXIT

# Creates a lockfile. Sets global variable $ACQUIRED to true on success.
# 
# Returns 0 if it is successfully able to create lockfile.
acquire () {
    set -C #Shell noclobber option. If file exists, > will fail.
    UUID=`ps -eo pid,ppid,lstart $$ | tail -1`
    if (echo "$UUID" > "$LOCKFILE") 2>/dev/null; then
        ACQUIRED="TRUE"
        return 0
    else
        if [ -e $LOCKFILE ]; then 
            # We may be dealing with a stale lock file.
            # Bring out the magnifying glass. 
            CURRENT_UUID_FROM_LOCKFILE=`cat $LOCKFILE`
            CURRENT_PID_FROM_LOCKFILE=`cat $LOCKFILE | cut -f 1 -d " "`
            CURRENT_UUID_FROM_PS=`ps -eo pid,ppid,lstart $CURRENT_PID_FROM_LOCKFILE | tail -1`
            if [ "$CURRENT_UUID_FROM_LOCKFILE" == "$CURRENT_UUID_FROM_PS" ]; then 
                echo "Script already running with following identification: $CURRENT_UUID_FROM_LOCKFILE" >&2
                return 1
            else
                # The process that created this lock file died an ungraceful death. 
                # Take ownership of the lock file.
                echo "The process $CURRENT_UUID_FROM_LOCKFILE is no longer around. Taking ownership of $LOCKFILE"
                release "FORCE"
                if (echo "$UUID" > "$LOCKFILE") 2>/dev/null; then
                    ACQUIRED="TRUE"
                    return 0
                else
                    echo "Cannot write to $LOCKFILE. Error." >&2
                    return 1
                fi
            fi
        else
            echo "Do you have write permissons to $LOCKFILE ?" >&2
            return 1
        fi
    fi
}

# Removes the lock file only if this script created it ($ACQUIRED is set), 
# OR, if we are removing a stale lock file (first parameter is "FORCE") 
release () {
    #Destroy lock file. Take no prisoners.
    if [ "$ACQUIRED" ] || [ "$1" == "FORCE" ]; then
        rm -f $LOCKFILE
    fi
}

# Test code
# int main( int argc, const char* argv[] )
echo "Acquring lock."
acquire
if [ $? -eq 0 ]; then 
    echo "Acquired lock."
    read -p "Press [Enter] key to release lock..."
    release
    echo "Released lock."
else
    echo "Unable to acquire lock."
fi

別の解決策として+1しました。Althougは、AIXでは機能しません(> ps -eo pid、ppid、lstart $$ | tail -1 ps:無効なリスト-o。)HP-UXではありません(> ps -eo pid、ppid、lstart $$) | tail -1 ps:不正なオプション-o)。ありがとう。
Tagar

2

スクリプトの最初にこの行を追加します

[ "${FLOCKER}" != "$0" ] && exec env FLOCKER="$0" flock -en "$0" "$0" "$@" || :

これはman flockの定型コードです。

より多くのロギングが必要な場合は、これを使用してください

[ "${FLOCKER}" != "$0" ] && { echo "Trying to start build from queue... "; exec bash -c "FLOCKER='$0' flock -E $E_LOCKED -en '$0' '$0' '$@' || if [ \"\$?\" -eq $E_LOCKED ]; then echo 'Locked.'; fi"; } || echo "Lock is free. Completing."

これは、flockユーティリティを使用してロックを設定およびチェックします。このコードは、FLOCKER変数をチェックして初めて実行されたかどうかを検出し、スクリプト名が設定されていない場合、FLOCKER変数を初期化して、FLOCKER変数が初期化された状態でスクリプトを再帰的に再起動します。成功し、続行しても問題ありません。ロックがビジーの場合、構成可能な終了コードで失敗します。

Debian 7では動作しないようですが、試験的なutil-linux 2.25パッケージで再び動作するようです。「flock:... Text file busy」と書き込みます。スクリプトの書き込み権限を無効にすることで上書きできます。


1

PIDとロックファイルは間違いなく最も信頼できます。プログラムを実行しようとすると、ロックファイルをチェックできます。ロックファイルが存在するps場合は、プロセスがまだ実行されているかどうかを確認するために使用できます。そうでない場合、スクリプトを開始して、ロックファイルのPIDを独自のものに更新できます。


1

少なくとも私のユースケースでは、bmdhackのソリューションが最も実用的であることがわかりました。flockとlockfileの使用は、スクリプトの終了時にrmを使用してロックファイルを削除することに依存しています。これは常に保証されるわけではありません(たとえば、kill -9)。

私はbmdhackのソリューションについて1つのマイナーなことを変更します。これは、このセマフォの安全な動作にこれが不要であると述べずに、ロックファイルを削除することを意味します。彼のkill -0の使用により、デッドプロセスの古いロックファイルが単に無視/上書きされることが保証されます。

したがって、私の単純化した解決策は、以下をシングルトンの先頭に追加することです。

## Test the lock
LOCKFILE=/tmp/singleton.lock 
if [ -e ${LOCKFILE} ] && kill -0 `cat ${LOCKFILE}`; then
    echo "Script already running. bye!"
    exit 
fi

## Set the lock 
echo $$ > ${LOCKFILE}

もちろん、ロックテストと設定操作は単一のアトミックアクションではないため、このスクリプトには、同時に開始する可能性のあるプロセスが競合する危険があるという欠点があります。しかし、これをlhunathがmkdirを使用するために提案した解決策には、強制終了したスクリプトがディレクトリに残ってしまい、他のインスタンスが実行できないという欠点があります。


1

semaphoricユーティリティ用途flock(presto8によって例えば、上述したように)実装する計数セマフォを。特定の数の同時プロセスを必要に応じて有効にします。これを使用して、さまざまなキューワーカープロセスの同時実行レベルを制限します。

semに似ていますが、はるかに軽量です。(完全な開示:semが私たちのニーズに対してあまりにも重く、単純なカウントセマフォユーティリティが利用可能ではなかったので、私はそれを書いた。)


1

flock(1)の例ですが、サブシェルはありません。flock()されたファイル/ tmp / fooは決して削除されませんが、flock()され、un-flock()されるので、それは重要ではありません。

#!/bin/bash

exec 9<> /tmp/foo
flock -n 9
RET=$?
if [[ $RET -ne 0 ]] ; then
    echo "lock failed, exiting"
    exit
fi

#Now we are inside the "critical section"
echo "inside lock"
sleep 5
exec 9>&- #close fd 9, and release lock

#The part below is outside the critical section (the lock)
echo "lock released"
sleep 5

1

すでに100万回回答されていますが、別の方法で、外部の依存関係は必要ありません。

LOCK_FILE="/var/lock/$(basename "$0").pid"
trap "rm -f ${LOCK_FILE}; exit" INT TERM EXIT
if [[ -f $LOCK_FILE && -d /proc/`cat $LOCK_FILE` ]]; then
   // Process already exists
   exit 1
fi
echo $$ > $LOCK_FILE

現在のPID($$)をロックファイルに書き込むたびに、スクリプトの起動時に、最新のPIDでプロセスが実行されているかどうかを確認します。


1
トラップ呼び出し(または少なくとも通常の場合は終了近くのクリーンアップ)がない場合、最後の実行後にロックファイルが残っており、PIDが後で別のプロセスによって再利用されるという誤検知のバグがあります。(そして最悪の場合、apacheのような長期実行プロセスに恵まれています...)
Philippe Chaintreuil

1
同意します。私のアプローチには欠陥があり、トラップが必要です。ソリューションを更新しました。私はまだ外部依存関係を持たないことを好みます。
Filidor Wiese

1

プロセスのロックを使用すると、はるかに強力になり、不審な出口も処理します。プロセスが実行されている限り、lock_fileは開いたままになります。プロセスが存在すると(シェルによって)(強制終了された場合でも)閉じられます。これは非常に効率的であることがわかりました:

lock_file=/tmp/`basename $0`.lock

if fuser $lock_file > /dev/null 2>&1; then
    echo "WARNING: Other instance of $(basename $0) running."
    exit 1
fi
exec 3> $lock_file 

1

私はスクリプトの冒頭でoneliner @を使用します。

#!/bin/bash

if [[ $(pgrep -afc "$(basename "$0")") -gt "1" ]]; then echo "Another instance of "$0" has already been started!" && exit; fi
.
the_beginning_of_actual_script

メモリ内のプロセスの存在を確認するのは良いことです(プロセスのステータスが何であっても)。しかし、それは私のために仕事をします。


0

群れの道は行くべき道です。スクリプトが突然死んだときに何が起こるか考えてください。群れの場合、群れを失うだけですが、それは問題ではありません。また、邪悪なトリックはスクリプト自体に群れをとることであることに注意してください。もちろん、これにより、許可の問題に完全に先行することができます。


0

早くて汚い?

#!/bin/sh

if [ -f sometempfile ]
  echo "Already running... will now terminate."
  exit
else
  touch sometempfile
fi

..do what you want here..

rm sometempfile

7
これは、使用方法に応じて問題になる場合と問題にならない場合がありますが、ロックのテストと作成の間に競合状態があり、2つのスクリプトを同時に開始できます。一方が最初に終了した場合、もう一方はロックファイルなしで実行されたままになります。
TimB 2008年

3
ポータブルシェルスクリプトについて多くのことを教えてくれたCニュースは、lock。$$ファイルを作成し、それを "lock"でリンクしようとしました-リンクが成功した場合はロックがあり、そうでない場合はロックを削除しました。$$と終了しました。
ポールトンブリン

これは本当に良い方法ですが、何か問題が発生してロックファイルが削除されなかった場合に手動でロックファイルを削除する必要がまだあります。
Matthew Scharley、2008年

2
早くて
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.