シグナル制御
シグナルはUNIXにおけるプロセス間通信の手法の一つであり
シグナルの種類とドキュメント
シグナルには特定の現象が発生した場合にOSが送信してくるものや、perldoc perlipc
を参照してください。
シグナル名 | 意味 |
---|---|
SIGHUP | 主に設定ファイルの再読み込みを要求するために使用 |
SIGINT | 割り込みを要求 |
SIGKILL | プロセスを強制終了 |
SIGPIPE | 閉じられたパイプやソケットに書き込もうとした |
SIGALRM | alarmで設定した時間が経過 |
SIGTERM | プロセスの終了を要請 |
SIGCHLD | 子プロセスが終了 |
ネットワークプログラムとSIGPIPE
「私の書いたサーバが突然死するんです。どうしてでしょうか」
# デフォルトの動作(SIGPIPEの場合はプロセスの終了)に設定
$SIG{PIPE} = 'DEFAULT';
# SIGPIPEを無視するよう設定
$SIG{PIPE} = 'IGNORE';
# SIGPIPEを受信した際に実行するサブルーチンリファレンスを
# 設定
$SIG{PIPE} = sub {
...
};
# SIGPIPEを受信した際に実行するサブルーチン名を設定(古い
# スタイル)
$SIG{PIPE} = 'my_func';
シグナルとErrno::EINTR
シグナルハンドラ'IGNORE'
をセットした場合、
一方でサブルーチンを指定した場合は、
# SIGALRMのハンドラとして、何もしないサブルーチンをセット
$SIG{ALRM} = sub {};
# 1秒後にSIGALRMを送出するようセット
alarm(1);
# 10秒sleep
sleep(10);
# 実際は1秒後にSIGALRMを受信した段階でsleepが中断される
alarm関数は、$SIG{ALRM}
にサブルーチンがセットされているので、$!
にはシグナルを受信したためにシステムコールの実行が中断されたことを示すエラーコードErrno::EINTRがセットされます。
ただし先述したように、'IGNORE'
だった場合は、
alarmを使った通信のタイムアウト処理
sleepを中断するよりも実際的な例としては、
my $sock = IO::Socket::INET->new(...);
...
# localを用いて、このブロック限定のシグナルハンドラを設定
local $SIG{ALRM} = sub {};
# タイマーを設定
alarm($timeout);
# 読み込み開始。データが読み込まれるか、あるいはタイマー
# 時間が経過するまでブロック
my $len = $sock->read(my $buf, $maxlen);
# もしデータがなく、$!がEINTRだったらタイムアウト
if (! defined($len) && $! == Errno::EINTR) {
warn 'timeout’;
...
}
alarmを使ったタイムアウト処理のメリット/デメリット
alarmを使ってタイムアウト処理を行うことのメリットは、
そのため、
シグナルとデーモンの終了処理
シグナルはタイムアウト処理以外にもさまざまな場面で使われます。多くのデーモンプログラムには、
このような終了処理の実現方法に関する知識は、
GearmanのワーカをGraceful Shutdownする方法
ドキュメントには書いてありませんが、
use Gearman::Worker;
my $worker = Gearman::Worker->new;
... # ワーカのセットアップ
# フラグを立てるシグナルハンドラを登録
my $stop_worker = undef;
$SIG{TERM} = sub {
$stop_worker = 1;
};
while (! $stop_worker) {
$worker->work(
stop_if => sub {
$stop_worker;
},
);
}
中断可能なコードの書き方
Gearmanのワーカ以外にもGraceful Shutdownが必要なケースはありますし、
# 中断要請を管理するフラグを用意し、シグナルを受信したら
# セット
my $stop_worker = undef;
$SIG{TERM} = sub {
$stop_worker = 1;
};
sub do_some_work {
...
my $len;
{
# 処理の開始前に中断要請があったか確認し、
# あったならreturn
return if $stop_requested;
# ブロックする処理を実行
$len = $sock->read(my $buf, $maxlen);
if (! defined($len) && $! == Errno::EINTR) {
# シグナルを受信したのでリトライ
redo;
}
}
...
}
このコードを理解するうえでのポイントは次の3点です。
- 中断要請を管理するフラグ
($stop_ requested) を設け、 シグナルを受信したらフラグをセットする - 重い処理
(ここでは$sock->read) を実行する前に、 必ず中断要請フラグを確認し、 要請があればreturn - 重い処理がシグナルの受信が原因で失敗した場合には再実行
fork、waitとSIGCHLD
UNIXでは、
use POSIX;
# 子プロセスを生成
my $pid = fork;
die "fork failed:$!"
unless defined $pid;
if ($pid == 0) {
# 子プロセス
...
exit(0);
}
# 親プロセスは子プロセスが終了するまでwait
while (wait == -1) {}
# 終了コード(あるいはシグナル)を表示
if (WIFEXITED($?)) {
print "child exitted status: ", WEXITSTATUS($?),
"\n";
} else {
print "child was killed by signal ", WTERMSIG($?),
"\n";
}
子プロセスの終了ステータスは親プロセスがwaitを呼び出して回収するまでOSの中に残り続け、
あるいは、$SIG{CHLD} = 'IGNORE'
とすることで、
残念なことにPerlのwait関数は、
まとめ
特にデーモンプログラムの場合、
また、