連鎖ゲームのサーバ上で発生するゾンビプロセスの,掃除方法について説明する.(perl)
1.前提
連鎖ゲームのサーバ側は,以下のようになっています.
- OS - Linux.カーネル2.4
- サーバプロセスを記述する言語 - Perl 5
サーバプロセスはPerlで記述しています.Javaにしたかったのですが,あまりにもサーバ機のメモリが少ない(残り数MB)ため,JavaVMがまともに動く気がせず,あきらめました.
2.サーバー側プロセスのライフサイクル

特定のポートで待ち受ける親プロセスが1個あります.
そして,クライアントからの接続を受けると,その接続を処理する子プロセスを生成します. (fork)

ここで「子プロセスA」となっていますが,複数のクライアントから接続されると, B,C,D...とどんどん増加してゆきます.
処理が終わると,クライアントから切断されます.

この時,対応する子プロセスは終了します.
3.ゾンビプロセス(defunct)の発生
上述したプロセスの開始と終了のタイミングをまとめると,下表になります.
プロセス | 開始タイミング | 終了タイミング |
親プロセス | 本体の起動 | 本体の終了 |
子プロセス | クライアントからの接続 | クライアントからの切断 |
子プロセスはクライアントからの接続が切断されたタイミングで,終了します.
ところが実際には,psコマンド等で確認すると,終了したはずの子プロセスが<defunct>として残っています. Linux(UNIX)では,子プロセスの終了を親がwaitで受け取る必要があり,受け取られるまではその子プロセスはdefunct状態で待機する,ということになっているらしいのです.
defunctのプロセスが増え続ける状態で放置しておくと,遅かれ早かれ新プロセスを生成できない状態になり,サーバの停止に追い込まれます(ました).これを回避するには,
- 親プロセスを終了する.そうするとdefunctの子プロセスも一緒に消滅する
という方法もありますが,そうすると親プロセスを止めている間はサービスそのものが停止してしまいますので,今回は適用できませんでした.親プロセスは止めずに,defunctの子プロセスは消してゆくという対策が要求されました.
4.ゾンビプロセスの回収方法
親プロセスが,子プロセスに対してwaitを実行すれば良いのですが,実装するのはやや面倒でした.ポイントは,以下の2点でした.
- 親プロセスは,生成した全ての子プロセスのPID(プロセスID)を保持しておく必要がある
- 親プロセスは,終了した子プロセスのみに対して,waitを実行する必要がある
------ 連鎖ゲームのサーバプログラムより抜粋(1)
%proc_table;
$AF_INET = 2;
$SOCK_STREAM = 1;
$sockaddr = 'S n a4 x8';
($name, $aliases, $proto) = getprotobyname('tcp');
$this = pack($sockaddr, $AF_INET, $port, "\0\0\0\0");
select(NS); $| = 1; select(stdout);
socket(S, $AF_INET, $SOCK_STREAM, $proto) || die "socket: $!";
bind(S, $this) || die "bind: $!";
listen(S, 5) || die "listen: $!";
select(S); $| = 1; select(stdout);
for ($con = 1; ; $con++) {
# defunctプロセスの回収
my ($proc_key);
# 連想配列から,登録済み子プロセスを列挙する
foreach $proc_key (keys(%proc_table)) {
# ★
if (is_process_termed($proc_key)) {
# プロセス終了フラグファイルを確認できたので,waitで回収する
# ★
waitpid($proc_key, 0);
delete $proc_table{$proc_key};
# ★
delete_process_termed($proc_key);
}
}
($addr = accept(NS, S)) || die $!;
my($child);
if ($child = fork) {
# 子プロセスのPID($child)を,タイムスタンプ付きで連想配列に登録する
# ★
$proc_table{$child} = time;
} elsif (defined $child) {
# 子プロセス処理開始
while (<NS>) {
〜 子プロセス実処理 〜
}
close(NS);
# プロセス終了フラグファイルを設定して子プロセスを終了する
# ★
set_process_termed();
exit;
}
close(NS);
}
ややこしいのですが,要点は★を付けた箇所です.
- fork()時には,%proc_tableという連想配列に子プロセスのPIDを登録する
- fork()の前に%proc_tableをスキャンして,もし終了しているPIDがあれば,そのプロセスをwait()してPIDを連想配列から除去する
%proc_table登録時のタイムスタンプは,今回は無視してください. (実プログラムでは,ある時間が経過したプロセスのみをチェック対象として,処理の軽量化をはかっています.)
ここで出てきた,プロセスの終了判定の関数(is_process_termed(), set_process_termed(), delete_process_termed())は,次のリストに出てきます.
------ 連鎖ゲームのサーバプログラムより抜粋(2)
# プロセス終了フラグファイルの存在確認
sub is_process_termed {
my($proc_key) = @_;
my($proc_term_file) = "/tmp/pterm_" . $proc_key;
if (-e $proc_term_file) {
return 1;
} else {
return 0;
}
}
# プロセス終了フラグファイルを削除する
sub delete_process_termed {
my($proc_key) = @_;
my($proc_term_file) = "/tmp/pterm_" . $proc_key;
unlink($proc_term_file);
}
# プロセス終了フラグファイルを作成する
sub set_process_termed {
my($proc_key) = $$;
my($proc_term_file) = "/tmp/pterm_" . $proc_key;
open(PTERMFILE, ">$proc_term_file");
print PTERMFILE "1\n";
close(PTERMFILE);
}
要は,プロセス開始時にテンポラリファイルを作成して,終了時に削除することで,そのファイルの有無をチェックすればプロセスが終了したかどうかを知ることができる,というだけの話です.
多分,psコマンドの実行結果からawk,grep等を使ってうまく情報を取り出せば,こんな面倒なことをやらなくても終了判定はできるような気がしますし,また,perlに詳しい人ならもっとエレガントな方法があるのだろうと思います.
まぁ,連鎖ゲームでは問題なく動いているので,いいかなというところです.