'00.7.26 ('02.8.17更新)
ファイル操作の排他制御といえば、以下のものが代表的です。
flockとロックファイル(ディレクトリ)式に大きく分けられます。
基本は単なる使用中の目印です。目印が無いときに入って、自分で目印を作り、操作が終わったときに消して出るだけですね。トイレみたい(^^) で、使用中なら何回かノックします。(せかすな〜)
$retry = 5; # リトライ回数セット while (-f $lockfile) { # ファイルが存在するなら待つ if (--$retry <= 0) { &error("BUSY"); } # 5回ダメならあきらめる sleep(1); # 1秒待つ } →ここに隙がある open(LOCK,"> $lockfile"); # ファイル作成 close(LOCK); # 閉じる 一連の処理 unlink($lockfile); # 削除 【while (-f $lockfile) は while (-e $lockfile)でもよい】
$retry = 5; # リトライ回数セット while (!symlink(".", $lockfile)) { # 作成。出来なければ待つ if (--$retry <= 0) { &error("BUSY"); } # 5回ダメならあきらめる sleep(1); # 1秒待つ } 一連の処理 unlink($lockfile); # 削除 【ここで作成しているのはカレントディレクトリのシンボリックリンク】 【MacPerlで使う場合は ":" とします】
$retry = 5; # リトライ回数セット
while (!mkdir($lockdir, 0755)) { # 作成。出来なければ待つ
if (--$retry <= 0) { &error("BUSY"); } # 5回ダメならあきらめる
sleep(1); # 1秒待つ
}
一連の処理
rmdir($lockdir); # 削除
$retry = 5; # リトライ回数セット
while (!rename($lockfile, $lockfile2)) { # リネーム。出来なければ待つ
if (--$retry <= 0) { &error("BUSY"); } # 5回ダメならあきらめる
sleep(1); # 1秒待つ
}
一連の処理
rename($lockfile2, $lockfile); # ファイル名を戻す
フリーのCGIプログラムなどだと、symlink式とopen式と両方スクリプトが書いてあってユーザーに選ばせるとか、symlink関数が有効かどうかテストして自動で場合分けするとか、汎用性を求めるために複雑になっていますが、基本は同じです。
上記4つの方法は、プロセスが途中で止まってロックファイル(ディレクトリ)が残ることがあるのが最大の欠点です。
古いロックの自動消去を書くこともできます。たとえば、作られたのが10分以上前なら使用中ではないと見なして消去します。ただし、ロックを消すのにも隙があるといけませんね。作成時間を確認するのとロックを消すのと2段階かかりますから、そのままだと、新しいロックを消してしまう虞れがあります。
というわけで、ここまで気を使ったスクリプトもあまりないのですが、悩んだ末に「ロック入れ替え方式」という画期的(おおげさ)な方法を考案しました。mkdir関数で書いてみます。
$retry = 5; # リトライ回数セット
while (!mkdir($lockdir, 0755)) { # 作成。出来なければ待つ
if (--$retry <= 0) { # 5回ダメなら
if (mkdir($lockdir2, 0755)) { # ロックを消すための排他
if ((-M $lockdir) * 86400 > 600) { # 作成時間が10分以上前なら
# ロック入れ替え
rename($lockdir2, $lockdir) or &error("LOCK ERROR");
last; # 一連の処理へ
}
else { rmdir($lockdir2); } # 部分ロック削除
}
&error("BUSY"); # あきらめる
}
sleep(1); # 1秒待つ
}
一連の処理
rmdir($lockdir); # 削除
ロックのrename失敗時には、安全のため、エラーとしてプロセス終了とします。
2つ目のロックは1つ目より残る可能性は薄いですけど、消せるといいですね。
サーバから飛んでくるシグナルでプロセスが途中で止まる事があります。たとえば、あらかじめタイムアウト時間(5分とか10分とか)が決められているサーバだと、混んでいて実行に時間がかかるとシグナルを飛ばしてプロセスを止めようとします。そのまま止まるとロックが残ってしまうので、シグナルが来たときに「ちょっと待ったぁ〜」って感じにサブルーチン(シグナルハンドラ)を実行してロックを消すようにします。symlink関数で書いてみます。
# シグナルハンドラをセット $SIG{HUP} = $SIG{INT} = $SIG{PIPE} = $SIG{QUIT} = $SIG{TERM} = \&unlock; sub unlock { # プロセス番号が一致して、自分が作ったロックならば消去 if ($$ == readlink($lockfile)) { (必要ならtmpファイル削除等も) unlink($lockfile); } exit; } $retry = 5; # リトライ回数セット while (!symlink($$, $lockfile)) { # 作成。出来なければ待つ if (--$retry <= 0) { &error("BUSY"); } # 5回ダメならあきらめる sleep(1); # 1秒待つ } 一連の処理 unlink($lockfile); # 削除
【MacPerlで symlink($$, $lockfile) はエラーになります。存在しないファイルのエイリアスは作れないようです】
ロックを作ったプロセスを特定しない場合は、他のプロセスが作ったロックを消さないために、ロックファイル作成後にシグナルハンドラをセットするようにします。その場合、ロック作成からセットまでの間に来たシグナルは防げません(確率低いですが)。ロック作成時刻を判断するなどして後で消すしかないでしょう。
flockは排他制御専用関数ですから、使えるならそれが一番簡単なわけです。他のプロセスを自動的に待たせることが出来ますし、無用のロックが残ることもありません。flockは他のflockを使うプロセスに使用中であると知らせるだけで、flockを使わないプロセスをロックすることは出来ません。(NTの場合はロックしちゃいます)
間違った書き方をしない限り確実にロックされるはずですが、大手プロバイダではflockが使えないところが多いようです。リムネットは使えないと明記してありますし、@niftyでも場合によってflockが利かなくなることを確認しています (サーバがZeusになってからは未確認)。
使うのはほとんど1と2だけですね。
読込モードで開くときに 2 にしてもロックがかからなかったりします(Solaris)。より強力なロック(排他的ロック)ですから使っても大丈夫に思いがちですが。
open(IN, "< $datafile"); # 読込モードで開く flock(IN, 2); # ロックがかからない
読込モードで開くときには 1 か 5 を使います。
open(IN, "< $datafile"); # 読込モードで開く flock(IN, 1); # ロック成功
1,5 (共有ロック)を使えない環境があったりして確認は必要です。
追加モードも危なそうです。
open(OUT, ">> $datafile"); # 追加モードで開く
→他のプロセスが書き込んで末尾がずれる。
flock(OUT, 2);
なので、以下のようにファイルポインタのセットし直しが必須とされていますが、(「プログラミングPerl」とかで)
open(OUT, ">> $datafile"); # 追加モードで開く flock(OUT, 2); # ロック確認。ロック seek(OUT, 0, 2); # ファイルポインタを末尾にセット print OUT "$data\n"; # 書き込む close(OUT); # closeすれば自動でロック解除
実験してみると seek(OUT, 0, 2) が無くてもずれなかったりします。最近のUNIXでは、write(2) のたびに、lseek(2) で、末尾に移動して書き込むらしいですね。(ariesさんからの情報)
上書きモードで開くとおかしくなることがあります。
open(OUT, "> $datafile"); # 上書きモードで内容消去
→他のプロセスが書き込む。あるいは読み込む。
flock(OUT, 2);
ロックしてないのに内容消去ってのがいけませんね。読むプロセスがないとしても、消したつもりなのに内容が書かれているのもいけません。じゃあ上書きしたいときはどうするかというと、
open(OUT, "+< $datafile"); # 読み書きモードで開く flock(OUT, 2); # ロック確認。ロック truncate(OUT, 0); # ファイルサイズを0バイトにする seek(OUT, 0, 0); # ファイルポインタを先頭にセット print OUT "$data\n"; # 書き込む close(OUT); # closeすれば自動でロック解除
上記と同じ事をしますが、下記の方が速いそうです。ファイルサイズが大きいほど差が出ます。プロセスが途中で止まるかもしれないことを考えると、書込む前にファイルサイズを0にしないので、安全度も少し高いですね。
open(OUT, "+< $datafile"); # 読み書きモードで開く flock(OUT, 2); # ロック確認。ロック seek(OUT, 0, 0); # ファイルポインタを先頭にセット print OUT "$data\n"; # 書き込む truncate(OUT, tell(OUT)); # ファイルサイズを書き込んだサイズにする close(OUT); # closeすれば自動でロック解除
アクセスカウンター等でサイズが増えても減ることはない場合には、truncateは必要ないです。
システムコールを行うとき(スクリプトの外に働きかけるとき)には、それが失敗する可能性も考えておきます。失敗したまま次に進むとデータが壊れますね。というわけで、エラー処理を入れた実用flock。dieではなく&errorで処理をしてもいいです。
open(OUT, "+< $datafile") or die "Can't open : $!"; flock(OUT, 2) or die "Can't flock : $!"; seek(OUT, 0, 0) or die "Can't seek : $!"; print OUT "$data\n" or die "Can't print : $!"; truncate(OUT, tell(OUT)) or die "Can't truncate: $!"; close(OUT);
書き込みが数回に渡る時は、プロセスが途中で止まるとデータが壊れるので、tempファイルに書き込んで書き込み成功確認後にrenameする必要があります。その場合下記のまとめてロックを使います。(確認はファイルサイズや最後に書き込んだ行の内容等で行います)
操作するファイル数が多かったりして、一連のスクリプトを纏めてロックしたいときには、ロック専用の空のファイルを使います。tempファイル方式にも有効です。
ロックファイル式に似てますね。
# ロックファイルは内容がどうなっても関係ないから上書きモードでいい。
open(LOCK,"> $lockfile") or die "Can't open lockfile: $!";
flock(LOCK, 2) or die "Can't flock : $!";
一連の処理
close(LOCK); # ロックの終了
flock(XX, 2)は、排他したプロセスが何らかの理由で終わらないと、次のプロセスが延々待たされることになります。なので制限時間をセットすることもあります。可能性は低いでしょうから、実用性はあまりないかもしれません。使うとしたら、先行プロセスの待ち時間設定の部分でしょうか。
eval { local $SIG{ALRM} = sub { die "time out" }; # 時間が来たら抜け出す open(OUT, "+< $datafile") or die; alarm(5); # 先行プロセスを待つ時間(5秒) flock(OUT, 2) or die; # ロック確認。ロック alarm(300); # 自分自身の制限時間(5分) 一連の処理 close(OUT); # closeすれば自動でロック解除 alarm(0); # 無事済んだのでリセット }; if ($@ =~ /time out/) { タイムアウト時の処理 } elsif ($@) { die } # タイムアウト以外の理由なら
以上です。