2026年7月25日

2026年7月25日

シェルスクリプトのロックファイルで排他制御する方法|flockで二重起動を防ぐ

cron で定期実行しているバックアップや WP-CLI のスクリプトが、前回の処理が終わらないうちに次の回が始まり、二重に動いてしまうことがあります。これを防ぐ排他制御は、Linux なら util-linux に含まれる flock コマンドを使うのが最も手堅い方法です。この記事では、ロックファイルの「存在チェック」だけでは不十分な理由、flock を使った3つの書き方とオプション、flock がない環境での mkdir による代替、ロックファイルの置き場所、動作の確認方法までを説明します。

(2026年10月現在)主要な Linux ディストリビューションには util-linux が標準で入っており、flock コマンドを利用できます。macOS には標準では入っていないため、その場合は後半の mkdir を使う方法を参照してください。

なぜ排他制御が必要なのか

二重起動で起きる問題は、処理の内容によって深刻さが変わります。

  • バックアップが重なり、ディスクの読み書きが詰まってさらに処理が遅れる。最悪の場合は同じファイルに同時に書き込んで壊れる
  • WP-CLI で wp cron event run --due-now を数分おきに実行していて、時間のかかるイベントが重なり、同じ処理(メール送信や外部 API 呼び出し)が2回走る
  • デプロイスクリプトが2人の作業者から同時に実行され、ファイルの状態が中途半端になる

処理時間が普段は短くても、データ量の増加やサーバーの負荷で突然長くなることがあります。定期実行するスクリプトには、最初から排他制御を入れておくと安心です。

「ファイルがあれば終了」だけでは不十分な理由

よく見かけるのが次のような書き方です。

# 問題のある例
lock=/tmp/myjob.lock
if [ -e "$lock" ]; then
  echo "実行中のため終了します"
  exit 1
fi
touch "$lock"
# ... 処理 ...
rm -f "$lock"

この方法には2つの問題があります。

1つ目は競合です。「存在を確認する」と「作成する」が別々の操作なので、2つのプロセスがほぼ同時に起動すると、両方が「存在しない」と判断してから作成し、両方とも処理に進んでしまいます。

2つ目は、ロックが残り続ける問題です。処理の途中でエラー終了したり、kill -9 されたり、サーバーが再起動したりすると、rm が実行されずにロックファイルが残ります。以降は「実行中」と判断され続け、誰かが気付いて手で消すまでスクリプトが動きません。バックアップが何日も止まっていた、という事故の典型的な原因です。

flock を使った排他制御

flock は、ファイルに対してカーネルのロック(アドバイザリロック)を取得するコマンドです。ロックはファイルの「存在」ではなく、ファイルを開いているプロセスに結び付いています。プロセスが終了すると、正常終了でも kill -9 でも、カーネルがロックを解放します。このため、前述の「ロックが残り続ける」問題が起きません。ロックの取得自体も1回の操作で行われるので、競合も起きません。

書き方1:crontab でコマンドを包む

既存のスクリプトを変更せずに使える、最も簡単な方法です。

# crontab -e
*/5 * * * * flock -n /home/deploy/locks/wp-cron.lock wp cron event run --due-now --path=/var/www/html >> /home/deploy/logs/wp-cron.log 2>&1

flock ロックファイル コマンド の形で、ロックを取得できたときだけコマンドを実行します。-n を付けると、ほかのプロセスがロックを持っている場合に待たずにすぐ終了します(終了ステータスは1)。ロックファイルが存在しない場合は自動で作成されます。

書き方2:スクリプトの中でファイルディスクリプタを使う

スクリプト自身に排他制御を組み込みたい場合の定番の書き方です。

#!/usr/bin/env bash
set -euo pipefail

lockfile=/home/deploy/locks/backup.lock

exec {lock_fd}>"$lockfile"          # ロックファイルを開き、空いている番号を lock_fd に入れる(bash 4.1 以降)
if ! flock -n "$lock_fd"; then
  echo "$(date '+%F %T') 別のバックアップが実行中のため終了します" >&2
  exit 1
fi

# ここから下はロックを保持した状態で実行される
echo "バックアップを開始します"
wp db export /home/deploy/backup/db-$(date +%F).sql --path=/var/www/html

exec {lock_fd}>ファイル で、スクリプトが終わるまでファイルを開いたままにし、そのファイルディスクリプタに対して flock でロックをかけます。スクリプトが終了すればファイルが閉じられ、ロックも解放されるため、trap で後始末を書く必要はありません。古い bash にも対応させたい場合は、番号を固定して exec 9>"$lockfile" と flock -n 9 を使います。

書き方3:一部の処理だけをロックする

スクリプト全体ではなく、特定の処理だけを排他にしたい場合は、サブシェルを使います。

(
  flock -w 30 9 || { echo "30秒待ってもロックを取得できませんでした" >&2; exit 1; }
  # この中だけが排他実行される
  rsync -a /var/www/html/wp-content/uploads/ /home/deploy/backup/uploads/
) 9>/home/deploy/locks/uploads.lock

-w 秒数 を付けると、指定した秒数だけロックの解放を待ちます。短い処理が重なる可能性があり、後の回も実行したい場合は、-n で諦めるより -w で少し待つ方が合っています。

よく使うオプション

  • -n:ロックを取得できなければ待たずに終了する
  • -w 秒数:指定した秒数まで待ち、取得できなければ終了する
  • -x:排他ロック(既定)。-s は共有ロックで、読み取り処理どうしは同時に実行させたいときに使う
  • -o:コマンドを実行する前にロックのファイルディスクリプタを閉じる。コマンドがバックグラウンドのデーモンを起動する場合、そのデーモンがロックを持ち続けるのを防ぐ
  • -E 数値:ロックを取得できなかったときの終了ステータスを指定する(比較的新しい util-linux で使えるオプションなので、flock --help で確認してください)

-E で、たとえば「実行中のためスキップ」を専用の終了ステータスにしておくと、監視の仕組みで本当の失敗と区別できます。

flock で使ったロックファイルは削除しない

flock 方式では、処理の最後にロックファイルを rm で消してはいけません。消すと、次のような競合が起きる可能性があります。

  1. プロセスAがロックファイルを開いてロックを取得する
  2. プロセスBが同じファイルを開き、ロックの解放を待つ
  3. プロセスAが処理の最後にファイルを削除してから終了し、Bがロックを取得する
  4. プロセスCが起動し、ファイルが存在しないので新しく作成してロックを取得する

BとCはそれぞれ別のファイル(削除された古いファイルと新しいファイル)をロックしているため、同時に処理が進んでしまいます。ロックファイルは中身が空の目印として置いたままで問題ありません。

flock がない環境では mkdir を使う

macOS や、util-linux が入っていない最小構成のコンテナでは、mkdir を使う方法があります。mkdir はディレクトリの作成と存在確認を1回の操作で行い、既に存在すれば失敗するため、競合の問題を避けられます。

#!/usr/bin/env bash
set -euo pipefail

lockdir=/home/deploy/locks/report.lock.d

if mkdir "$lockdir" 2>/dev/null; then
  echo "$$" > "$lockdir/pid"
  trap 'rm -rf -- "$lockdir"' EXIT
else
  pid=$(cat "$lockdir/pid" 2>/dev/null || true)
  if [[ -n $pid ]] && kill -0 "$pid" 2>/dev/null; then
    echo "実行中のため終了します(PID $pid)" >&2
  else
    echo "古いロックが残っている可能性があります: $lockdir" >&2
  fi
  exit 1
fi

# ここから処理

trap ... EXIT で、正常終了やエラー終了のときにロックを消します。bash では kill の既定の SIGTERM で終了する場合も EXIT トラップが動きますが、kill -9 や停電では動かないため、ロックが残る可能性はなくなりません。

そこで、ロックのディレクトリに PID を書いておき、取得に失敗したときに kill -0 でそのプロセスが生きているかを確認しています。生きていなければ「古いロックが残っている」と判断できますが、上の例ではあえて自動で消さずにメッセージを出して終了しています。複数のプロセスが同時に古いロックを見つけて削除と作成を行うと、再び競合が起きるためです。ログや監視で通知を受け、状況を確認してから手で削除する運用にしておくと安全です。なお、kill -0 は別のユーザーのプロセスに対しては権限エラーで失敗するので、ロックを使うスクリプトは同じユーザーで実行してください。

ロックファイルの置き場所と権限

ロックファイルは、そのスクリプトを実行するユーザーだけが書き込めるディレクトリに置きます。

  • root で動かすスクリプト:/run/lock(多くのディストリビューションで /var/lock はここへのリンク)
  • 一般ユーザーやデプロイ用ユーザー:ホームディレクトリの下などに専用ディレクトリを作る
mkdir -p /home/deploy/locks
chmod 700 /home/deploy/locks

/tmp は全ユーザーが書き込めるため、ほかのユーザーが同じ名前のファイルを先に作ってロックを妨げたり、シンボリックリンクを置いて別のファイルを書き換えさせたりする余地があります。共有サーバーでは特に避けてください。

また、NFS などのネットワークファイルシステム上のロックは、カーネルやマウント設定によって挙動が変わります。複数のサーバーにまたがる排他制御が必要な場合は、ファイルロックではなく、データベースのロックや専用の仕組みを検討する方が無難です。

排他制御が効いているか確認する

導入したら、2つのターミナルで同時に実行して確認します。

# ターミナル1: ロックを取って60秒保持する
flock /home/deploy/locks/test.lock sleep 60

# ターミナル2: すぐに終了し、終了ステータス1になれば成功
flock -n /home/deploy/locks/test.lock echo "取得できた"; echo "終了ステータス: $?"

現在保持されているロックは、util-linux の lslocks コマンドで一覧できます。どのプロセスがどのファイルをロックしているかがわかるので、「ずっと実行中と判断される」ときの調査に役立ちます。

systemd タイマーという選択肢

cron の代わりに systemd タイマーでサービスユニットを起動する構成では、同じユニットが実行中の間は、タイマーが発火しても2つ目のインスタンスは起動されません。新しく定期実行の仕組みを作るなら、この性質を利用するのも1つの方法です。ただし、手動で同じスクリプトを直接実行した場合には効かないので、スクリプト側にも flock を入れておくと二重の備えになります。

まとめ

  • 「ファイルがあれば終了」方式は、同時起動の競合とロックの残留という2つの問題がある
  • Linux では flock を使う。プロセスの終了と同時にカーネルがロックを解放するため、ロックが残らない
  • crontab で flock -n ロックファイル コマンド と包むのが最も手軽。スクリプト内なら exec {fd}>ファイル と flock -n "$fd"
  • flock のロックファイルは削除しない。削除すると別ファイルのロックになり、排他が崩れる
  • flock がない環境では mkdir と trap を使い、古いロックは自動削除せず通知して確認する
  • ロックファイルは実行ユーザー専用のディレクトリに置き、2つのターミナルで同時実行して動作を確かめる

お気軽にご相談ください

お見積りへ お問い合わせへ
▲