Perl Hackers Hub

第6回 UNIXプログラミングの勘所(3)

この記事を読むのに必要な時間:およそ 3 分

シグナル制御

シグナルはUNIXにおけるプロセス間通信の手法の一つであり注7),プロセスへ一方向の単純な通知機能を提供すると同時に,子プロセスの死活管理にも使われる,とても身近なものです。Perlでは%SIGというグローバル変数を使って,シグナルを受信したときの処理をカスタマイズできます。本節では,シグナルに呼応して処理を行うプログラムの書き方を紹介します。

注7)
ほかの通信手法としては,パイプやUNIXドメインソケットなどがあります。

シグナルの種類とドキュメント

シグナルには特定の現象が発生した場合にOSが送信してくるものや,デーモンプロセスを制御するためにユーザが送信するものなど,さまざまなものが存在します。Perlでプログラムを書く際によく取り扱うシグナルは表1のとおりですが,これ以外にもPOSIXで規定されているもの,OSごとに独自のものがあり,manなどを参照することで確認できます注8)。それぞれのシグナルを受信した場合のデフォルトの動作がなんであるかについても,これらのドキュメントに記載があります。また,Perlにおけるシグナルの取り扱い方について詳しくは,perldoc perlipcを参照してください。

表1 主なシグナル

シグナル名意味
SIGHUP主に設定ファイルの再読み込みを要求するために使用
SIGINT割り込みを要求(Ctrl-Cが押された場合など)
SIGKILLプロセスを強制終了
SIGPIPE閉じられたパイプやソケットに書き込もうとした
SIGALRMalarmで設定した時間が経過
SIGTERMプロセスの終了を要請
SIGCHLD子プロセスが終了
注8)
Linuxの場合はman 7 signalです。

ネットワークプログラムとSIGPIPE

「私の書いたサーバが突然死するんです。どうしてでしょうか」という質問を受けることがあります。これは多くの場合,SIGPIPEの処理を忘れていることが原因です。SIGPIPEとは,切断されたネットワークソケットなどにデータを書き込もうとした際に送出されるUNIXシグナルです。特に設定しない限り,プロセスはSIGPIPEを受け取ると強制終了されます。そのため,通信が突然切断される可能性のあるTCPサーバにおいては,SIGPIPEを無視するよう設定する必要があります。

# デフォルトの動作(SIGPIPEの場合はプロセスの終了)に設定
$SIG{PIPE} = 'DEFAULT';

# SIGPIPEを無視するよう設定
$SIG{PIPE} = 'IGNORE';

# SIGPIPEを受信した際に実行するサブルーチンリファレンスを
# 設定
$SIG{PIPE} = sub {
    ...
};

# SIGPIPEを受信した際に実行するサブルーチン名を設定(古い
# スタイル)
$SIG{PIPE} = 'my_func';

シグナルとErrno::EINTR

シグナルハンドラ(%SIGの各要素の値)として'IGNORE'をセットした場合,シグナルは無視され,プロセスは何事もなかったかのように処理を継続します。

一方でサブルーチンを指定した場合は,シグナルを受信した段階でそのサブルーチンが呼び出されます。また,ネットワーク通信などを行うシステムコールが実行中だった場合は,その処理は中断されます。例として,次のコードを見てみましょう。

# SIGALRMのハンドラとして,何もしないサブルーチンをセット
$SIG{ALRM} = sub {};
# 1秒後にSIGALRMを送出するようセット
alarm(1);
# 10秒sleep
sleep(10);
# 実際は1秒後にSIGALRMを受信した段階でsleepが中断される

alarm関数は,指定した時間(単位:秒)が経過したあとに自分自身のプロセスに対してSIGALRMを送信するようOSに依頼する関数です。$SIG{ALRM}にサブルーチンがセットされているので,SIGALRMを受信した段階で,ただちにシグナルハンドラ(この例では空のサブルーチン)が実行され,そのままsleepの処理が中断されます。また,$!にはシグナルを受信したためにシステムコールの実行が中断されたことを示すエラーコードErrno::EINTRがセットされます。

ただし先述したように,シグナルハンドラが'IGNORE'だった場合は,sleepの実行は中断されないという点に注意してください。

alarmを使った通信のタイムアウト処理

sleepを中断するよりも実際的な例としては,ネットワーク通信のタイムアウト処理を挙げることができます。次のコードは,alarmを用いてソケットからの読み込みについてタイムアウト処理を行っています。

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を使ってタイムアウト処理を行うことのメリットは,ほとんどすべてのシステムコールに対してタイムアウトを設定できるという点にあります。一方,デメリットとしては,プロセス全体で1個のタイマーしか持てないため,階層化されたプログラムにおいて使いにくいという点が挙げられます注9)。

そのため,alarm(とシグナルを使ったタイムアウト処理)はモジュールレベルではなくアプリケーションレベルでのタイムアウト処理に使われる一方,ネットワーク通信のようなモジュールレベルで実現可能なタイムアウト処理については,select関数を使って実装される場合が多いです。

注9)
alarmの戻り値を利用することで(あるいは,より細かな操作を行うための関数であるTime::HiRes::setitimerを呼び出すことで),階層化されたタイマーを実現することも不可能ではないのですが,煩雑な処理が必要になります。

著者プロフィール

奥一穂(おくかずほ)

株式会社ディー・エヌ・エー所属

コメント

コメントの記入