🌕

【まとめ】signal(シグナル)について

2022/03/08に公開約9,000字

シグナルと何か?

プロセス間通信の一種だ。

UNIXは、非同期イベントをプロセスに通知するためにシグナルを使用。

シグナルというのは、イベントが発生するとプログラムの実行部分に関係なく割り込んで来るので、「ソフトウェア割り込み」と呼ばれることもある。

例えば、ユーザが Ctrl-C を端末から打つと、現在実行中のプロセスに対して、 SIGINTシグナルが送られる。

同様にプロセスが終了すると、その親に対して SIGCHLD シグナルが送られる。

シグナルを使うには大きく分けて、signal関数とsigaction関数の2つの関数がある。

signal関数よりもsigaction関数のほうが、機能や移植性から見ても有用で、利用を推奨。

signal関数の使い方

signal関数は
signal(シグナルの種類, シグナルハンドラとしたい関数)
という形で使われる。

シグナルが発生した際、シグナルハンドラが呼び出される。

用語解説

用語 意味
シグナルハンドラ プロセスにシグナルを送信すると、そのプロセスの正常処理に割り込んで、シグナル固有の処理(シグナルハンドラ) が実行される。
PID プロセスID(process ID。
プロセスIDとは、オペレーティング(OS)が現在実行中のプロセスを識別するために割り当てる識別子。WindowsやUNIX系OSなどで利用され、一般的には整数の通し番号。
シグナルマスク 「〇〇のシグナルハンドラの実行中は、××のシグナルは無視する」っていう時に便利な機能。
シグナルというのはイベントが起きるとプログラムの実行部分に関係なく割り込んで来るので、それを防ぐことができる。

サンプルコード

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>

void handler1(int signal)
{}

void handler2(int signal)
{
	write(1, "6", 1);
	alarm(5);
	write(1, "7", 1);
}

int main()
{
	write(1, "1", 1)	
	signal(SIGINT, handler1);
	write(1, "2", 1);
	signal(SIGALRM, handler2);
	write(1, "3", 1);
	alarm(2);
	write(1, "4", 1);
	while(1) {}
	write(1, "5", 1);
	return 0;
}

出力結果

1234

と表示された後に、67が5秒おきに出力される。

signal(SIGINT, handler1);は、SIGINTがCtrl + cを表しており、Ctrl + cが押された際に、本来はプロセスが終了させられるが、その代わりにhandler1関数を呼び出す。

signal(SIGALRM, handler2);は、alarm(2);がSIGALRMシグナルを2秒後に発信するので、それを受けてhandler2関数を呼び出す。

プログラムを終了する場合は、Ctrl + zを入力し、Ctrl + zは「EOFコード」を意味する。

sigaction関数とは

特定のシグナルを受信した際の プロセスの動作を変更するのに使用。

sigaction使用の流れ

①struct sigaction型の構造体変数saを宣言

struct 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); → 廃止予定で使用すべきではない。
};

sigset_t型のオブジェクト

sigset_t 型のオブジェクトは、関数 sigaddset(), sigdelset(), sigismember() や後述の glibc の追加関数 (sigisemptyset(), sigandset(), sigorset()) に渡す前に、 sigemptyset() か sigfillset() を呼び出して初期化しなければならない。

初期化しなかった場合の結果は未定義である。

Data Type: sigset_t
The sigset_t data type is used to represent a signal set. Internally, it may be implemented as either an integer or structure type.

For portability, use only the functions described in this section to initialize, change, and retrieve information from sigset_t objects—don’t try to manipulate them directly.

※アーキテクチャによっては共用体 (union) が用いられている場合、 sa_handler と sa_sigaction の両方を同時に割り当てることはできない。

sa_handler

signum に対応する動作を指定するもので、 デフォルトの動作を行う SIG_DFL 、そのシグナルを無視する SIG_IGN 、シグナルハンドラ関数へのポインタが設定可能。

シグナルハンドラ関数の引き数は一つであり、シグナル番号が引き数として渡される。

sa_flags に SA_SIGINFO(下のsa_flagsの項で説明) が指定された場合、 ( sa_handler ではなく) sa_sigaction により signum に対応するシグナルハンドル関数が指定される。

指定される関数は、最初の引数としてシグナル番号を、 二番目の引数として siginfo_t へのポインタを、三番目の引数として (void * にキャストした) ucontext_t へのポインタを受けとる。

sa_mask

シグナルハンドラ実行中に禁止 (block) するシグナルのマスクを表す。

さらに、 SA_NODEFER (下のsa_flagsの項で説明) フラグが指定されていない場合は、ハンドラを起動するきっかけとなる シグナルにも sa_mask が適用される。

シグナルはシグナルハンドラの処理中に他のシグナルのシグナルハンドラを呼び出すことができるので、これが問題となることがある。

sa_mask要素は、sigaction関数の第一引数で与えられたシグナルのシグナルハンドラ実行中に無視(block)するシグナルのマスクを表す。

これが設定できるのはSIG_IGN、SIG_DEL以外のシグナルハンドラ。

例えばSIGINTで呼び出されたシグナルハンドラ処理中に、新たにSIGINTが到着した場合、新しく到着したSIGINTを無視する。これはデフォルト。

sa_maskはひとつが一種類のシグナルを扱うboolen型のフラグの集合として実装されており、このフラグセットの操作は次の4つの関数を用いる。

int sigemptyset(sigset_t *set)
    /* setの全フラグをセット */

int sigfillset(sigset_t *set)
     /* setの全フラグをリセット */

int sigaddset(sigset_t *set, int signum)
     /* signumで指定したフラグを個別にセット */

int sigdelset(sigset_t *set, int signum)
     /* signumで指定したフラグを個別にリセット */

sa_flags

sa_flagsはシグナル・ハンドラの動作を変更するためのフラグの集合を指定する。 sa_flags には、以下に示すフラグの (0個以上の) 論理和をとったものを指定する。

SA_NOCLDSTOP

signum が SIGCHLD の場合、 子プロセスが停止したり (子プロセスが SIGSTOP , SIGTSTP , SIGTTIN , SIGTTOU を受けたとき) 再開したり (子プロセスが SIGCONT を受けたとき) したときに SIGCHLD の通知を受けない。

SA_NOCLDWAIT

(Linux 2.6 以降) signum が SIGCHLD の場合、子プロセスが終了したときに 子プロセスをゾンビプロセスに変化させない ( waitpid (2)も参照)。

SA_RESETHAND

シグナルハンドラが呼ばれる度に、シグナルの動作をデフォルトに戻す。 SA_ONESHOT はこのフラグと同じ意味だが、廃止されており、非標準である。

SA_ONSTACK

sigaltstack (2) で提供される別のシグナル・スタックでシグナルハンドラを呼び出す。 別のシグナル・スタックが利用可能でなければ、デフォルトのスタックが 使用される。

SA_RESTART

いくつかのシステムコールをシグナルの通知の前後で再開できるようにして、 BSD シグナル方式 (semantics) と互換性のある動作を提供する。

SA_NODEFER

それ自身のシグナル・ハンドラーの内部にいる時でも そのシグナルをマスクしないようにする。

SA_NOMASK はこのフラグと同じ意味だが、廃止されており、非標準である。

SA_SIGINFO

シグナルハンドラは一つではなく、三つの引き数を持つ。

この場合は sa_handler のかわりに sa_sigaction を設定しなければならない ( sa_sigaction フィールドは Linux 2.1.86 で追加)。

sigaction構造体のsa_flagsメンバがSA_SIGINFOになってたら、シグナルハンドラ関数の第2引数に使われる。

sa_sigaction のパラメータ siginfo_t は以下の要素を持つ構造体。

siginfo_t {
	int	si_signo;	/* Signal number */
	int	si_errno;	/* An errno value */
	int	si_code;	/* Signal code */
	pid_t	si_pid;	/* Sending process ID */
	uid_t	si_uid;	/* Real user ID of sending process */
	int	si_status;	/* Exit value or signal */
	clock_t	si_utime;	/* User time consumed */
	clock_t	si_stime;	/* System time consumed */
	sigval_t	si_value;	/* Signal value */
	int	si_int;	/* POSIX.1b signal */
	void *	si_ptr;	/* POSIX.1b signal */
	void *	si_addr;	/* Memory location which caused fault */
	int	si_band;	/* Band event */
	int	si_fd;	/* File descriptor */
}

kill(2) や sigqueue(3) で送信されたシグナルでは si_pid と si_uid が設定される。

さらに、 sigqueue(3) で送信されたシグナルでは si_int と si_pid にシグナルの送信者により指定された値が設定される。詳細は sigqueue(3) を参照。

pid_tとはプロセスIDを表わす型。実装により、intであったりlongであったりする。

実装

struct sigaction型の構造体変数saを宣言しておく。

struct sigaction sa;

②sigemptyset(&sa.sa_mask)でsaのシグナルマスクをクリアする

sigemptyset()を使うと、シグナルマスクをゼロクリア(マスクの設定は特になし)にすることができる。
引数は、struct sigaction型の構造体変数のメンバsa_maskのアドレスを渡す。

sigemptyset(&sa.sa_mask)

シグナルがセットのメンバであり正常に実行された場合、sigemptyset() は 1 を返す。

それ以外は 0 を返す。

③sa.sa_handlerにシグナルハンドラにしたい関数を割り当てる

シグナルハンドラにしたい関数をfunction()ならば、struct sigaction型の構造体saのメンバsa_handlerにfunctionを割り当てる。

sa.sa_handler = function;

④sa.sa_flagsにフラグを設定する

フラグは、シグナルの詳細な情報が欲しい時などに、SA_SIGINFOを指定したりする。

とりあえず0でOK。

sa.sa_flags = 0;

サンプルコード

#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>

int count;

void signal_handler(int signum)
{
	count += 100;
	write(1, "8", 1);
}

int main(void)
{

	struct sigaction sa;
	
	write(1, "1", 1);
	if (-1 == sigemptyset(&sa.sa_mask))
	{
		write(1, "2", 1);
		exit(1);
	}
	write(1, "3", 1);
	sa.sa_handler = signal_handler;
	write(1, "4", 1);
	sa.sa_flags = 0;
	write(1, "5", 1);
	if (-1 == sigaction(SIGINT, &sa, NULL))
	{
		write(1, "6", 1);
		exit(1);
	}
	while(count < 50)
	{}
	write(1, "7", 1);
	return 0;
}

出力結果

1345

と表示された後に、Ctrl + cを入力すると、^C87と出力される。

if (-1 == sigemptyset(&sa.sa_mask))
/* シグナルマスクのクリア(エラーチェック付き) */

/* シグナルハンドラの登録(エラーチェック付き) */ 
if (-1 == sigaction(SIGINT, &sa, NULL))

SIGUSR1とSIGUSR2

ユーザ定義のシグナルで、プログラムによって意味が異なる。

SIGUSR1 と SIGUSR2 の2つは、アプリケーションで自由に使える。

sig_atomic_t

アトミックにアクセス可能なオブジェクトの整数型。

アトミックにアクセスできる(他の処理が割り込むことができない)オブジェクトが持つ型で、何らかの整数型。

volatile で修飾されているかもしれない。

格納できる値の範囲は、SIG_ATOMIC_MIN と SIG_ATOMIC_MAX で定義されている。

関連するその他の関数

getpid関数

getpid() は、自分のプロセスIDを戻り値として返す
getpid() は呼び出し元のプロセスのプロセス ID を返す。(テンポラリ用のファイル名として 他と
getpid ()は現在のプロセスのプロセス ID を返す。
(テンポラリ用のファイル名として 他と重ならない名前を生成するルーチンでしばしば使用される。)

名前が似た関数にgetppid関数が存在する。

getppid ()は現在のプロセスの親プロセスのプロセス ID を返す。
重ならない名前を生成するルーチンでしばしば使用される。)
getppid() は呼び出し元のプロセスの親プロセスのプロセス ID を返す。

sleep関数

sleep関数は処理を指定した時間だけ止める。

usleep関数

usleep関数は秒よりも細かく、マイクロ秒単位で処理を止める。

マイクロ秒というのは、1/1000000秒で、μsとも書く。

つまり、1s = 1,000,000 μs。

pause関数

pauseはシグナルを待つ。

p呼び出したプロセス (またはスレッド) を、 そのプロセスを終了させたり、シグナル捕捉関数が起動されるようなシグナルが配送されるまで、スリープさせる。

返り値は、シグナルを受け取りシグナル捕獲関数から返った場合に-1を返し、 errno に EINTR が設定される。

kill関数

kill - プロセスにシグナルを送る

任意のプロセスグループまたはプロセスにシグナルを送ることができる。第一引数に指定されたプロセスpidに第二引数のsigが送られる。pidに0を指定した場合は、呼び出し元のプロセスグループすべてに送られる。pidに0を指定した場合シグナルは送られないが、エラーチェックは行われる。

返り値
成功した場合は0、失敗した場合には-1が返されerrnoにエラー内容が格納される。
エラー内容は以下のとおり

名前 意味
SINVAL 無効なシグナル
EPERM シグナルを送る許可を持っていない
ESRCH 指定したプロセスまたはプロセスグループが存在しなかった

exit関数

プログラムを終了するときに、exit関数を利用する

exit関数は、プログラムを実行した側にプログラムの成功や失敗を伝える一つの手段を提供。

必要なヘッダーはstdlib.h。

exit関数は、慣例的に、以下のように使われる。

正常終了:exit(0);
異常終了:exit(1);
慣例的に、1以上は異常終了。

0や1以外の値を利用することで、なぜ失敗したのか?を使えることも可能。

fork関数

呼び出し元のプロセスをコピーして新たなプロセスを生成。

コピーして新たなプロセスを生成するため、変数やポインタなどもそのまま使用することが可能。

呼び出し元のプロセスを親プロセス、生成されたプロセスを子プロセスと呼ぶ。

その他

無限ループからでる方法

Ctrl-cでカレントフォアグランドプロセスに割り込みシグナルを送る。

Ctrl-zでカレントフォアグランドプロセスに中断シグナルを送る。
※中断した場合にはfgでコマンドで再開可能。

ビット演算

演算子 種類 意味 ビットごとの操作内容
~ NOT 論理否定(ビット反転) 1と0を逆にする(反転)
& AND 論理積 どちらかが0なら0
OR 論理和 どちらかが1なら1
^ XOR 排他的論理和 両方同じなら0,違っていたら1

シフト演算

2進数をあらわすビット列を左または右にずらす操作のこと

PSコマンド

psコマンドの-aオプションで、現在実行しているプロセスを確認できる

ps -a

ビット表記

符号あり(signed)型の場合、左端のビットが符号を司る。

// uint8_t
0b00000000 //    0
0b10000000 //  128
0b11111111 //  255

// int8_t
0b01111111 //  127
0b10000000 // -128
0b10000001 // -127
0b10000010 // -126
//
0b11111101 //   -3
0b11111110 //   -2
0b11111111 //   -1

Discussion

ログインするとコメントできます