シグナル
シグナルはカーネルに備わるプロセスの実行に関わる機能のひとつで、実行中のプロセスへそのプロセス内やシステムで発生した状態変化や緊急的な命令などを通知します。シグナルを受け取ったときのプロセスのデフォルトの振る舞いはOSで定められており、シグナルの発生理由によってはカーネルにより強制終了となる場合があります。
全てのプロセスはカーネルや他のプロセスで発生したシグナルを受け取る、または他のプロセスに送ることができます。シグナルの各プロセスへの配送はカーネルにより行われます。
シグナルは
POSIX標準で32種類定義されており、通知するイベント内容ごとに1~32番の番号が割り当てられています。シグナルが通知するイベントは、プロセスの中断や終了の通知、例外発生や状態変化などが非同期に通知され、各シグナルの通知理由ごとに定められた処理(
シグナルアクション)がプロセスの実行に「割り込んで」実行されます。シグナルアクションは各シグナルごとに次のいずれかを行います。
なにもしない(無視)
|
終了
|
異常終了(終了とコアダンプ生成)
|
停止
|
再開
|
シグナルは何れのアクションもデフォルトはカーネルで処理されますが、signal()またはsigaction()により、プロセスが受け取る任意のシグナルの配送を捕捉して、デフォルトのアクションからユーザ定義の関数を実行するように変更できます。このとき設定する関数をシグナルハンドラと呼びます。デフォルトではプロセス終了するところを、終わらせるのではなく独自の処理を行わせるなどが可能になります。
ただし例外として、SIGKILL/SIGSTOP/SIGABRTはデフォルトのアクションから変更できません。
実行中の例外などによるシグナルは非同期に発生しますが、killコマンドやkill()システムコールの呼び出しにより、任意、またはプログラムされたタイミングで発生させることができます。あるいはraise()により自分自身に送ることもできます。
最も身近なシグナルは、シェルからコマンドの実行を止めるときのCTRL-Cです。CTRL-Cのとき、フォアグラウンドで実行しているコマンドにSIGINTが送られます。SIGINTのデフォルトのシグナルアクションはプロセス終了なので、コマンドが終わりシェルプロンプトに戻ります。
シグナルの種類と番号はPOSIXで標準化されていますが、OSやアーキテクチャによって異なることがあります。シグナル番号はsignal.hに定義されています。Linuxでは以下の番号とデフォルトのシグナルアクションが定義されています。
名称
|
シグナル番号
|
デフォルト動作
|
発生理由
|
SIGHUP
|
1
|
終了
|
制御端末の切断(ハングアップ)、仮想端末の終了※
|
SIGINT
|
2
|
終了
|
キーボード割り込み(CTRL+C)
|
SIGQUIT
|
3
|
異常終了
|
キーボードによる中止(CTRL+\)※
|
SIGILL
|
4
|
異常終了
|
プログラムカウンタの場所が命令でない
|
SIGTRAP
|
5
|
異常終了
|
デバッガなどのブレークポイント
|
SIGABRT
|
6
|
異常終了
|
プロセスの中断(abort()による)※
|
SIGBUS
|
7
|
異常終了
|
バスエラー 未定義領域、許可がないなどの不正なアドレスへのアクセス
|
SIGFPE
|
8
|
異常終了
|
ゼロ除算、オーバーフローなど浮動小数点演算違反(整数演算でも発生する)
|
SIGKILL
|
9
|
終了
|
強制終了※
|
SIGUSR1
|
10
|
終了
|
ユーザー定義シグナル※
|
SIGSEGV
|
11
|
異常終了
|
不正なメモリ参照(NULLポインタアクセス)
|
SIGUSR2
|
12
|
終了
|
ユーザー定義シグナル※
|
SIGPIPE
|
13
|
終了
|
すでに閉じたパイプやソケットへの書き込み
|
SIGALRM
|
14
|
終了
|
alrrm()によるタイマー割り込み
|
SIGTERM
|
15
|
終了
|
終了(killコマンドのデフォルト)
|
SIGSTKFLT
|
16
|
異常終了
|
未使用(数値演算プロセッサのスタックフォルト)
|
SIGCHLD
|
17
|
無視
|
子プロセスの状態変化(終了・停止・再開)の親プロセスへの通知
|
SIGCONT
|
18
|
再開
|
SIGSTOPで一時停止しているジョブの再開
|
SIGSTOP
|
19
|
一時停止
|
一時停止(SIGCONTで再開)※
|
SIGTSTP
|
20
|
一時停止
|
フォアグラウンドジョブの一時停止(CTRL+Z)
|
SIGTTIN
|
21
|
一時停止
|
バックグラウンドジョブがキーボード入力待ちとなって一時停止
|
SIGTTOU
|
22
|
一時停止
|
バックグラウンドジョブが端末出力待ちとなって一時停止
|
SIGURG
|
23
|
無視
|
ソケットに緊急データ(OOB)の受信があった
|
SIGXCPU
|
24
|
異常終了
|
プロセスに許可されているCPU時間制限を超過した
|
SIGXFSZ
|
25
|
異常終了
|
ファイルサイズの制限を越えたファイルを作成しようとした
|
SIGVTALRM
|
26
|
終了
|
仮想アラームによるCPU時間経過(setitimer()による)
|
SIGPROF
|
27
|
終了
|
仮想アラームによるCPU時間経過(setitimer()による)
|
SIGWINCH
|
28
|
無視
|
ウィンドウサイズ(ターミナルのサイズ)の変更
|
SIGPOLL
|
29
|
終了
|
ポーリングしているディスクリプタのイベント発生
|
SIGPWR
|
30
|
終了
|
電源喪失
|
SIGSYS
|
31
|
異常終了
|
不正なシステムコール呼び出し
|
※SIGHUP
|
デフォルトのアクションはプロセス終了だが、デーモンやサーバなどでは再起動するように実装して設定をリロードする目的に使われること多い。
|
※SIGQUIT
|
SIGINTと違うのはコアダンプを生成して終了すること。
|
※SIGABRT、SIGKILL、SIGSTOP
|
ユーザのシグナルハンドラでの捕捉や、無視に変更することはできない。
|
※SIGUSR1、SIGUSR2
|
古いLinuxカーネルではスレッド間同期に使われていたが、現在は特に用途は決められていない。
|
※SIGXCPU、SIGXFSZ
|
プロセスへのリソースはsetrlimit()で制限を設定できる。
|
killコマンド
シグナルはプロセスからプロセスへ送ることができます。killコマンドは任意のプロセスへ任意のシグナルを発生させます。
次の例は、killコマンドでSIGTERMシグナルを送信して実行中(スリープ中)のsleepコマンドを終了させています。
$ sleep 60 &
[1] 254313
$ kill 254313
$ ps
[1]+ Terminated sleep 60
killコマンドはオプションでシグナル番号かシグナル名を指定することで、送信するシグナルを選択できます。指定しない場合のデフォルトはSIGTERMです。上の例はsleepのプロセスにSIGTERMを送信していることになります。
シグナルの送信 kill( )
kill()システムコールは、指定するプロセスIDのプロセスへ指定するシグナルを送ります。
int kill(pid_t pid, int sig);
pid
|
送信先プロセスのプロセスID
|
sig
|
送信するシグナル番号
|
return
|
成功:0 失敗:-1
|
次の例は、親プロセスがfork()で生成した子プロセスへ3秒後にSIGTERMを発生させます。子プロセスは3秒後に終了します。
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
int main(void)
{
int pid;
pid = fork();
if (pid == 0) {
while (1) {
puts("Sleep");
sleep(1);
}
}
sleep(3);
kill(pid, SIGTERM);
return 0;
}
Sleep
Sleep
Sleep
kill()を使ってそのままkillコマンドと同等なものが作れます。
sig_kill.c
#include <stdlib.h>
#include <signal.h>
int main(int argc, char *argv[])
{
return kill(atoi(argv[1]), SIGTERM);
}
$ sleep 60 &
[1] 270926
$ ./sig_kill 270926
[1]+ Terminated sleep 60
自身のプロセスにシグナル発生 raise( )
raise()は自身のプロセスへ任意のシグナルを発生させます。
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
void sighdl(int sigtype)
{
printf("Handle signal %d\n", sigtype);
}
int main(void)
{
signal(SIGUSR1, sighdl);
puts("start");
sleep(3);
raise(SIGUSR1);
return 0;
}
start
Handle signal 10
シグナルの捕捉とアクションの変更① signal( )
signal()により、自身のプロセスに配送されるシグナルのデフォルトのシグナルアクションをデフォルト以外の動作に変更する、あるいはシグナルを捕捉し、カーネルのデフォルト処理からユーザ定義の関数(シグナルハンドラ)を実行するように変更できます。
※SIGKILL/SIGSTOP/SIGABRTは変更できません。
void(*signal(int sig, void(*func)(int)))(int);
sig
|
シグナル番号
|
func
|
ユーザ定義の関数(シグナルハンドラ)
あるいは、
SIG_IGN: 無視するように変更
SIG_DFL: デフォルトのシグナルアクションに戻す
|
return
|
成功:func() 失敗:SIG_ERR
|
独自のシグナルハンドラを設定した場合、指定のシグナルを受けると、デフォルトのアクションではなくハンドラ関数がプロセスの実行に割り込みます。ハンドラが返ると割り込みで中断していた箇所から再開します。
次の例は、SIGINT(CTRL-C)をデフォルトのアクションからユーザ定義したsighdl()関数を実行するように変更しています。このプロセスをシェルで起動しCTRL-Cを入力すると、即時停止せずにハンドラの中でメッセージを表示して永久ループを脱出してから終了します。
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
int run = 1;
void sighdl(int sigtype)
{
printf("Handle signal %d\n", sigtype);
run = 0;
}
int main(void)
{
signal(SIGINT, sighdl);
while (run) {
puts("running");
sleep(1);
}
puts("stop by signal");
return 0;
}
running
running
running
running
^C
Handle signal 2
stop by signal
ユーザ定義のシグナルハンドラではなくSIG_IGNを指定すると、シグナルを無視するように変更できます。また、SIG_DFLでデフォルトのアクションに戻すことができます。
次の例では、SIGINTを無視するように設定変更して、10秒経過後にデフォルトに戻しています。開始から10秒間はCTRL-Cを受け付けず無視し、10経過後から受け付けて止まるようになります。
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
int main(void)
{
int i;
signal(SIGINT, SIG_IGN);
for (i = 1; ; i++) {
puts("running");
sleep(1);
if (i >= 10)
signal(SIGINT, SIG_DFL);
}
return 0;
}
running
running
running
running
^Crunning
running
running
running
running
^Crunning
running
running
running
running
^C
タイマー alarm( )
alarm()は、引数に指定する秒数後にSIGALRMを自分自身へ発行します。
次の例は、永久ループが3秒後に終了しプロセスが終了(SIGALRMのデフォルトのアクション)します。
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
int main(void)
{
alarm(3);
while (1) {
sleep(1);
printf("sleep\n");
}
}
sleep
sleep
Alarm clock
alarm()を設定して、別の秒数を再設定すると再設定した時間で開始します。その場合の戻り値は最初のalarm()からの残り時間が返ります。また、alarm()に0秒を指定すると、タイマーキャンセルになります。
次の例では、最初に設定する10秒のアラームは2秒後に取り消され、5秒に再設定されます。
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
int main(void)
{
alarm(10);
sleep(2);
printf("%d\n", alarm(5));
while (1) {
sleep(1);
printf("sleep\n");
}
}
0
8
sleep
sleep
sleep
sleep
Alarm clock
SIGALRMのデフォルトのアクションはプロセス終了です。signal()によりシグナルハンドラを設定するとインターバルタイマー処理が実装できます。
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
int loop = 1;
void hdlalarm()
{
puts("ALARM!");
loop = 0;
}
int main(void)
{
signal(SIGALRM, hdlalarm);
alarm(2);
while (loop) {
sleep(1);
}
puts("EXIT");
}
ALARM!
EXIT
シグナル受信まで中断 pause( )
pause()はシグナルを受けるまでプロセスの実行を中断します。中断中にデフォルトがプロセスを終了するシグナルか、シグナルハンドラが設定されているシグナルの受信を受けることで再開します。再開したpause()は-1を戻り値に返し、errnoにEINTRを代入します。
次の例では、pause()で処理が止まり、SIGALRMで3秒後に再開します。ただしpause()の次から実行が再開するのではなく、SIGALRMのデフォルトのアクションとして即時プロセス終了となります。
#include <unistd.h>
#include <signal.h>
#include <errno.h>
int main(void)
{
alarm(3);
pause();
return 0;
}
次のように、SIGALRMのシグナルハンドラに何もしない関数を設定してプロセスが終了しないようにすることで、pause()の解除の続きを実行させることができます。
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <errno.h>
void hdl(int sig)
{
}
int main(void)
{
signal(SIGALRM, hdl);
alarm(3);
pause();
printf("errno=%d <%s>\n", errno, strerror(errno));
return 0;
}
errno=4 <Interrupted system call>
シグナルの捕捉とアクションの変更② sigaction( )
sigaction()はsignal()と同様に、ユーザ定義のシグナルハンドラを設定することで、デフォルトのシグナルアクション以外の処理を実行させることができます。sigaction()はsignal()よりも機能が多く、より細かいシグナルハンドリングができます。そのかわり使い方は面倒です。
POSIX標準ではsigaction()を推奨しsignal()を非推奨としてます。その一方でsigaction()はC準拠ではないのでUNIX系以外のOSとの移植性に問題があります。
int sigaction(int sig, const struct sigaction *act, struct sigaction *oldact);
sig
|
シグナル番号
|
act
|
sigaction構造体(シグナルハンドラ設定やマスクするシグナルの指定)
oldactを使う場合は通常NULL
|
oldact
|
現在の設定のsigaction構造体を取得
actを設定する場合は通常NULL
|
return
|
成功:0 失敗:-1
|
sigaction構造体は次のように定義されています。
struct sigaction {
void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void); };
sa_handler
|
シグナルハンドラ関数(signal()と同じ)
|
sa_sigaction
|
シグナルハンドラ関数(詳細)
|
sa_mask
|
ハンドラ実行中にブロック(保留)するシグナルセット
|
sa_flags
|
シグナルハンドラ実行に関する設定のフラグ
|
sa_restorer
|
未使用
|
次の例は、signal()の例で示したSIGINT(CTRL-C)をユーザ定義シグナルハンドラでハンドリングする実装を、sigaction()を使って置き換えたものです。
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
int run = 1;
void sighdl(int sigtype)
{
printf("Handle signal %d\n", sigtype);
run = 0;
}
int main(void)
{
struct sigaction sa;
memset(&sa, 0, sizeof(sa));
sa.sa_handler = sighdl;
sa.sa_flags = 0;
sigaction(SIGINT, &sa, NULL);
while (run) {
puts("running");
sleep(1);
}
puts("stop by signal");
return 0;
}
running
running
running
^CHandle signal 2
stop by signal
sigaction構造体のsa_handlerにsignal()と同様な関数ポインタを代入してシグナルハンドラを設定できます。
sigaction構造体のsa_sigactionにもシグナルハンドラを設定することができ、こちらを使うと発生したシグナルについての詳細な情報を得ることができます。
sa_sigactionにハンドラを設定する場合は、sa_flagsにSA_SIGINFOを指定する必要があります。関数の第二引数にはsignal.hに定義されているsiginfo_t構造体にシグナル発生に関する詳細な情報が格納されます。
第三引数はucontext_t構造体をキャストしたものが返り、ここにはシグナル発生時点のプロセスのコンテキストの状態(スタックなど)が保存されます。第三引数は通常は使いません。
次の例は、上の例をsa_sigactionのシグナルハンドラを使うように変えたものです。ハンドラの第二引数からいくつかの詳細情報を表示しています。
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
int run = 1;
void sighdl(int sigtype, siginfo_t *info, void *ctx)
{
printf("Handle signal %d code=%d\n", info->si_signo, info->si_code);
run = 0;
}
int main(void)
{
struct sigaction sa;
memset(&sa, 0, sizeof(sa));
sa.sa_sigaction = sighdl;
sa.sa_flags = SA_SIGINFO;
sigaction(SIGINT, &sa, NULL);
while (run) {
puts("running");
sleep(1);
}
puts("stop by signal");
return 0;
}
running
running
running
^CHandle signal 2 code=128
stop by signal
現在のsigaction設定を取得する
sigaction()の第二引数をNULLとして第三引数にsigaction構造体を渡すと、現在の当該シグナルのアクションの設定を取得できます。これは、独自のハンドラを設定した後にデフォルトに戻したいときなどに役立ちます。
次の例は、最初にSIGINTのデフォルトのアクションをsigaction構造体にバックアップし、SIGINT(CTRL-C)についてユーザ定義のシグナルハンドラを設定して処理した後にバックアップから元に戻しています。元に戻した後のCTRL-Cではシグナルハンドラは実行されず、デフォルトのアクションであるプロセス終了になるので、最後の「stop by signal 2」は表示されません。
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
int run = 1;
void sighdl(int sigtype)
{
printf("Handle signal %d\n", sigtype);
run = 0;
}
int main(void)
{
struct sigaction sa, oldsa;
sigaction(SIGINT, NULL, &oldsa);
sa.sa_handler = sighdl;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
sigaction(SIGINT, &sa, NULL);
while (run) {
puts("running 1");
sleep(1);
}
puts("stop by signal 1");
sigaction(SIGINT, &oldsa, NULL);
run = 1;
while (run) {
puts("running 2");
sleep(1);
}
puts("stop by signal 2");
return 0;
}
running 1
running 1
running 1
^CHandle signal 2
stop by signal 1
running 2
running 2
^C
シグナルのブロック(保留)
プロセスがシグナルを受けたとき、デフォルトのシグナルアクション、もしくはユーザ定義のシグナルハンドラを実行する以外に、シグナルをブロックすることができます。シグナルのブロックとは無視ではなく、カーネルによるシグナルの配送を一時保留することです。任意のシグナル(複数可)にブロックを設定すると、それらを受けてもすぐにアクションを行わず処理を保留し、ブロック解除後に所定のシグナルアクションを実行します。
ブロックするシグナルはsigset_t構造体へマスクを設定します。sigset_t構造体はメンバを直接操作するのではなく、次のマスク関数を使います。
int sigemptyset(sigset_t *set) 全てリセット
int sigfillset(sigset_t *set) 全てセット
int sigaddset(sigset_t *set, int sig) 指定シグナルをセット
int sigdelset(sigset_t *set, int sig) 指定シグナルをリセット
int sigismember(sigset_t *set, int sig) 指定シグナルがセットされているか
set
|
sigset_t構造体(シグナルの集合を扱う構造体)
|
sig
|
シグナル番号
|
return
|
成功:0 失敗:-1
sigismember()は セット:1 リセット:0
|
これらの関数によりブロックするシグナルをマスクしたsigset_t構造体を、sigprocmask()へ設定し呼び出すことで、選択したシグナルのブロック・ブロック解除操作を行います。
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
how
|
SIG_BLOCK(ブロック)
SIG_UNBLOCK(ブロック解除)
|
set
|
マスク対象シグナル(sigset_t構造体)
|
oldset
|
現在のマスク(不要ならNULLを指定)
|
return
|
成功:0 失敗:-1
|
次の例は、5秒間sleepが回っている最中はSIGINTをブロック(保留)しているためCTRL-Cが効かず、ループ終了でブロック解除すると同時にCTRL-Cのアクションであるプロセス終了になります。ブロック解除によりデフォルトのアクションが起動するので最後の「Unreachable」は表示されません。
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
int main(void)
{
int i;
sigset_t set;
sigemptyset(&set);
sigaddset(&set, SIGINT);
sigprocmask(SIG_BLOCK, &set, NULL);
printf("SIGINT blocked\n");
for (i = 0; i < 5; i++) {
puts("running");
sleep(1);
}
printf("unblock SIGINT\n");
sigprocmask(SIG_UNBLOCK, &set, NULL);
puts("Unreachable\n");
return 0;
}
SIGINT blocked
running
running
^Crunning
running
running
unblock SIGINT
シグナルハンドラ実行中のブロック
sigaction()に指定するsigaction構造体のsa_maskはsigset_t構造体であり、上の例のようにsigaddset()などでブロックしたいシグナルをセットすると、シグナルハンドラ実行中のシグナルの配送をブロック(保留)できます。
例えば、あるシグナルのユーザ定義のシグナルハンドラを実行している最中に別のシグナルが続けて入ると、シグナルハンドラの処理中に別のシグナルが割り込み、種類によってはその瞬間にプロセスが終了してしまいます。sa_maskの設定により、シグナルハンドラが完了するまで設定したシグナルを保留させることができます。
次の例は、SIGUSR1のシグナルハンドラ実行中のSIGINTを保留するよう設定し、5秒かかるシグナルハンドラの最中にCTRL-Cが入力されても、5秒のハンドラ処理が終わってからプロセス終了するようにしています。シグナルハンドラの終了と共にブロックは自動で解除されるので、最後の「Unreachable」は表示されません。
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
void sighdl(int sigtype)
{
printf("Handle signal %d\n", sigtype);
sleep(5);
puts("exit handler");
}
int main(void)
{
struct sigaction sa;
memset(&sa, 0, sizeof(sa));
sa.sa_handler = sighdl;
sigemptyset(&sa.sa_mask);
sigaddset(&sa.sa_mask, SIGINT);
sa.sa_flags = 0;
sigaction(SIGUSR1, &sa, NULL);
raise(SIGUSR1);
puts("Unreachable");
return 0;
}
Handle signal 10
^C
保留したシグナルの取得 sigpending( )
sigpending()は、ブロック中に受けて処理を保留しているシグナルを取得します。
int sigpending(sigset_t *set);
set
|
処理保留中のシグナルセットが返る
|
return
|
成功:0 失敗:-1
|
次の例は、SIGINTをブロックしている5秒間のループ中にCTRL-Cが入力されたことを表示しています。
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
int main(void)
{
int i;
sigset_t set, pend;
sigemptyset(&set);
sigaddset(&set, SIGINT);
sigprocmask(SIG_BLOCK, &set, NULL);
printf("SIGINT blocked\n");
for (i = 0; i < 5; i++) {
puts("running");
sleep(1);
}
sigpending(&pend);
if (sigismember(&pend, SIGINT))
puts("pending SIGINT");
printf("unblock SIGINT\n");
sigprocmask(SIG_UNBLOCK, &set, NULL);
puts("Unreachable\n");
return 0;
}
SIGINT blocked
running
running
^Crunning
running
running
pending SIGINT
unblock SIGINT
シグナルの待機 sigwait( )
sigwait()はsigset_t構造体にセットしたシグナルを受けるまで実行を待機します。
sigwait()で待つシグナルが処理待ち(保留)になった時点で返ります。
int sigwait(const sigset_t *set, int *sig);
set
|
シグナルセット(待ち受けるシグナル)
|
sig
|
受けたシグナル番号
|
return
|
成功:0 失敗:EINVAL
|
次の例では、fork()で生成した子プロセスへ3秒後にSIGTERMを送ります。子プロセスはsigwait()でSIGTERMを待機します。
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
int main(void)
{
sigset_t set;
int sig;
int pid;
pid = fork();
if (pid == 0) {
sigemptyset(&set);
sigaddset(&set, SIGTERM);
sigprocmask(SIG_BLOCK, &set, NULL);
puts("waiting");
sigwait(&set, &sig);
puts("received");
}
sleep(3);
kill(pid, SIGTERM);
return 0;
}
waiting
〜3秒後〜
received