おっさんエンジニアの滝澤です。「わしゃあ、まだまだ若いもんには負けん」と老害を振りまくよりかは「俺の屍を超えていけ」と言うようになりたい今日この頃です。

アトミック(atomic)なファイル操作について紹介しているシリーズの最終回です。この内容は弊社の社内勉強会で話した内容をまとめ直したものです。

今回は「みなさん、安全にロックができていますか?」ということについて、考えてみましょう。

そのファイル、安全にロックできていますか?

複数のプロセスから同時にファイルに対して書き込みを行うとファイルのデータが壊れるときがあります。これを防ぐために、ファイルへの書き込み処理を行うプログラムのプロセスがロックをかけ、他のプロセスがそのファイルに対して書き込み処理を行わないようにします。

ロックをかける方法にはいくつかあり、flock()のようなシステムコールでロックをかける方法、ロックファイルを利用してロックをかける方法がよく使われています。本記事ではシェルスクリプトからロックをかける方法としてロックファイルを利用する方法について紹介します。

基本的なファイル操作コマンドだけでどうやってロックをかけるかを考えてみましょう。

ロックファイルを作成する方法を検討してみる

簡単に思いつくのはロックファイルを普通のファイルとして直接作成することです。シェルスクリプトの処理としては、ロックファイルが存在するかを確認し、存在していなければロックファイルを作成し、存在していればロックがかけられていると判断して処理を中止します。この方法で安全にロックをかけることができるでしょうか。

結論を先に述べると、この方法では安全にロックを行うことができません。確認してみましょう。

テスト用のシェルスクリプトとして次の内容のものを用意します。1秒ごとに0から9までの数字を出力して最後にOKを出力します。

#!/bin/bash
LOCK_FILE=$HOME/tmp/file.lock

## ロックファイルの確認
if [ -f $LOCK_FILE ]; then
    echo "LOCKED"
    exit 1
fi

## ロックファイルの作成
touch $LOCK_FILE

## メインの処理
for ((i=0;$i<10;i=$i+1)); do
    echo $i
    sleep 1
done
echo OK

## ロックファイルの削除
rm -f $LOCK_FILE

exit 0

実行例は次の通りです。

$ ./lock.sh 
0
1
2
3
4
5
6
7
8
9
OK

上記の実行中に別端末でこのシェルスクリプトを実行すると、ロックファイルが存在するためスクリプトが中断します。

$ ./lock.sh 
LOCKED

一見うまくロックができているように見えますね。実は安全にロックできているわけではありません。「ロックファイルの確認」と「ロックファイルの作成」は同時に行われているわけではありません。そのため、この処理間に別にシェルスクリプトが実行されたら、ロックファイルの作成前であるため、中断せずに実行されてしまいます。

このことを手動で動作確認するため、「ロックファイルの確認」と「ロックファイルの作成」の間にあえて次のように処理遅延時間として10秒のsleepを入れてみました。

#!/bin/bash
LOCK_FILE=$HOME/tmp/file.lock

## ロックファイルの確認
if [ -f $LOCK_FILE ]; then
    echo "LOCKED"
    exit 1
fi

## 処理遅延
sleep 10

## ロックファイルの作成
touch $LOCK_FILE

## メインの処理
for ((i=0;$i<10;i=$i+1)); do
    echo $i
    sleep 1
done
echo OK

## ロックファイルの削除
rm -f $LOCK_FILE

exit 0

このスクリプトを2〜3秒遅れくらいで複数の端末から実行してみてください。後から実行したスクリプトがそのまま実行を続けてしまうことがわかります。つまり安全にロックができていません。もしこれが書き込み処理であればファイルのデータが破壊されていることでしょう。

普通のファイルとしてロックファイルを作成する方法は「ロックファイルの確認」と「ロックファイルの作成」が別々に行われているため、ロックファイルの操作がアトミックに行われていないということになります。

アトミックにロックファイルを操作するには

ロックファイルの操作においてアトミックにするためには、「ロックファイルの確認」と「ロックファイルの作成」を同時に行う必要があります。つまり、「ロックファイルの作成」を試みて、成功してロックファイルが作成できたら、ロックされていなかったと判断して、そのまま処理を続けます。ロックファイルの作成に失敗したら、ロックされていると判断して、処理を中断します。

シンボリックリンクでロックファイルを作成する方法

ロックファイルを作成する方法としてはシンボリックリンクとして作成する方法がよく使われます。

シンボリックリンクを作成するときに、作成先にすでにファイルが存在するとエラーとなります。そのため、ロックファイルをシンボリックリンクとして作成を試みれば、ロックファイルが存在すればエラーが発生するため、ロックファイルが存在すると判断し処理を中断できます。ロックファイルが存在しなければ、ロックファイルがシンボリックリンクとして作成されます。「ロックファイルの確認」と「ロックファイルの作成」が同時に行われるため、安全にロックすることができるのです。普通のファイルとしてロックファイルを作成する方法での問題はシンボリックリンクの場合は起きません。

テスト用のシェルスクリプトとして次の内容のものを用意します。

#!/bin/bash
LOCK_FILE=$HOME/tmp/file.lock

## ロックファイルの確認と作成
if ! ln -s $$ $LOCK_FILE; then
    echo "LOCKED"
    exit 0
fi

## メインの処理
for ((i=0;$i<10;i=$i+1)); do
    echo $i
    sleep 1
done
echo OK

## ロックファイルの削除
rm -f $LOCK_FILE

exit 0

このスクリプトを実行すると次のようになります。

$ ./lock.sh 
0
1
2
3
4
5
6
7
8
9
OK

上記の実行中に別端末でこのシェルスクリプトを実行すると、ロックファイルが存在するためスクリプトが中断します。

$ ./lock.sh 
ln: /home/foo/tmp/file.lock: File exists
LOCKED
$ ls -l /home/foo/tmp
lrwxrwxrwx  1 foo foo 4 Oct  5 16:36 file.lock -> 966

シンボリックリンクのリンク元は実在のファイルである必要はなく、文字列であれば通るのでここではスクリプトの実行中のプロセスIDとしています。

このようにロックファイルをシンボリックリンクで作成するとアトミックにロックすることができるのです。

ハードリンクでロックファイルを作成する方法

ロックファイルを作成する方法としてはハードリンクとして作成する方法も使われます。

ハードリンクを作成するときに、作成先にすでにファイルが存在するとエラーとなります。そのため、ロックファイルをハードリンクとして作成を試みれば、ロックファイルが存在すればエラーが発生するため、ロックファイルが存在すると判断し処理を中断できます。ロックファイルが存在しなければ、ロックファイルがハードリンクとして作成されます。「ロックファイルの確認」と「ロックファイルの作成」が同時に行われるため、安全にロックすることができるのです。

シンボリックリンクの場合とは異なり、ハードリンクではリンク元のファイルを必要とします。そのため、操作対象のファイルをリンク元のファイルにしたり、プロセスの情報(プロセスID)を含めたファイルを作成して、そのファイルをリンク元のファイルとしたりします。

テスト用のシェルスクリプトとして次の内容のものを用意します。

#!/bin/bash
DATA_FILE=$HOME/data/file.dat
LOCK_FILE=${DATA_FILE}.lock

## ロックファイルの確認と作成
if ! ln $DATA_FILE $LOCK_FILE; then
    echo "LOCKED"
    exit 0
fi

## メインの処理
for ((i=0;$i<10;i=$i+1)); do
    echo $i
    sleep 1
done
echo OK

## ロックファイルの削除
rm -f $LOCK_FILE

exit 0

このスクリプトを実行すると次のようになります。

$ ./lock.sh 
0
1
2
3
4
5
6
7
8
9
OK

上記の実行中に別端末でこのシェルスクリプトを実行すると、ロックファイルが存在するためスクリプトが中断します。

$ ./lock.sh 
ln: /home/foo/data/file.dat.lock: File exists
LOCKED
$ ls -l /home/foo/data
-rw-r--r--  2 foo foo  5963 Oct  8 15:00 file.dat
-rw-r--r--  2 foo foo  5963 Oct  8 15:00 file.dat.lock

このようにロックファイルをハードリンクで作成してもアトミックにロックすることができます。

最後に

3回に分けてアトミックなファイル操作について紹介しました。今回紹介した問題はすべて、私がユーザー企業の兼任システム管理者としてサーバの管理を始めたナウなヤングの若者だった頃(15年くらい前か。何もかも懐かしい)に経験していて、色々調べてアトミックにファイル操作しないといけないんだということを学びました。失敗はしないに越したことはないのですが、失敗から学ぶといやな記憶と共に忘れられないため、いつまでも覚えていますね。

株式会社ハートビーツのインフラエンジニアから、ちょっとした情報をお届けします。