TOP > スクリプト制作メモ > ファイルロック(排他処理)について

*以下の内容は Perlメモ とほぼ同じですが、ダイが苦心して噛み砕いたのでちょっとわかりやすいかもです。

 CGIでファイルを読み書きする場合、ファイルロック(排他処理)が必要になります。なぜかと言えば、CGIは一定の実行条件がある場合を除き、不特定多数の人が起動し実行できるからです。そのような場合、同時に同じファイルにアクセスしてしまうと、正しくデータが書き込めなかったり、ファイル自体を破壊してしまったりということが起こります。

・カウンターCGIでファイルロックが無い例
プロセスA
プロセスB
open  
read 999 open
処理 999→1000 read 999
write 1000 処理 999→1000
close write 1000
  close

 本来であれば、プロセスBはプロセスAよりも後の起動なので1001をカウントするべきところにもかかわらず、1000をカウントしています。これでは正確なカウントができていません。また、open や write の処理が重なるとデータ構造が崩れたり、ファイル自体の破壊という心せつないことにもなりかねません。



 ファイルロック(排他処理)を実現するには、ファイルを読み書きする際にフラグを立てるような感覚で、読み書き処理の実行を他のプロセスに知らせなければなりません。各プロセスはフラグを見て処理を待つことになります。

・カウンターCGIでファイルロックが有る例
プロセスA
プロセスB
ファイルロック  
open  
read 999 open不可
処理 999→1000 ↓待機
write 1000 ↓待機
close ↓待機
ファイルロック解除 ↓待機
  ファイルロック
  open
  read 1000
  処理 1000→1001
  write 1001
  close
  ファイルロック解除



 ファイルロック(排他処理)にはいくつかの方法があります。
  • ファイルロック関数を使用する方法(flock式)
  • シンボリックリンクを使用する方法(symlink式)
  • ディレクトリを使用する方法(mkdir式)
  • ファイルの存在やファイル名を使用する方法(rename式)


 しかし、残念なことにファイルロック(排他処理)は、あるOSでは使えるのにあるOSでは使えなかったり、同じOSでも設定によって使えなかったりします。上の例では flock式と symlink式がそれに当てはまります。特にflock式が使えるサーバは少なくあまり使われていません。

 では多くのCGIで行われているファイルロック(排他処理)を見てみましょう。

	  
      sub lock {
	  
        local($retry,$mtime);
# 1分以上古いロックは削除する if (-e $lockfile) { ($mtime) = (stat($lockfile))[9]; if ($mtime < time - 60) { &unlock; } } # symlink関数式ロック if ($lockkey == 1) { $retry = 5; while (!symlink(".", $lockfile)) { if (--$retry <= 0) { &error('LOCK is BUSY'); } sleep(1); } # mkdir関数式ロック } elsif ($lockkey == 2) { $retry = 5; while (!mkdir($lockfile, 0755)) { if (--$retry <= 0) { &error('LOCK is BUSY'); } sleep(1); } } $lockflag=1; }


 symlink式 と mkdir式 を $lockkey という変数によって選択できるようにしてOSの違いによる不具合を回避しています。中には自動的にロック方式を判定するようなものもあります。しかし、このようなファイルロック(排他処理)ルーチンには致命的な欠陥があります。

 どうゆうことかと言うと、
  • 一定時間経過したロックを無条件に解除するのはまずい


 ということになります。

 symlink式 や mkdir式、rename式 の関数は、ロックできるかどうかのテストと実際にロックする操作を同時に行なうことができる関数です。
 なのに、ロックを解除するかどうかの判断と実際にロックを解除する操作は同時に行なえません。そこに重大な落とし穴があるのです。

 例えば、「 複数のプロセスが同時に一定時間経過(異常と判断)したロックを解除する 」 という事態が発生した場合に不具合が生じます。

・複数プロセスの同時異常判断不具合の例
プロセスA
プロセスB
プロセスC
異常と判断 異常と判断
ロック解除
ファイルロック
ロック解除



 上表のように、プロセスA と プロセスB が同時に一定時間経過(異常と判断)してともにロックを解除しようとするのですが、前述のとおりロックを解除するかどうかの判断とロックの解除処理を同時に行えないため、両方がロック解除する間にもうひとつの処理(プロセスC)がロックしてしまうという事態が発生する可能性があります。しかし、その(プロセスCによる)ロックは以前のロック状態から判断された処理(プロセスB)によってロック解除されてしまいます。

 では、このような不具合が起きないようにはどうすればいいかというと、ロック状態をユニークなキーで識別してプロセスが正しくロック解除するような仕組みにします。具体的には、ファイルロック(排他処理)用にCGIから書き込み可能なディレクトリとファイルを用意します。ロック状態はそのファイル名をユニークな識別子つきの名前に変更することでフラグの代わりとします。

 と、いうわけで具体的なスクリプトに落とし込むと以下の様になります。

sub my_flock {
  my %lfh = (dir => './lockdir/', basename => 'lockfile',
	     timeout => 60, trytime => 10, @_);

  $lfh{path} = $lfh{dir} . $lfh{basename};

  for (my $i = 0; $i < $lfh{trytime}; $i++, sleep 1) {
    return \%lfh if (rename($lfh{path}, $lfh{current} = $lfh{path} . time));
  }
  opendir(LOCKDIR, $lfh{dir});
  my @filelist = readdir(LOCKDIR);
  closedir(LOCKDIR);
  foreach (@filelist) {
    if (/^$lfh{basename}(\d+)/) {
      return \%lfh if (time - $1 > $lfh{timeout} and
	  rename($lfh{dir} . $_, $lfh{current} = $lfh{path} . time));
      last;
    }
  }
  undef;
}

sub my_funlock {
  rename($_[0]->{current}, $_[0]->{path});
}

例えば、
・ロックする時(タイムアウトするとdie)は
$lfh = my_flock() or die 'Busy!';

・ロックする時(タイムアウト無し)は
1 while (not defined($lfh = my_flock()));

・ロックを解除する時は
my_funlock($lfh);

という風に呼び出します。

処理を大まかに解説すると、
 ・ファイルロック(排他処理)用ディレクトリ /filelock/
 ・ファイルロック(排他処理)用ファイル /filelock/abc
だとして

 ロック時には /filelock/abc に time関数の値を付け /filelock/abc1287654321 とリネームします。ロックを解除する時には、もとの /filelock/abc という名前にリネームして戻します。

他のプロセスが、ロック状態を判断する時は/filelock/abc のうしろに数字の文字列がついているかを調べます。

 すると、同時に異常なロック状態を判断して解除しようとしても、別プロセスがロックした状態を解除してしまうことはなくなります。

 なぜなら、上表でプロセスCがロックして付加した数字文字列は、プロセスBが解除しようとする数字文字列とは違うものになっているからです。

 ファイル名のうしろに付加される数字は、ロック状態を示すフラグの役割とともに、ロック状態とプロセスをユニークに結びつける役割も担っているのです

 UNIX系サーバで flock が使えるなら flockを使えば済む事ですが、残念ながら多くのレンタルスペースでは flock が使えません。広く普及しているCGIの多くはmkdir式やsymlink式のロックを利用していることが多くありますが、それらのロックがそれほど強固ではないことをなんとなく経験的に気付いていたので、この度いろいろと調べてみました結果がこのページです。

 では、お約束のオチとして、ファイルロック(排他処理)の安全手順を書いてサヨウナラ。

 1.ロックする
 2.読み込む
 3.一時データファイルに書き込む
 4.一時データファイルを正規ファイル名にリネームする
 5.ロック解除する


TOP > スクリプト制作メモ > ファイルロック(排他処理)について
(C) bayashi.net