search
LoginSignup
431

posted at

updated at

シェルスクリプトで安全簡単な二重起動防止・排他/共有ロックの徹底解説

はじめに

シェルスクリプトで二重起動防止やロックをする方法を検索すると、いろいろな方法や書き方が見つかりますが、どれを使えばよいのか、本当に正しく動くのか、不安になりますよね? ディレクトリ (mkdir) やシンボリックリンク (ln) を使った独自実装の例も見かけますが、エラー発生時や予期せぬ電源断、CTRL+C で止めたときなどでも問題は発生しないのでしょうか?

まず、ディレクトリやシンボリックリンクを使った独自実装はしない。これを肝に銘じてください。シェルスクリプトでのロック管理はとても難しく、一般的な排他制御の知識に加えて、シェルスクリプト特有の問題、シグナルやトラップ、サブシェルや子プロセスの問題、さらには特定のシェル固有の仕様やバグなどさまざまな問題に対処する必要があり大変です。独自実装の例では古いロックファイルが残ってしまい、それをいつどのタイミングで片付ければ安全なのか?という問題に悩まされます。それらに対応していくと悪い意味で凝った作りの長くて複雑なコードが出来上がってしまいます。

シェルスクリプトでロックを行うのであれば flock コマンドを使うのが鉄板です。専用のコマンドがあるのですからそれを使いましょう。flock コマンドは通常の flock(2) を使った実装の他、fcntl(2)(別名 POSIX ロック)を利用する実装もあるため POSIX 準拠のほとんどの環境で動作可能なコマンドなので、可搬性を持たせたいシェルスクリプトにも適しています。OS が備えている排他制御のための専用機能(システムコール)を利用しているため信頼性が高くパフォーマンスも良いです。ほぼ全ての POSIX 環境で利用可能でロックファイルも後片付け不要で使い方が簡単な flock コマンドはシェルスクリプトのロックの現代のベストプラクティスであると言えます。シェルスクリプトでのロックは解決済みの問題です。歴史的な手法は古い手法、すでに過去のものとなっています。

しかしながら、その簡単な flock コマンドであっても面倒な書き方やシェル依存の書き方を紹介している記事があったりします。正しいと言える記事もあるのですが包括的ではなく、他の記事を見ると別の書き方をしてあって何が違うのか、何が良い書き方なのかすぐには判断できません。この記事ではシェルスクリプトで二重起動防止や排他ロック・共有ロックを行う時、どのように書けばいいかを徹底的に解説します。特記部分を除き POSIX に準拠したどのシェルでも動作する書き方で書いています。おそらく他の記事の解説内容を全てカバーできていると思います。

2022-09-28 仮追記 lslocks コマンドでロックされているファイル一覧を見ることが出来るようです。 後で詳細を調べてなにかしら追記しようと思います。

注意 トランザクションの話は対象外

この記事で扱っているのはロックの話だけです。トランザクションをどうやって実現するかについてはこの記事の対象外です。気が向いたらトランザクションの話を別記事で書くかもしれませんが、エラーなどで不意に終了したときに「処理を全く行っていない状態」にするためには別でそのような実装をしなければいけません。例えば処理途中のデータをテンポラリファイルに書き出すなどです。またシェルスクリプトというか、プレーンなテキストファイルを使っている限りデータベース並みのトランザクションを実装するのは事実上不可能です。この記事の最後にデータベースの話も書いています。

なぜ flock コマンドを使うのが良いのか?

1. flock コマンドはどこでも動く

flockコマンドは Linux だけではなく macOS、FreeBSD、UNIX (一部?) などで使うことが出来ます(下記参照)。環境によってはインストールが必要ですが、必要な作業はそれだけです。90 パーセント以上の OS をカバーしており、台数ベースで言えば 99 パーセントといっても過言ではないでしょう。100 パーセントではないかもしれませんが、100 パーセントを目指すのはコストが高くつくので必要にならない限り考慮するのはやめるべきです。ガンカーズの UNIX 哲学の「90 パーセントの解を目指す」というやつです。

2. 使い方が簡単

flock コマンドは使い方がとても簡単です。既存のスクリプトをそのまま、または僅かな行数を加えるだけで二重起動を禁止したり、シェルスクリプトの途中で部分的にロックを行ったりする(ロックをかけた状態でシェルスクリプトの処理を続ける)ことが出来ます。ファイルディスクリプタを使った少々見慣れない書き方をするので、戸惑うかもしれませんがわかってしまえば簡単です。

3. 終了処理・エラー処理が不要

flock コマンドは終了時やエラー発生時に手動でアンロック(ロックの解放)をする必要がありません。たとえ予期せぬ電源停止などが発生しても、独自実装をした場合に発生するロックファイルの削除漏れなど回避が難しい問題が発生しません。

4. パフォーマンスが良い

flock コマンドは OS が持っているシステムコール flock(2) (実装によっては fcntl(2))によるアドバイザリロックを使用します。これは OS の機能(システムコール)であるため、ファイルやディレクトリを使った擬似的なロック処理を実装した場合よりもパフォーマンスが高いです。

5. 排他ロックと共有ロックに対応

flock コマンドは共有ロックにも対応しています。flock コマンドが対応している言うよりそこから呼び出している flock(2) システムコール自体が排他ロックだけではなく共有ロックにも対応しており、つまり OS の基本機能です。

共有ロックはあまり使うことがないかもしれませんが、読み込みがメインのプロセスを多数起動したりする場合に有効でしょう。ロックが取得できるまで待ったり、タイムアウトを設定したり、取得できない場合はすぐに終了したりと、基本的な機能をサポートしています。

6. OS の機能で信頼性が高い

ロックの処理というのは極めて厳密な処理が必要な難しい処理です。だからこそ OS にシステムコールが用意されたわけです。専用のシステムコールが必要とされるようなものを、それなしで実現するのは困難です。正常系の範囲では動作しているように見えてもエラー発生時の対応が大変になります。flock コマンドは OS の API を利用しており信頼性が高いです。

7. 他言語との間でロック制御を共有可能

独自実装の手法の場合、それぞれの言語で同じ方法でロック制御を実装しなければなりませんが、flock コマンドが内部的に使用するシステムコール flock(2) は OS の機能であるため、他の言語との間でもロック制御を共有することが可能です。ただしロックを行う仕組みには flock(2) と fcntl(2) があり、同じ仕組みでロックを制御している必要があります。flock(2) を使ったロック制御は多くの言語でライブラリが用意されているため比較的簡単に他の言語のプログラムとの間でもロック制御を共有することが出来るでしょう。これはシェルスクリプトから他の言語へ、またはその逆へコードを書き換える時に重要になります。

ロック処理を独自実装する事について

この記事では、ロック処理をシェルスクリプトで独自実装するのではなく「移植性も信頼性も高いコマンドが、すでに作られているのだからそれを利用せよ」と主張しています。実はその一方で私は独自でロック処理(mkdir とディレクトリを使った排他ロック)を書いているものもあります。それを見つけられて矛盾しているではないかと指摘されたくはないのでその理由を説明したいと思います。

理由の一つは flock コマンドがインストールされていない環境でも動くようにしたかったからです。通常は何らかのコマンドをインストールできないということはありません。OS に標準でインストールされているものだけを使って何かを作ることはなく、いずれにしろ何かのソフトウェアをインストールして使います。しかしそれでも flock コマンドがインストールされていない環境でも動かしたいという理由が私にはありました。

もう一つの理由は並列処理の同期のためのロックだからです。この記事のロックというのは一般的に異なるプログラム、関連しないプロセス同士での排他制御のためのロックです。しかし私が書いたのは「一つのコマンドから内部で並列に実行するコマンドの同期のためのロック」なのです。システム全体でグローバルなロックを使っているのではなく、あくまで一つのプロセスの子プロセスを制御するためのものです。単純な排他ロックしか必要なく最小限のかなり短いコードで十分です。プロセス終了時に確実にロックファイルも削除できるように親プロセスで管理するという構成にしており、たとえロックファイルが残ったとしても、異なるプログラムの起動では異なるロックファイルを使うため関係ありません。

そのような理由と性質の違いがあるため、flock を使えず、また使う必要もありませんでした。このように独自で実装しても問題ない場合もあります。明確な理由があるのであれば独自で実装しても良いと思います。ですが、すでにコマンドとして完成されており、そのコマンドを呼び出すだけで使えるのであれば、それに越したことはありません。何事にも例外はありますが理由のない車輪の再発明は悪です。「作らない技術」という言葉もありますよね。

Linux 以外 (UNIX) で flock コマンドを使う方法

flock コマンドは Linux 環境では通常 util-linux パッケージによってあらかじめインストールされているはずなので何も準備することなく使うことが出来ます。Cygwin、msys、BusyBox にも組み込まれているようです。

BSD 系の UNIX では FreeBSD では Ports から util-linux 版がインストール可能なようです。NetBSD では標準パッケージがあるようです。macOS では Homebrew から discoteq/flock 版がインストール可能です。discoteq/flock 版は Linux (Debian & CentOS), Illumos (OmniOS & SmartOS), Darwin & FreeBSD に対応しているようです。

discoteq/flock のソースコードは 400 行もないシンプルなものです。flock(2) が実装されていない環境向けに fcntl(2) を使ったラッパー関数も実装されているので、Linux / BSD 系だけではなく System V 系の UNIX もカバーしていると思われます。実際 Illumos は System V 系の UNIX です。

OS のファイルロックのシステムコールについて知りたい方は、オライリーの「ファイルロックと新OFDロック」などを参照すると良いでしょう。

flock コマンドの公式ドキュメントを読もう❗

flock コマンドの使用方法は公式ドキュメントにちゃんと書いてあります。Linux なら man コマンドで見ることが出来ます。ただし英語版を参照してください。(現時点での)日本語版は古く使用方法が載っていません。LANG=C man flock で英語版のドキュメントを見ることが出来ます。ウェブからであれば「flock(1) — Linux manual page」で見ることが出来ますが、なんかレイアウトがおかしくなっているので、コードを読む時は「Ubuntu Manpage: flock - manage locks from shell scripts」の方が良いです。なお最新のドキュメントの方が記述内容が増えているのでコード以外は最新版を参照した方が良いでしょう。

flock が行うロックについて

排他ロックと共有ロック

flock はデフォルトでは排他ロック (-e, -x, --exclusive) を使用します。これはオプションを指定することで共有ロック (-s, --shared) に変更することが出来ます。排他ロックがかかっている場合、他のプロセスからは排他ロックも共有ロックもかけることが出来ません。共有ロックがかかっている場合、他のプロセスから共有ロックをかけることはできますが排他ロックをかけることはできません。

一般的に共有リソース(共有ファイル等)に対して複数のプロセスから書き込みを行う時は排他ロックを使います。これは複数のプロセスから同時にデータを変更するときにデータが矛盾したり壊れたりする可能性を防ぐためです。読み込みだけを行う場合は共有ロックを使います。複数のプロセスから同時にデータを読み込むことは安全ですが、読込中に書き込みが行われると矛盾したデータが生じる可能性があるためです。共有ロック中は排他ロックは妨害されます。ちなみに共有ロックは読み込みだけを行う時に使うロックであるためリードロックと言われることもあります。

余談ですが、そもそも複数のプロセスから読み書きしない場合や、複数のプロセスから同時に読み書きされても大きな問題が生じない方法を使っている場合はロックは不要です。例えばログファイルは複数のプロセスから書き込んでも追記しか行わないためロックは不要です(厳密に言えば一回の書き込み量が大きすぎる場合は、複数のプロセスから同時に書き込んだ時に出力が混ざることがあります。詳しくは PIPE_BUF を検索してください)。しかし共有データを扱う時に排他制御が不要な場合は例外です。複数のプロセスからデータを共有するならば、排他制御が必要になる場合が多数です。

ロックできない場合の動作

flock はデフォルトではロックを確保できるまで待ちます。-n, --nb, --nonblock オプションを指定することでロックが確保できない場合にすぐに終了させることが出来ます。また -w, --wait, --timeout seconds オプションでロックを待つ時間を指定することも出来ます。待つ時間には小数の値(例 0.5 秒など)が指定できます。0 を指定すると -n オプションを指定したのと同じ意味になります。ロックを指定時間内に確保できなかった場合はデフォルトで終了ステータス 1 で終了します。終了ステータス値は -E, --conflict-exit-code number オプションで変更可能です。

ロックファイルについて

flock の使用方法は、大きく次の三つがあります。

  1. flock [options] file|directory command [arguments]
  2. flock [options] file|directory -c command
  3. flock [options] number

この時、1 と 2 の使い方ではロックファイル(ディレクトリ)を指定します。3 の使い方はファイルディスクリプタの番号を指定する方法で主にシェルスクリプトの中で使います。

file|directory には任意のファイルまたはディレクトリを指定します。同じロックを使いたいプロセスは同じロックファイル・ディレクトリを指定します。このファイル・ディレクトリはロックの管理に使われるだけなので、ファイルの内容を変更したりディレクトリの中にファイルが作られたりすることはありません。ファイルを指定した時に、そのファイルが存在しなければ新たに作成されます。 存在しないディレクトリ(末尾 /)を指定した場合はエラーになります。

新たに作成される時のパーミッションは touch コマンドでファイルを作成した場合と同じように umask の値に基づいて設定されます。もしセキュリティ的な事情でパーミッションが重要な場合、あらかじめファイル・ディレクトリを作成しておくこともできます。この場合、ロックをかけるには読み込み権限だけがあれば十分です。

ロックに関する注意点(NFS や CIFS)

最新の flock コマンドのドキュメント には NOTES の項目が追加されており、flock コマンドを使用する際の 2 つの注意点が書かれています。

  • flock コマンドはデッドロックを検出しません。
  • NFS や CIFS などのファイルシステムによっては flock(2) の実装が限定的で flock が常に(マウントオプションによっては?)失敗することがあります。

どちらも詳細は flock(2) を参照してくださいとのことです。

NFS や CIFS に対応していないことはクリティカルな問題にはなりません。なぜなら NFS や CIFS だけで構成されたシステムはまずないからです。ロックファイルを置く場所だけをローカルのディスクにすれば OK です。これは注意が必要な問題と言うだけです。一部のファイルシステムで使えないからと言って flock は使えないと結論付けないようにしてください。

もしネットワーク経由で複数のクライアントがいて分散ロックが必要なのであれば、それは(ネットワーク)ファイルシステムを使ったロックを使うようなレベルではなく、データベースサーバーなどのネットワークプロトコルを利用しましょうという話でスケールが異なります。そのようなものは、そもそもシェルスクリプトでやるような内容ではありません。

使用方法

3 種類の使用方法

flock の使用方法は、大きく次の三つがあります。

A: flock [options] file|directory command [arguments]
B: flock [options] file|directory -c command
C: flock [options] number

上記のうち A と B は flock コマンドから別のプログラムを呼び出すという使い方です。シェルスクリプトの中でロックの制御を行うには C を使います。

A: コマンド呼び出しのラッパーとして使う

A: flock [options] file|directory command [arguments]

既存プログラムにロック制御を加える

例えばすでにプログラム(シェルスクリプト以外でも OK)があり、それを cron から実行しているが、それに対してプログラムを修正することなく二重起動を防止したいなどという時に使う方法です。例えば次のような一時間おきにプログラムを実行する cron の設定があったとします。

0 * * * * prog.sh

まれに一時間で処理が終わらない可能性があり、その時に二重起動を防止したい場合は以下のようにいつものプログラムの呼び出しの前に、flock <file|directory> をつけるだけです。

0 * * * * flock /tmp/prog.lock prog.sh

上記の例では /tmp/prog.lock をロックファイルとして使用しています。同じロックファイルを使用するのであれば他のプログラム(prog.sh 以外)との間でもどちらか一つしか起動できなくすることが出来ます。

使い方に特に難しい所はないと思いますが、直接 prog.sh を実行した場合は二重起動が防止されることはないので注意が必要です。直接プログラムを実行しても二重起動を防止したい場合は、次項のように該当のシェルスクリプトを修正する必要があります。

シェルスクリプトの二重起動防止

シェルスクリプトの内部からロックをかけたい。つまりプログラムの呼び出しの前にいちいち flock をつけたくないという場合にはシェルスクリプトの冒頭に次のようなコードを書きます。

#!/bin/sh

[ "$FLOCKER" = "$0" ] || exec env FLOCKER="$0" flock -n "$0" "$0" "$@"

... 以下通常通りのシェルスクリプトコードを書く ...

上記のコードは、flock のドキュメントに載っているコードを少し短く書き換えたものですが、終了ステータスの状態なども含め意味はまったく同じです(厳密に言えば exec env が失敗した時に私が書き換えたコードは終了ステータスがエラーになりますが、元のコードは正常終了になってしまいます)。ダブルクォートは重要なのでこれ以上短くはできません。もし私が書き換えたコードが不安なら flock のドキュメントに載っている以下のコードを利用してください。

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

This is useful boilerplate code for shell scripts. Put it at the top of the shell script you want to lock and it'll automatically lock itself on the first run. If the env var $FLOCKER is not set to the shell script that is being run, then execute flock and grab an exclusive non-blocking lock (using the script itself as the lock file) before re-execing itself with the right arguments. It also sets the FLOCKER env var to the right value so it doesn't run again.

翻訳 これはシェルスクリプトのための便利な定型コードです。これをロックしたいシェルスクリプトの先頭に置いて、初回実行時に自動的に自分自身をロックするようにします。環境変数 $FLOCKER が実行中のシェルスクリプトで設定されていないならば、正しい引数で自分自身を再実行する前に、flock を実行して排他的なノンブロッキングロックを行います(スクリプト自身をロックファイルとして使用する)。また、FLOCKER 環境変数 を正しい値に設定して再実行されないようにします。

解説 このコードに exit はありませんが exec env 〜 を実行した後に、このコードに処理が戻ってくることはありません。なぜなら exec を使用しており現在のプログラム(シェルスクリプト)が flock に置き換わるからです。flock は現在と同じ引数で自分自身 ($0) を再実行します。flock の排他ロック(-e オプション)はデフォルトなので省略可能です。-n オプションはノンブロッキングにするためのオプションで二重起動であればロックが確保できるのを待たずにすぐに終了します。もしロックが確保できるまで待ちたいのであれば -n オプションを取り除きます。末尾の || :: は何もしないコマンド)は再実行されたスクリプトで [ "${FLOCKER}" != "$0" ] の結果が異常終了(終了ステータス 1)になったものを正常終了にリセットするためのものですが条件を反転させれば不要になります。実際の所わざわざ正常終了にリセットせずとも直後で $? を参照したりしていなければ || : は省略しても問題ないコードだったりします。初回実行前に環境変数 FLOCKER に実行するプログラム名を入れていれば誤動作しますが、そのような天の邪鬼なことをする人はいないでしょう。念の為に言うのであれば環境変数 FLOCKER は未定義のままにしておいてください。

B: シェルスクリプトコードを実行する

B: flock [options] file|directory -c command

A との違いは A は別のプログラムを直接起動するのに対して、B はシェルを使って引数のシェルスクリプトコードを実行するという点です。(おそらく sh -cbash -c 相当のコマンドを実行している)

A: flock /tmp prog.sh foo bar baz
B: flock /tmp -c "echo foo; echo bar"

おそらく B の書式を使うことは、あまりないでしょう。シェルスクリプトを作れば出来ることを作らずにやりたいというときぐらいです。ちなみに実行するシェルは環境変数 SHELL で指定されているシェルが使われるようです。現在使用しているシェルとは限らないので注意してください。実行されるシェルが重要ならシェルスクリプトにして A を使った方が良いです。

この書式をあえて使うことはなさそうですし、基本的に A と変わらないので、これ以上の解説は不要でしょう。

C: シェルスクリプトの中でロック制御を行う

C: flock [options] number

この方法の考え方は「ファイルをオープンしている間ロックをかける」です。ファイルがクローズされるとロックは自動的に解放されます。シェルスクリプトの処理の中で部分的にロックをかけて、必要なくなったらすぐに解放したいという時に使います。

シェルスクリプトでファイルをオープンしたりクローズしたりするという話はあまり聞き覚えがないかもしれませんが exec コマンドでファイルのオープン・クローズ相当のことを行うことが出来ます(別のプログラムに置き換えるのとは異なる使い方)。一般的には exec はリダイレクト先の変更やファイルディスクリプタのオープン・クローズとして説明されるため exec がファイルのオープン・クローズ相当の意味を持っていることがあまり知られてない気がします。

ただし通常は exec を使って書く必要はありませんexec を使うと自分でファイルのオープンやクローズを管理しなくてはいけないので面倒です。シェルスクリプトは暗黙的にファイルのオープンとクローズの管理を行うことが出来ます。

シェルスクリプト全体に渡ってロックをかける(二重起動防止)

二重起動防止のもう一つの書き方です。「通常は exec を使って書く必要はありません」と言っておいてなんですが、この使い方では exec を使った方が簡単です。

#!/bin/sh

exec 9<"$0"
flock -n 9 || {
    echo "this script is already running" >&2
    exit 1 # 任意の終了ステータスで終了する
}

# エラーメッセージが不要な場合は単にこれだけで良い
# flock -n 9 || exit 1

「シェルスクリプトの二重起動防止」のコードとの違いは、こちらは(POSIX の範囲だと 9 までしか使えない)ファイルディスクリプタを一つロックのために使用しなければいけません。一方でスクリプトの再実行がないため、ほんの僅かですがパフォーマンスが良いのと、環境変数 FLOCKER が不要というメリットがあります。ファイルディスクリプタを一つ使うと言ってもそんなに必要になることはありませんし、総合的に考えるとこちらの方が良いかもしれません。

注意 exec 9<"$0" の 向きを間違えて exec 9>"$0" と書かないように注意してください。シェルスクリプト自体が空ファイルになってしまいます。(はい、この間違いをしたのは私です……。この記事の下の方のコメント参照)

ロックの取得・解放を自動的に行う

この使い方も flock のドキュメントに書いているので、そちらも参照してください。まずは flock のドキュメントを例に基本の書き方です。

#!/bin/sh

(
  flock 9 # 排他ロックを取得できるまで待つ(デフォルトの動作)

  # この区間はロックされている

) 9>/tmp/lockfile

# ここではロックされていない

( ... ) 9>/tmp/lockfile はサブシェル ( ... ) が実行されている間、ファイルディスクリプタ番号 9 に関連付けて /tmp/lockfile ファイルをオープンするという書き方です。ファイルディスクリプタ番号は POSIX シェルの仕様に厳密に準拠するのであれば 3 〜 9 を使用してください。それ以上の番号を使う書き方は後述します。

( ... ) を抜けるとファイルは自動的にクローズされロックも解除されます。手動でファイルをクローズする方法よりも使い方が簡単です。少し異なるもう一つの書き方の例です。

#!/bin/sh

{
  flock 9 # 排他ロックを取得できるまで待つ(デフォルトの動作)

  # この区間はロックされている

} 9>/tmp/lockfile

# ここではロックされていない

違いは、サブシェルである ( ... ) か、サブシェルでない { ... } かです。 flock のドキュメントにはサブシェルの書き方しか載っていませんが、別にサブシェルでなくとも構いません。{ ... } グループを抜けると自動的にファイルはクローズされます。この 2 つの違いは ... の部分で行った変数の代入などがグループの外に伝わるかどうかです。

#!/bin/sh

var=1

(
  var=2
)
echo "$var" # => 1 (サブシェルの中で行った処理はサブシェルの外には反映されない)

{
  var=3
}
echo "$var" # => 3 (グループの中で行った処理はグループの外には反映される)

どちらを使うかは状況次第ですが、単に自動的にロックを解放させたい場合(自動的にファイルはクローズさせたい場合)は、{ ... } を使った方が軽く、おそらく変数の代入なども外に伝えたいと思われるので便利です。

ロックの取得・解放を手動で行う

exec を使用してロックの解放を手動で行う方法です。面倒なだけなので推奨しません。と言ってもサブシェルを使っているのであれば、サブシェル終了時に自動的に開放されますし、サブシェルを使わなくても少なくともシェルスクリプト終了時にはいずれにしろロックは解放されます。

#!/bin/sh

exec 9>/tmp/lockfile # 1. ファイル(ディスクリプタ)を開く
flock 9 # 2. 排他ロックをする

# この区間はロックされている

flock -u 9 # 3. 排他ロックを解放する
exec 9>&- # 4. ファイル(ディスクリプタ)を閉じる

# 何らかの処理を行う(ここではロックされていない)

上記のコードは exec 9&- を使って手動でロックを解放してファイルを閉じています。丁寧に書いていると言えば聞こえは良いですが、ただ単に無駄な処理を書いているだけです。まず、3. に関しては 4. でファイルを閉じているのだから不要です。ロック範囲が明確になる { ... } 9>/tmp/lockfile を使った方が可読性が高く優れています。

「ロックの取得・解放を手動で行う」場合の少しマシな例です。

#!/bin/sh

(
    exec 9>/tmp/lockfile
    flock 9 # 2. 排他ロックをする

    # この区間はロックされている
)

# ここではロックされていない

上記の例ではサブシェルを使っており、サブシェル終了時に自動的にファイルが閉じられロックが解放されるのでそこまで冗長ではありません。それでも ( ... ) 9>/tmp/lockfile を使用した方がわずかにシンプルです。

exec を使う方法というのは、ファイルを開く・閉じるというのをイメージしたやり方で、そちらの方に慣れている人にとってはわかりやすいのかもしれません。しかし { ... }( ... ) を使ってグループ化すれば、そのグループ内でだけファイルを開くことが出来ます。明示的に開いたり閉じたりする必要がないので、通常は手動で行うこの方法は避けたほうが良いでしょう。

補足: ロックファイルのオープン方法による違い

先程の例ではロックファイルのオープンに > を使用していました。別の使い方として < を使うことも出来ます。他にも >><> などがありますが、ロックをかける上での違いはないのであえて使う必要はないでしょう。

> でオープンする場合

> でオープンする場合、もしロックファイルが存在しない場合に自動的に作成されます。便利ですが書き込み権限が必要になります。

#!/bin/sh
{
  flock 9
  # ...
} 9>/tmp/lockfile # 書き込み権限が必要

既存ファイルの場合、オープン時に中身が空になるので要注意です。
シェルスクリプト内で自ファイルを指定すると…泣く事になります。

< でオープンする場合

< を使うとファイルがあらかじめ存在している必要がありますが、読み込み権限だけで使うことができます。ロックファイルとして使うのに必要なのは読み込み権限があることだけです。

#!/bin/sh
{
  flock 9
  # ...
} 9</tmp/lockfile # あらかじめ作成しておく必要がある

< をつかう場合ロックファイルだけではなくロックディレクトリをつかうことも(おそらく)可能です。

補足: 9 を超えるファイルディスクリプタを使う方法

POSIX シェルの標準規格で規定されているファイルディスクリプタ番号は 9 までで 10 以上は使えません。0、1、2 は標準入力、標準出力、標準エラー出力ですでに使われているので(POSIX で標準化されている範囲では)3 から 9 までの番号しか空きがありません。通常はこれだけあれば十分だと思いますが、それより多くのファイルディスクリプタを使いたい場合もあるかもしれません。一部のシェルでは 10 以上の番号を使うことが出来ます。

またファイルディスクリプタの番号は現在のプロセスで共有する番号なので、他の部分で同じ番号を使用している可能性を否定できません。マジックナンバー的なので直接数値を指定したくないと思うのも普通です。これも一部のシェルに限られますが、空いているファイルディスクリプタ番号を自動的に割り当てることも可能です。

いくつかの書き方があります。この項目の書き方は POSIX シェルの標準規格には含まれていない拡張機能なので注意してください。

bash 3.00 以降
#!/bin/sh
{
  flock 10
  # ...
} 10>/tmp/lockfile

macOS の /bin/sh (bash 3.2.57) でも動作する書き方です。ちなみに最大のファイルディスクリプタ番号は ulimit で制限されている可能性があります(ディスクリプタ数ではなく最大値)。大きなファイルディスクリプタ番号で動かない場合、ulimit -n <limit> で上限を上げてください。

bash 4.1.0 以降、ksh93 2006 年頃以降
#!/bin/sh
{
  flock "$fd"
  # ...
} {fd}>/tmp/lockfile

この方法は空いているファイルディスクリプタ番号を自動的に割り当ててくれるので、使えるシェルの場合におすすめです。fd は変数名なので自由に名前をつけられます。ksh93 は 2006年頃のバージョンから使えるようですが、ファイルディスクリプタ番号がとても大きな値になる場合があり、怪しい動作をしているので 2007 年頃以降がおすすめです(と言ってもそんな古いバージョンを使う人はいないと思いますが)

重要 bash では { ... } {fd}>/tmp/lockfile を使用しても自動で変数に代入したファイルディスクリプタを閉じることはありません。{ ... } 9>/tmp/lockfile のようにファイルディスクリプタ番号を直接指定した場合は閉じます。このバグのような挙動は bash 5.2 (2022-09) 以降で shopt -s varredir_close を有効すると解決します。{ ... } の代わりにサブシェル ( ... ) を使った場合は問題ありません。

bash 4.1.0 以降、ksh93 2007 年頃以降、zsh 5.0 以降
#!/bin/sh
func() {
  flock "$fd"
  # ...
}
func {fd}>/tmp/lockfile

ファイルディスクリプタ番号を自動的に割り当ててくれて zsh でも動作する書き方ですが、関数を定義する必要があります。ksh93 は 2006 年頃のバージョンでも一応動作するのですが、しばしば fd にマイナスの値が入るバグが有るようなので 2007 年以降でないと安心して使えません。

重要 bash では func {fd}>/tmp/lockfile を使用しても自動で変数に代入したファイルディスクリプタを閉じることはありません。func 9>/tmp/lockfile のようにファイルディスクリプタ番号を直接指定した場合は閉じます。このバグのような挙動は bash 5.2 (2022-09) 以降で shopt -s varredir_close を有効すると解決します。

bash 4.1.0 以降、ksh93 2007 年頃以降、zsh 5.0 以降
#!/bin/sh
{
  exec {fd}>/tmp/lockfile
  flock "$fd"
  # ...
  exec {fd}>&-
}

# サブシェルならクローズしなくとも良い
(
  exec {fd}>/tmp/lockfile
  flock "$fd"
  # ...
)

ファイルディスクリプタ番号を自動的に割り当ててくれて zsh でも動作する書き方ですが、exec を使うため適切にクローズする必要があります。サブシェル ( ... ) の場合はサブシェルを抜けると自動的にクローズされますが、{ ... } の場合はグループを抜けても自動的にクローズされません。ksh93 は 2006 年頃のバージョンでも一応動作するのですが、しばしば fd にマイナスの値が入るバグが有るようなので 2007 年以降でないと安心して使えません。

あまり使わないオプションについて

-F, --no-fork

コマンドを実行する際に fork しないためのオプションです。具体的な例で示すと以下のような違いがあります。

$ flock /tmp ps --forest
    PID TTY          TIME CMD
1207333 pts/0    00:00:00 bash
1207445 pts/0    00:00:00  \_ flock
1207446 pts/0    00:00:00      \_ ps

$ flock --no-fork /tmp ps --forest
    PID TTY          TIME CMD
1207333 pts/0    00:00:00 bash
1207447 pts/0    00:00:00  \_ ps

-F, --no-fork をつけない場合、flock コマンドの子プロセスとしてプログラムが起動しています。一方 -F, --no-fork をつけた場合は、flock が間に入っていないような形になります。これは flock プロセスから別プログラムを起動するのではなく、flock プロセスそのものが別のプログラムに置き換わっているからです。

このオプションを使用したい場合はあまりないと思いますが、起動しているプロセス数を少しでも減らしたいとか、何らかの理由でプロセスツリーを厳密に想定したものにしたい(例 ps コマンドの上は bash であるはずだ!)場合ぐらいでしょう。

このオプションは次項の --close と互換性がありません。--clone は別の flock 自身がロックで保持しているファイルディスクリプタを閉じた上で別プログラムに置き換えるため、ロックを保持するプログラムがいなくなるからです。

-o, --close

コマンドを実行する前に、ロックを保持しているファイル記述子を閉じます。これは別プログラムがロックを保持していないはずの子プロセスを生成した場合に便利 (This is useful if command spawns a child process which should not be holding the lock) というような事が書かれているのですが、どういう時に便利なのかよくわかっていません。

-u, --unlock

Drop a lock. This is usually not required, since a lock is automatically dropped when the file is closed. .

ロックを解除します。すでに説明したとおりファイルが閉じられると自動的にロックは解除されるため、通常は必要ありません。しかし、特殊な場合には必要になることがあるようです。例えばロックされていないはずのバックグランドプロセスがコマンドグループからフォークされた場合などには必要な場合がある (it may be required in special cases, for example if the enclosed command group may have forked a background process which should not be holding the lock) らしいのですが、こちらもどういう時に便利なのかよくわかっていません。

さいごに

以上、シェルスクリプトで安全で簡単なロックの方法として flock コマンドの解説でした。既存の記事で flock コマンドを扱っているものはいくつもあるのですが、私がシェルスクリプトに詳しくない時に読んでも、すっきりしないままでした。使い方自体は簡単なのですが、書き方に幅があってしかもファイルディスクリプタというシェルスクリプト特有の慣れない技術を使うため、意外と知識が必要だったからなのでしょう。

新しい知識を学ぶよりもすでに知っていて慣れているディレクトリやシンボリックリンクを使う手法の方が理解するのは容易です。しかし手法を理解したからといって、それを実装するのは大変です。排他ロックだけではなく共有ロックにまで対応しようとするとなおさらです。ここからの教訓はすでに持っている知識だけで頑張るよりも、新しい知識を学んだ方が、最終的には近道だということです。

この記事によってシェルスクリプトによるロック手法のベストプラクティスがまとめられたと信じています。

本当に必要だったものは flock ではなくデータベースかもしれない

最後の最後に一つだけ。この記事では flock を使った排他制御について解説しました。排他制御が必要になる大きな理由は、複数のプログラムからデータを共有するからです。排他制御というのは共有データを守るためにあります。

何が言いたいかと言うと、flock コマンドを使ってデータを守る必要があるのであれば、データ管理に排他制御の機能が含まれているデータベースを使うという方法もあるということです。例えば SQLite はシェルスクリプトからも使用することが出来る排他制御の機能(トランザクション機能)が含まれているデータ管理ソフトです。目的はデータを守ることです。ロック処理は目的を達成するために必要な手段ですが目的そのものではありません。

排他制御が必要になるならば、このようなソフトウェアを使うという候補も考慮しなければいけません。自分のやるべき仕事を間違えてはいけません。ロック処理をシェルスクリプトで独自実装することはあなたの仕事ではないはずです。他の人が書いたプログラムを利用する。それがガンカーズの UNIX 哲学の「ソフトウェアを梃子(てこ)として利用せよ」という言葉の本質です。

データの保存にファイルをつかう場合は flock を使って排他制御をするしかありませんが、そもそもデータベースをつかう場合は flock は不要になります。データ管理にプレーンなファイルを使うという考え方自体が、そもそも設計として間違っている可能性があります。データ管理にはデータ管理の専門家です。SQLite について興味がある方は以下の記事を参照してください。


参考 flock を扱っている他の方の記事

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
431