排他処理

前へ << 負荷について考える 連続アクセス防止 >> 次へ

排他処理の必要性

掲示板やアクセスカウンタのように、ファイルにデータを保存しておき、 次回それを参照するような CGI プログラムは、排他処理を行う必要があります。 なぜなら、たまたま複数の閲覧者がページを見ていて、CGI プログラムが ほぼ同時に実行された場合、複数のプログラムが同時にファイルに書き込もうとします。 すると、ファイルの内容が壊れてしまうのです。

つまり複数のプロセスが同じことを同時に行わないように、「排他処理」が必要なのです。

掲示板を例に、どのような排他処理を行えばよいのか考えましょう。まず思い付くのは、

ここでいう「ロックファイル」とは、「排他状況を確認するためのファイル」であって、 特別なファイルではありません。Perl でコーディングすると、
while (1){                 # 無限ループ
    if ( ! -f "lockfile"){
        open(LOCK,">lockfile");  # ロックファイルを作成
        close(LOCK);
        last;                       # ループを抜ける
    }
    sleep(1);                       # 既にロックファイルがあったので、1秒待つ
}
…メッセージの書き込み…
unlink("lockfile");                 # ロックファイルを削除
となります。考え方としてはまぁ正しいのですが、 これではうまくいきません。

ほぼ同時に実行されたプロセス1とプロセス2を考えます。 UNIX では複数のプロセスを細切れに実行することで、 見かけ上同時に複数のプログラムが実行されているように見えます。

プロセス1が少し先に実行されたとして、

if ( ! -f "lockfile"){
でロックファイルが存在しないことを判断します。 ここですぐに open(LOCK,">lockfile") に処理がいけばよいのですが、 プロセスの切り替えが起こり、OS がプロセス2を実行したとしましょう。 こちら
if ( ! -f "lockfile"){
でロックファイルが存在しないことを判断します。 再びプロセスが切り替わり、プロセス1の続きが実行されます。 ここでやっと
open(LOCK,">lockfile");
が実行されます。ここで、またまたプロセス2の続きが実行されると、 こちらも
open(LOCK,">lockfile");
が実行されます。結果として、排他処理は全く働いていないことになります。

なぜうまくいかないかと言うと、 ロックファイルが存在するかどうかの判別 (-f) と、 ロックファイルの作成 (open) が別々だからです。 排他処理を行うときは、ロックファイルの作成とロックファイルが 存在するかどうかの判別を、同時に行わなくてはいけません。 つまり open だけで排他処理を行うのは無理なのです。

普通、排他処理にはシンボリックリンク・mkdir・flock などを使います。

シンボリックリンク

シンボリックリンクは、UNIX でのファイルの別名を扱うための仕組みですが、 深くは解説しません。ここで求めているのは、シンボリックリンクの 性質ではなく、シンボリックリンクを利用した排他処理だからです。

Perl では

symlink('dummy','fuga');
とすると、カレントディレクトリにシンボリックリンク fuga が作成されます。 dummy の部分は何でもいいです。もしシンボリックリンク fuga の作成に成功すると、symlink は 1 を返します。 もし既にシンボリックリンク fuga が作成されていた場合は、 シンボリックリンクの作成に失敗したということなので symlink は 0 を返します。

一般的には、

while (1){
    $lock_flg = symlink('dummy','fuga');
    if ( $lock_flg == 1 ){
        last;
    }
    sleep 1;
}
…ここでファイルの読み書きなどを行う…
unlink('fuga');   # シンボリックリンクを消す
という風に使います。同時に2つの同じシンボリックリンクを 作成しようとすると、少しでも早かったプロセス1では symlink が成功し while ループを抜けますが、もう片方のプロセス2はエラーになり、0 を返します。 プロセス1がファイル読み書きなどの処理を行う間、 プロセス2はひたすら symlink・sleep を繰り返し、待ち続けます。 プロセス1は最終的にシンボリックリンクを unlink で消去します。 するとプロセス2 の symlink が成功しますので、 先に進めるわけです。

Windows はよく知りませんが、Windows の Perl では symlink が使えないそうです。そういう場合は同等品の mkdir を使うとよいでしょう。

mkdir

mkdir も symlink と同様に

flock

flock は、symlink・mkdir とは違って、プロセスが 終了すると何の痕跡も残りません。symlink・mkdir の場合は 仮に Perl が core dump (異常終了) してしまうと そのプロセスが作成したシンボリックリンクやディレクトリが 残ったままになってしまいます。

なお、flock は NFS 環境で使用できない場合があるそうです。 ただし、当ページ管理人が使った Solaris の NFS 環境では、 NFS サーバ・NFS クライアントともうまく動作しました。 また、昔の HP-UX の NFS 環境では flock を使うと プロセスがハングしてしまうことがあったそうですが、 現在では大丈夫でしょう。そもそも最近の UNIX で、 flock が使えない NFS 環境があるかどうかは確認していません。

bind

ポートを bind できるのは 1つのプロセスだけなので、 bind でも排他処理はできます。ただし、いろいろと 問題もあるので普通は使いません。暇があればそのうち解説します。

シグナル処理

UNIX にはシグナルという仕組みがあります。 例えば、
% cat
(Ctrl-C)
などと、コマンドの実行中に Ctrl-C を押すことで コマンドを終了させることができます。これは、 シェルが cat に対して SIGINT というシグナルを送っているからです。 この他にも多くのシグナルがあります。シグナルを受けたプロセスは 終了してしまいます。

問題は、CGI プログラムにもシグナルが送られてくる可能性があることです。 以下は apache 環境に限った話です。 長期間ログを取ったところ、1日数回 SIGPIPE を受け取っていました。 SIGPIPE というのは、出力先のプロセスが存在しない場合に送られてくるシグナルです。 また、パイプ宛に出力しているときにブラウザが読み込みを中断すると、 SIGTERM が送られてくることがあります。

SIGPIPE も SIGTERM も、受けたプロセスは強制的に終了させられてしまいます。 もし symlink で排他処理を行った後に、これらのシグナルを受けた場合、 シンボリックリンクが残ってしまいます。

しかし、シグナルを受けたときにプロセスが終了してしまうのは、 デフォルトの設定です。実際は各プロセスが、プロセスを受けたときに どういう挙動をするかを設定できます。ですから、SIGPIPE や SIGTERM を受けたときに、シンボリックリンクを消してから終了すればよいのです。

Perl には %SIG というハッシュがあります。例えば

$SIG{TERM} = \&func;
とすると、SIGTERM を受けたときに &func という関数が実行されます。 つまり、
$SIG{TERM} = \&func;
sub func {
   シンボリックリンクを削除;
   exit;
}
とすれば、シグナルを受けてもシンボリックリンクを削除して 終了するようになります。

こちらで確認しているのは、SIGPIPE、SIGTERM だけですが、 念のため SIGINT、SIGHUP、SIGQUIT も設定しておきましょう。 結局は

$SIG{INT} = $SIG{HUP} = $SIG{QUIT} = $SIG{TERM} =  $SIG{PIPE} = \&lock_off;
sub lock_off {
   unlink($lock_file);
   exit;
}
とすればよいことになります。この lock_off のような関数を 「シグナルを扱うもの (シグナルを扱う関数)」という意味で、シグナルハンドラと呼びます。 また、シグナルを受けたときにある挙動をさせることを 「シグナルをブロックする」と言います。

強度検査

たまに symlink を使っていたのにログファイルが壊れただの、 flock はたまに失敗するだの、はては OS が信頼できないだの、 色々とおっしゃる方々がいます。おもしろいことに、そういう 人のほとんどはシグナルに関する知識が全くなかったりします。

OS を疑うことは非常によろしいのですが、まずは 以下のようなテストプログラムでチェックすべきでしょう。

このプログラムはコマンドラインで実行し、ロック機能の強度を調べます。 同時に複数の子プロセスを作成し、それぞれの子プロセスがカウンタの値を 1つずつ増やします。同時に行ってはカウンタファイルの内容が壊れてしまうので、 symlink・mkdir・flock により排他処理を行います。 1000個の子プロセスを作成した場合、最終的にはカウンタの値も 1000 に なるはずです。もし 1000 でなかったら、その排他処理では 信頼性がないということです。

% ./check-lock.pl mode [loop] [max_child]
引数の1番目 mode には、flock か symlink か mkdir を指定します。 指定した方法で排他処理を行います。 引数の2番目 loop には、カウント処理を行う回数を指定します。 省略すると 1000 回カウントします。 引数の2番目 max_child には、同時に生成する子プロセスの数を指定します。 省略すると 5 になります。この値は OS が許す限りいくらでも 大きくできますが、共用サーバであまり大きな値を指定するのはやめましょう。 そもそも共用サーバでこのプログラムを実行すること自体、お勧めしません。
% ./check-lock.pl flock
   … flock を使って1000回カウントアップする。同時に作成する子プロセスは5個
% ./check-lock.pl mkdir
   … mkdir を使って1000回カウントアップする。同時に作成する子プロセスは5個
% ./check-lock.pl symlink 500
   … mkdir を使って500回カウントアップする。同時に作成する子プロセスは5個
% ./check-lock.pl flock 500 10
   … mkdir を使って500回カウントアップする。同時に作成する子プロセスは10個

なお、このプログラムの意味がわからないなら、改造しないで下さい。 永遠に子プロセスを産み続ける羽目になって、共用のマシンを ダウンさせても当方は責任を負いません。

check-lock.pl

    1: #!/usr/local/bin/perl
    2: 
    3: $mode = shift;
    4: 
    5: if ( $mode eq 'flock' ){
    6: } elsif ( $mode eq 'symlink' ){
    7:     $lockfile = "sym-lock";
    8:     unlink($lockfile);
    9: } elsif ( $mode eq 'mkdir' ){
   10:     $lockdir = "mkdir-lock";
   11:     rmdir($lockfile);
   12: } else {
   13:     die "You must specify mode `flock', `symlink' or `mkdir'\n";
   14: }
   15: 
   16: $counter_file = "$mode.txt";
   17: $countup_func = "countup_$mode";
   18: open(F,">$counter_file");
   19: print F "0\n";
   20: close(F);
   21: 
   22: $loop = shift || 1000;        # カウントアップする数
   23: $max_children = shift || 5;  # 同時に生成する子供の数
   24: $now_children = 0;            # 現在の子供の数
   25: 
   26: for ( $x=0 ; $x<$loop ; $x++ ){
   27:     if ( $pid = fork() ){  # 親
   28:         $now_children++;
   29:     } else {               # 子
   30:         &$countup_func($x);
   31:         exit;
   32:     }
   33:     if ( ! $pid ){              # fork できなかった
   34:         die "Cannot fork! $!";
   35:     }
   36:     if ( $now_children == $max_children ){  # 制限以上のプロセスを作らない
   37:         wait;
   38:         $now_children--;
   39:     }
   40: }
   41: 
   42: while ($now_children){  # 最後に残った子供たちを待つ
   43:     wait;
   44:     $now_children--;
   45: }
   46: 
   47: open(F,$counter_file);   # 結果表示
   48: printf "Result ($counter_file) =%s",scalar(<F>);
   49: close(F);
   50: 
   51: exit;
   52: 
   53: 
   54: #--------------------- flock を使ったカウントアップ
   55: 
   56: sub countup_flock {
   57:     ($child_number) = @_;
   58:     open(F, "+< $counter_file");
   59:     flock(F,2);
   60:     $num=<F>;
   61:     seek(F, 0, 0);
   62:     printf F "%d\n",$num+1;
   63:     close(F);
   64: 
   65:     printf "$mode: I'm %dth child. counter=%d\n",$child_number+1,$num+1;
   66: }
   67: 
   68: 
   69: #---------------------- symlink を使ったカウントアップ
   70: 
   71: sub countup_symlink {
   72:     ($child_number) = @_;
   73:     $lock_flg = 0;
   74:     while (1){  
   75:         $lock_flg = symlink("$$",$lockfile);
   76:         last if ( $lock_flg );
   77:         select(undef,undef,undef,0.1);  # 0.1 秒 sleep
   78:     }
   79:     open(F,"$counter_file");
   80:     $num=<F>;
   81:     close(F);
   82:     
   83:     open(F,">$counter_file");
   84:     printf F "%d\n",$num+1;
   85:     close(F);
   86: 
   87:     printf "$mode: I'm %dth child. counter=%d\n",$child_number+1,$num+1;
   88:     unlink($lockfile);
   89: }
   90: 
   91: 
   92: #-------------------- mkdir を使ったカウントアップ
   93: 
   94: sub countup_mkdir {
   95:     ($child_number) = @_;
   96:     $lock_flg = 0;
   97: 
   98:     while (1){  
   99:         $lock_flg = mkdir($lockdir,0777);
  100:         last if ( $lock_flg );
  101:         select(undef,undef,undef,0.1);  # 0.1 秒 sleep
  102:     }
  103:     open(F,"$counter_file");
  104:     $num=<F>;
  105:     close(F);
  106:     
  107:     open(F,">$counter_file");
  108:     printf F "%d\n",$num+1;
  109:     close(F);
  110: 
  111:     printf "$mode: I'm %dth child. counter=%d\n",$child_number+1,$num+1;
  112:     rmdir($lockdir);
  113: }
  114: 

前へ << 負荷について考える 連続アクセス防止 >> 次へ

$Id: lock.html,v 1.5 2004/06/21 12:02:08 zxr400 Exp $