思い立ったので適当に書いてみる
Perlのシグナルハンドラは%SIGグローバル変数にシグナル毎のハンドラを設定することで実現できる。例えばCtrl-CはSIGINTを発生させるので、INT用のハンドラを設定する:
$SIG{ INT } = sub { warn "got SIGINT!" };
%SIGに限った事ではないのだけれども、ここでまず注意しなくてはいけないのが、%SIGはグローバル変数であること。ある関数foo()内で%SIGをいじったあとにreturnすると、foo()を呼び出したスコープでもその値は有効になってしまう。これだと自分はよくても他のモジュール等に影響を与える可能性がある。そこでまず癖としてlocal修飾子を使う癖をつけてほしい
local $SIG{INT} = sub { warn "got SIGINT (OUTSIDE)" };
sub foo {
warn "inside foo()";
local $SIG{INT} = sub { warn "got SIGINT (INSIDE foo)" };
sleep 10;
}
warn "sleeping...";
sleep 10;
foo();
warn "outside foo()";
sleep 10;
こんなコードを書いて、foo()を呼ぶ前、foo()の中、foo()を呼んだ後、と3回Ctrl-Cを押すとハンドラの効果がfoo()内で局所化されているのがわかると思う(ちなみにsleep中にCtrl-Cするとsleepが邪魔されるので10秒待つことはなくなる)
ちなみに、この局所化はfork()した時にも是非同じ処理をするようにする癖をつけたほうがいい。fork()するような場合では親プロセスでSIGCHLD用の処理を入れてたりするのだが、それをうっかりそのまま継承するとopen("|-")系のコマンドをうっかり使ってしまった時などに問題が起こることがある。ちなみに、この場合は%SIG全体を局所化すればいいだろう:
my $pid = fork();
if (! defined $pid) {
die "Could not fork: $!";
}
if ( $pid ) {
# 親プロセス
} else {
local %SIG; # これだけでOK
.....
exit 1;
}
シグナルはどの言語でもそうだけれども、普通のプログラム処理の流れをぶったぎって処理が入るものなので(厳密にはPerlの1OP毎にシグナルのチェックがはいるので、OP中にシグナルが入ることは基本的にはない)、とにかくシグナルハンドラ内で時間を食う処理をしない、に限る。
基本はフラグをたてるだけでさっとハンドラを終わらせるのが吉。例えばCtrl-Cした時に メインループを終わらせるなら、こんな感じ:
my $loop = 1;
local $SIG{INT} = sub {
$loop = 0;
};
while ( $loop ) {
....
}
自分がやりたいタイミングより1回余計にループが走るかもしれないけれど、逆にそこは基本の動作に任せてきれいにループを終えるほうが後々頭痛の種を作らない。さらに確実に停止したい動作に関してもうまくフラグをチェックしたりなんだりでやる方法が大概ある。
それでもやっぱりある程度の処理をシグナルハンドラ内でしなければいけない場合は、まずまっさきにハンドラ内でシグナルを無視するようにしたほうが良い:
local $SIG{ INT } = sub {
local $SIG{ INT } = 'IGNORE'; # このハンドラ内ではSIGINTは無視
....
}
こうしないとSIGINTで受け付けた処理中にもう一度同じ処理が入ってしまうような妙な状態になってしまうことがる。
ただし、この方法はひとつだけ危険な問題があって、もし万が一ハンドラ内でブロックするような処理をしてしまった場合にはもうシグナルを無視しているのでCtrl-C等が効かなくなる。そういう意味でもやはりシグナルハンドラからは最小限の処理をしてとっととコントロールを元の状態に戻すのがおすすめ。開発当初はIGNOREをつけるのはおすすめしない。俺は何回もそれで失敗してる
ちなみに綺麗に全ての子プロセスを待つためのSIGCHLDなんかは結構面倒くさい。AnyEvent->child() ウォッチャーはその辺りがとても美しいので、便利ですね。ただしfork()の直後に子プロセスが失敗すると、運が悪いとハンドラー設定の前にSIGCHLDが発生してしまうことがあるらしいので注意。