シェルスクリプト多重起動の防止(flock(2) を用い、極力可搬に)

シェルスクリプトにおけるアドバイザリロックの実現方法にはいくつかありますが、ロックファイルの有無では、ファイルの消し損ねがよく起きてしまいます。また、pgrep(1) を用いる方法もよく見られますが、同じパスを含むコマンドが実行中であることは多く(特に、エディタで同スクリプトを編集中である場合など)、これまた確実性に欠きます。

最終的にベストの方法は、flock(2) システムコールを使う方法のようです(レンタルサーバなどでは使えないこともあるようですが、まー、今どきならコンテナ環境とか安いですし…)。flock(1) コマンドが使える環境であれば、待ち・ノンブロックの選択、排他ロックの他に参照の共有ロックもできて便利ですが、当座は多重起動を防止したいだけですので、ノンブロックの排他ロックを用います(参考: 「~/bin - using flock to protect critical sections in shell scripts」)。

#!/bin/bash
exec 9> /tmp/${0//\//_}.lock
flock --nonblock 9 || exit 0

上記ではロックファイル名に $0 ということでスクリプト名を用いていますが、シンボリックリンクを用いていた場合には実ファイルを指すようにしてやらないとユニークになりませんね。以下のような感じでしょうか?(参照: 「MacOSX - shell スクリプト自身の絶対パスや格納ディレクトリを求める(OS X でも) - Qiita」)

#!/bin/bash
self="$(p="$0";while true;do t="$(readlink "$p")";cd "$(dirname "$p")";test -z "$t"&&break||p="$t";done;echo "$(pwd -P)/$(basename "$p")")"
exec 9> /tmp/${self//\//_}.lock
flock --nonblock 9 || exit 0

ここで、3 以上のファイルデスクリプタは、コマンドの呼び出し側で使うかスクリプト自身の中で明示的に開くかをしない限りは利用不能であることはありえないので(システムに勝手に使われることはないので)、3 以上の任意の数を使うことができます。上記ではタイプ量が少しでも減るように 9 を使っていますが。exec で開いたファイルは、このプロセスが終了すれば閉じられ、ロックも外れます。

さて、そんなこんなで他の Qiita の記事を見ていると、こんな内容が → 「Bash - プロセスの多重起動をアドバイザリロックで防止する・改 - Qiita」。左記での新要素としては 2 点あり、一つ目は、flock(2) の対象は、書き込みファイルである必要はなく、スクリプト自身の読み出しファイルへのロックで OK なのでロックのための一時ファイルを用意する必要が無い点です。なるほど、そんなものなんですね。二つ目は、Bash-4.1 以降では、10 以上の空いているファイルデスクリプタを割り当てる方法が提供されていることです(bash(1) の man の「REDIRECTION」の項に記載があります)。

以上を勘案すると、こうですか。

#!/bin/bash
exec {lock_fd}< "$0"
flock --nonblock ${lock_fd} || exit 0

おお、すっきりしました。今どきの Linux であれば、上記で良さそうです。 $0 をダブルクォートしているのは、何かと Program Files などのスペースを含むパスにスクリプトを入れたがる Windows 対策です。

しかし Mac OS X では問題がありまして、Bash のバージョンが 3 なので、この記法が使えません。そして、flock(1) コマンドも(Homebrew でも)見当たりません。むぅ。

しかたがないので、Ruby で同等のことをしてみます。OS X であれば Ruby は標準装備なので、これでよさそうです。

#!/bin/bash
exec 9< "$0"
ruby -e "exit(File.open(9).flock(File::LOCK_EX|File::LOCK_NB))" || exit 0

しかし今度は、Linux では Ruby が非標準(最小インストールでは入らないことが多い)であるという問題が出ます。今どきの distro であれば、Perl や Python はシステム標準ですので、可搬性を考えると Perl で書いた方が良いかも知れません。

#!/bin/sh
exec 9< "$0"
perl -e "open(LOCK,'<&=9');exit(!flock(LOCK,6))" || exit 0

これならば、大抵の環境で動きそうです。もっと短く書けそうであればコメントで教えていただけるとありがたいです。

では。


追記(Nov 20 2014):

Perl の例ですが、「6 = LOCK_EX | LOCK_NB」です。

...
/* lock operations for flock(2) */
#define	LOCK_SH		0x01		/* shared file lock */
#define	LOCK_EX		0x02		/* exclusive file lock */
#define	LOCK_NB		0x04		/* don't block when locking */
#define	LOCK_UN		0x08		/* unlock file */
...

即値の「6」がいやならば、ちょっと長くなりますが、こうかな。

#!/bin/sh
exec 9< "$0"
perl -mFcntl=:flock -e "open(LOCK,'<&=9');exit(!flock(LOCK,LOCK_EX|LOCK_NB))" || exit 0

詳細については、右記。定数は、Fcntl モジュールから :flock タグでまるっとインポートする必要がある。 ∥ flock - perldoc.perl.org

These constants are traditionally valued 1, 2, 8 and 4, but you can use the symbolic names if you import them from the Fcntl module, either individually, or as a group using the :flock tag.

Python のスクリプトであれば以下のようになりますが、ワンライナーにはできませんでした。言語的に、例外処理を一行に書けないんですわ。

#!/usr/bin/python

import fcntl
import sys

try:
	fcntl.flock(9, fcntl.LOCK_EX | fcntl.LOCK_NB)
except IOError:
	sys.exit(1)

無理やり書けなくもありませんが、どうかと思われます。もう Perl でよくね?

#!/bin/sh
exec 9< "$0"
python -c $'import sys;from fcntl import *\ntry:flock(9,LOCK_EX|LOCK_NB)\nexcept:sys.exit(1)' || exit 0

いや、例外処理をしなければいいのか。以下ならば Python でも行けますね。

#!/bin/sh
exec 9< "$0"
python -c "from fcntl import *;flock(9,LOCK_EX|LOCK_NB)" 2> /dev/null || exit 0