より多くの環境で動作させるために
CGIスクリプトを組む時などに問題になる事の一つにファイルのロックがあります。
排他制御をするための代表的なものとして、以下の三つの手法が挙げられます。
シンボリックリンクについても利用できない環境があるため、より多くの環境で使えるようにするためにはディレクトリを利用します。
排他制御をするための代表的なものとして、以下の三つの手法が挙げられます。
- flock()
- ディレクトリを利用するもの
- シンボリックリンク(ショートカットファイル)を利用するもの
シンボリックリンクについても利用できない環境があるため、より多くの環境で使えるようにするためにはディレクトリを利用します。
ディレクトリを使ったロックの問題点
Perlでディレクトリを使ったファイルロックを実現する簡単なコードは以下のようになります。
ディレクトリを利用した場合に問題になるのが、「スクリプトが異常終了した場合にディレクトリが残ってしまう」事です。
そこで多くのスクリプトでは、「作成されてから○分以上経ったディレクトリは異常終了で残ってしまったものと見なし削除する」という処理を加えています。
しかしこの処理を加える事で新たな問題が発生します。
ディレクトリが古いと判断するタイミングが複数のプロセスで重なってしまうと、片方のプロセスが削除・作成した後にもう片方のプロセスが再度削除・作成を行う可能性が出てきてしまいます。
#ロック関数
sub Lock
{
#20回試行する
for(1 .. 20)
{
if(mkdir('lock', 0755))
{
#ロック成功
return 1;
}
sleep(1);
}
#ロック失敗
return 0;
}
#アンロック関数
sub UnLock
{
rmdir('lock');
}
ディレクトリを利用した場合に問題になるのが、「スクリプトが異常終了した場合にディレクトリが残ってしまう」事です。
そこで多くのスクリプトでは、「作成されてから○分以上経ったディレクトリは異常終了で残ってしまったものと見なし削除する」という処理を加えています。
#ロック関数
sub Lock
{
#ディレクトリが作成されて5分以上経っていれば削除
if(-d 'lock' && (stat('lock'))[9] < time() - 300)
{
rmdir('lock');
}
#以下同様
しかしこの処理を加える事で新たな問題が発生します。
ディレクトリが古いと判断するタイミングが複数のプロセスで重なってしまうと、片方のプロセスが削除・作成した後にもう片方のプロセスが再度削除・作成を行う可能性が出てきてしまいます。
0th server で利用しているモジュール
0th server で利用しているファイルロックでは、ディレクトリ名に時間を用いる事で問題を上手く回避しています。
このモジュールを使用すると、「lock」ディレクトリの中にキー文字列の名前のディレクトリ、その中にUnix time文字列の名前のディレクトリが作成されます。
このモジュールの欠点を挙げるとすれば、ファイルをロックするためだけのものとしてはボリュームが大きいという事でしょう。
このモジュールを使用すると、「lock」ディレクトリの中にキー文字列の名前のディレクトリ、その中にUnix time文字列の名前のディレクトリが作成されます。
このモジュールの欠点を挙げるとすれば、ファイルをロックするためだけのものとしてはボリュームが大きいという事でしょう。
Source(Perl)
#ロックに使うディレクトリ
my $LOCK_DIR = 'lock/';
#ロックしたkeyと時間を保存するハッシュ
my %LOCK_TIME;
#ロックのトライ回数
my $TRY_TIME = 20;
#ロックを異常終了で残ったと判断する時間(秒)
my $ERR_TIME = 300;
#############################################################################
# 初期処理
#
mkdir($LOCK_DIR, 0755);
#############################################################################
# ディレクトリのリストを取得(ローカル関数)
# return
# 取得したリスト
# parameter
# 1:Path in 取得するパス
#
my $GetDirList = sub( $; )
{
my ($Path) = @_;
my @DirList;
opendir(DIR, $Path);
for(readdir(DIR))
{
if($_ ne '.' && $_ ne '..')
{
push(@DirList, $_);
}
}
closedir(DIR);
return @DirList;
};
#############################################################################
# ロック
# return
# 成功:非0/失敗:0
# parameter
# 1:BaseName in ロックに使うkey文字列
#
sub Lock_Lock( $; )
{
my ($BaseName) = @_;
my $LockPath = $LOCK_DIR . $BaseName . '/';
my $Try = $TRY_TIME;
my $Time = time;
#BaseNameの正当性チェック
if($BaseName =~ m{[/\\:*?"<>|]})
{
return 0;
}
#ロックに使うディレクトリ作成
mkdir($LockPath, 0755);
#順番の確保
while(!mkdir($LockPath . $Time, 0755))
{
$Try--;
if($Try == 0)
{
return 0;
}
select(undef, undef, undef, 0.2);
$Time = time;
mkdir($LockPath, 0755);
}
#異常終了で残ったロックの削除
for(sort(&$GetDirList($LockPath)))
{
if($_ < $Time - $ERR_TIME)
{
rmdir($LockPath . $_);
}
else
{
last;
}
}
#順番待ち
while((sort(&$GetDirList($LockPath)))[0] != $Time)
{
$Try--;
if($Try == 0)
{
rmdir($LockPath . $Time);
return 0;
}
select(undef, undef, undef, 0.2);
}
$LOCK_TIME{$BaseName} = $Time;
return 1;
}
#############################################################################
# アンロック
# return
# 成功:非0/失敗:0
# parameter
# 1:BaseName in ロックに使ったkey文字列
#
sub Lock_UnLock( $; )
{
my ($BaseName) = @_;
if(!$LOCK_TIME{$BaseName})
{
return 0;
}
if(!rmdir($LOCK_DIR . $BaseName . '/' . $LOCK_TIME{$BaseName}))
{
if(! -d $LOCK_DIR . $BaseName . '/' . $LOCK_TIME{$BaseName})
{
delete($LOCK_TIME{$BaseName});
}
return 0;
}
delete($LOCK_TIME{$BaseName});
rmdir($LOCK_DIR . $BaseName);
return 1;
}
#############################################################################
# ロックのトライ回数の取得
# return
# トライ回数
#
sub Lock_GetTryTime()
{
return $TRY_TIME;
}
#############################################################################
# ロックのトライ回数の設定
# parameter
# 1:TryTime in トライ回数
#
sub Lock_SetTryTime( $; )
{
my ($TryTime) = @_;
$TRY_TIME = $TryTime;
return;
}
#############################################################################
# ロックを異常終了で残ったと判断する時間の取得
# return
# ロックを異常終了で残ったと判断する時間(秒)
#
sub Lock_GetErrorTime()
{
return $ERR_TIME;
}
#############################################################################
# ロックを異常終了で残ったと判断する時間の設定
# parameter
# 1:ErrorTime in ロックを異常終了で残ったと判断する時間(秒)
#
sub Lock_SetErrorTime( $; )
{
my ($ErrorTime) = @_;
$ERR_TIME = $ErrorTime;
return;
}
1;
ロックをしてもファイルは壊れる
ファイルロックを行っていても、ファイルに書き込みを行っている最中に異常終了してしまうとファイル内容は壊れてしまいます。
そこで 0th server では以下のような関数でファイルが壊れる確率を下げています。
そこで 0th server では以下のような関数でファイルが壊れる確率を下げています。
#############################################################################
# ファイル書き出し
# return
# 成功:非0/失敗:0
# parameter
# 1:FileName in ファイル名
# 2:TempName in 一時ファイル名
# 3:Source in 書き出す内容の配列
#
sub FileIO_Write( $$@; )
{
my ($FileName, $TempName, @Source) = @_;
if(open(my $Fh, '>' . $TempName))
{
print $Fh @Source;
close($Fh);
return rename($TempName, $FileName);
}
else
{
return 0;
}
}