2.初期のシグナル
2.1 一般的な実装方法
シグナルにはデフォルト動作と無視する動作とユーザープログラム側の再定義可能な動作とが存在します。これらのシグナルの動作を指定するのがsignal(2)です。
#include <signal.h> typedef void (*sighandler_t)(int); sighandler_t signal(int signum, sighandler_t sighandler);
第1引数はintで、設定したいシグナルの番号になります。第2引数は設定すべきシグナルハンドラへの関数ポインタで、その関数はintの引数1つを受け付けるvoid型の関数です。
/*
gcc -g -W -Wall signal_test_1.c -o signal_test_1
*/ #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <signal.h> #include <errno.h> #include <string.h> void signal_handler( int signo ); int main( ) { char buf[256]; int ret; signal( SIGINT, signal_handler ); while( 1 ) { memset( buf, '\0', sizeof( buf )); ret = read( 0, buf, sizeof( buf )); if( ret == -1 ) { fprintf( stderr, "read error!\n" ); exit( 1 ); } write( 1, buf, strlen( buf )); } return 0; } void signal_handler( int signo ) { ( void )signo; char *mes = "signal happened!!\n"; write( 1, mes, strlen( mes )); }
上記プログラムはCtrl+Cが押された時にsignal_handler()関数が呼び出されます。この時、signal_handler()関数の引数signoにはそのシグナルの番号(前ページの表1参照)が入ります。複数のシグナルに対し同一のシグナルハンドラを設定した場合などに区別するのにsignoが使えます。
また、特定のシグナルを無視したり、デフォルト動作に戻すという指定も定義されています。
下記プログラムは、画面でなにか入力される度にSIGINTを有効/無効を切替えます。
/*
#define SIG_DFL ((__sighandler_t)0) <- デフォルト
#define SIG_IGN ((__sighandler_t)1) <- 無視する
#define SIG_ERR ((__sighandler_t)-1) <- エラーを表す
gcc -g -W -Wall signal_test_2.c -o signal_test_2
*/ #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <signal.h> #include <errno.h> #include <string.h> void signal_handler( int signo ); int main( ) { int flg = 0; signal( SIGINT, signal_handler ); while( 1 ) { getchar( ); if( ! flg ) signal( SIGINT, SIG_IGN ); else signal( SIGINT, signal_handler ); flg ^= 1; } return 0; } void signal_handler( int signo ) { ( void )signo; char *mes = "signal happened!!\n"; write( 1, mes, strlen( mes )); }
なお、これらプログラムを終了させるには、他の画面からSIGTERMを送っても良いですが、画面からCtrl+Zを押下してプロセスをバックグラウンドに回し、その後kill %+と打つことで終了できます。
2.2 リエントラント問題
上記の通りシグナルはOSを含むアプリケーションとのプロセス間通信を行っています。しかしシグナルは本質的に「非同期」のため、その動作に対するポリシーの違いが重要な問題となりました。
シグナルは同時に並行して動作しているプロセスの間でやりとりが行われるので、タイミングおよび処理内容が重要になります。また、システムコール(例えばread等)を呼び出し、入力待ち状態の時にシグナルが通知された場合、どのような状態が理想的なのかは意見が分かれる所でした。もしプログラム終了シグナルを受けた場合、その前にサスペンドしているシステムコールは中断される必要があります。しかし、プログラム終了以外のシグナルを受けた場合、システムコールは中断したとしても再開される必要があります。
この問題は、一般的に「リエントラント問題」と呼ばれています。リエントラント問題に対する対処は、同じsignal(2)関数、同じインターフェースに関わらず、SysV系OSとBSD系OSでは対応が異なっています。
- SysV系OSの対応
- システムコール(例えばread関数)の処理中にシグナルが到達したらシステムコールは停止。エラー値「-1」を返しerrno変数にはEINTRの値がセットされる。
- シグナルハンドラの処理中に同一のシグナルが到達したらプログラム上でデフォルト動作に戻す事で登録された1回だけが動くようにする。
- BSD系OSの対応
- システムコール(例えばread関数)の処理中にシグナルが到達したらシステムコールはシグナルハンドラの処理待ち。処理が終わればシステムコールの処理を継続。エラー値で戻ることはない。
- シグナルハンドラの処理中に同一のシグナルが到達したら新しいシグナルはOSによって「保留」され現在のシグナルハンドラの処理の終了を待ち、終わったら再度新たにシグナルハンドラが起動する。
これはつまり、汎用的なプログラムを書く場合、そのOS環境がどちらなのか知っておく必要があり、環境に合わせてプログラムを(特にシステムコールまわりを)書き換えなくてはいけない事になります。
Linuxにおいても同じ事が言えます。libc-2になる前はSysV、libc-2以降はBSDで動いています(libcのバージョン番号はrpm -qa | grep glibcと入力する事で確認できます)。
