理解を深めるために
プロセスは,オペレーティングシステムがコンピュータ(プロセッサ)を抽象化し,使いやすくしたものである. コンピュータは入出力機器からのイベント通知を割り込みという仕組みで受け取る. プロセスには,割り込みに相当するイベント通知のメカニズム(ソフトウェア割り込み)として,シグナルが提供されている.
割り込みは入出力機器からのイベント通知のために考え出されたものである. 入出力機器はコンピュータに I/O コントローラを通して接続されている. 以下の図は単純化したコンピュータの構成を示している.
コンピュータは,プロセッサとメモリに加え,キーボード,マウス,ディスプレイ,ディスク,ネットワークインタフェースなどの入出力機器(I/Oデバイス)からなる. 入出力機器は I/O コントローラを通して制御される. プロセッサ,メインメモリ,I/O コントローラはシステムバスに接続されており,システムバスを通してお互いにデータをやり取りしている.
I/O コントローラは,システムバスと入出力機器の間でデータの橋渡しをする. 入出力機器は様々な種類のコンピュータに対応するために,標準的なインタフェースを提供している. PC のキーボードやマウスには PS/2 というインタフェースがよく使われていたが,最近では USB がよく使われている. ディスクには SATA や SCSI というインタフェースがよく使われている. 一方,システムバスはプロセッサ固有のものである. 入出力機器の標準的なインタフェースに対応するように,I/O コントローラはプロセッサとのセットで使われるように開発される.
システムバスは以下のバスから構成される.
プロセッサは I/O コントローラに入出力要求を出し,I/O コントローラはその要求を解釈し,I/O デバイスに伝える. I/O デバイスは入出力要求を処理し,結果を I/O コントローラに転送し,プロセッサは I/O コントローラからその結果を受け取る. 下図は,この処理の流れを図示したものである.
入出力処理が終了したかどうかをプロセッサが知るための単純な方法は,プロセッサが I/O コントローラの状態をチェックし続けるというものである. ただしこの方法では,I/O コントローラ,I/O デバイスが入出力を処理している間は,プロセッサでの処理を続けられないため待ち時間ができてしまう. このような I/O デバイスへの入出力要求の処理方法をポーリング (polling) と呼ぶ.
通常,I/O デバイスの処理速度はプロセッサの処理速度と比べて非常に遅いため,ポーリングで入出力処理の終了を待っているとプロセッサの使用率を著しく下げてしまう. また,端末からの入力待ちなどの,ユーザからの入力を待つような処理では,複数の端末からの入力に対する応答性を確保することが難しい.
割り込み (interrupt) は,ポーリングの持つ非効率性を解決するため,入出力処理が終了したことをプロセッサに対し通知するために考え出された方法である. I/O コントローラは,I/O デバイスとの入出力処理が終了したら,プロセッサに対し割り込み要求を出す. プロセッサは割り込み要求を受け付けると,現在実行中の処理を中断し,割り込みを処理するために予め設定されたプログラムを呼び出し,実行する. この割り込み処理のためのプログラムを,割り込みハンドラとか割り込みサービスルーチン (ISR: Interrupt Service Routine) などと呼ぶ.
■ 割り込みは時間依存の処理を可能にする
割り込みが考え出されてから,時間に依存した処理もできるようになった. 時間を刻むタイマデバイスから一定周期で(例えば1秒ごとに)割り込みがかかるようにすれば,そのタイミングでプロセスの切り替えをしたり,ポーリングを行ったりすることができるようになる. 割り込みにより,特定のプロセスや特定のデバイスにかかりきりになることなく,公平に有効にプロセッサを分配活用できるようになった.
■ 関数呼び出しは同期処理,割り込みは非同期処理
割り込み処理は本来のプログラムの処理の流れとは無関係に,非同期的 (asynchronous) に発生する. 一方,関数呼び出しは同期的 (synchronous) であり,明示的に関数を呼び出し,呼び出された関数で処理が行われ,関数呼び出しから戻ってくる. 下図は,関数呼び出しと割り込み処理の違いを図示したものである.
ある関数における処理から別の関数(割り込み処理ハンドラもプログラムの中では関数として定義される)が呼び出されるという処理の流れだけを見ると,関数呼び出しも割り込み処理も違いはない. しかし,関数呼び出しの方には関数を呼び出す命令が入っているのに対し,割り込み処理の方では割り込まれたプログラムが割り込みハンドラを呼び出す命令は一切ない. いつどこで割り込まれるかわからないのが割り込みの特徴である.
OS カーネルでの処理にいつでも割り込まれてしまうのでは,データの一貫性を保つことができないので,プロセッサには割り込みを禁止する命令が用意されている.
割り込みとは別に,アクセスが許可されていない番地にアクセスしようとしたとか,0による除算をしようとしたとか,不正な命令(例えば特権が必要な命令)を実行しようとしたとかの理由で,プログラムの実行中にそのプログラムに原因が起因するエラーが起こることがある. このようなエラーのことを例外 (exception) と呼ぶ. 例外が発生した場合,OS カーネルの例外ハンドラ(例外処理のためのプログラム)が起動され対処する.
割り込みと例外が似ている点は,OS カーネルの割り込みハンドラまたは例外ハンドラが起動されるという点,そしてこれらのハンドラを呼び出す命令がプログラム中には含まれていないという点である. 異なっているのは,割り込みハンドラが起動されるのはデバイスなどプログラム外部が要因であるのに対し,例外ハンドラが起動されるのはプログラムの実行が要因であるという点である.
ハードウェアからの割り込みや例外は OS カーネルにより処理される. ハードウェアを直接操作できるのは OS カーネルだけであるので,通常のユーザプロセスがハードウェアからの割り込みを直接受けることはない. 例外は,その要因となったプログラムが処理することが可能な場合もあるが,プログラムに共通する処理も多いため,通常 OS カーネルにより処理される.
しかし,割り込みや例外の概念はユーザプロセスとして動作するプログラムにも便利である. ある一定時間ごとに割り込みを発生させることができれば,現在の処理を継続しながら別の処理(例えばプロンプトを点滅させたり,アニメーションを実行したり,割り込みをサポートしていないデバイスに対しポーリングを行ったりというようなこと)ができる. また,非同期的に命令を送りそれに応じた処理を行わせるためにも,割り込みの概念は使用できる. 例外が起こった場合に,そのイベントに対するプログラム独自の対処をしたい場合もある.
UNIXではプロセッサとメモリというコンピュータの基本的な部分はプロセスとして抽象化されているが,同じように割り込みおよび例外を抽象化したのがシグナル (signal) である. 割り込みハンドラに相当するのがシグナルハンドラである. 一般的に使用される割り込みや例外の種類に応じたシグナルが OS カーネルにより定義されている. プロセスはそれぞれのシグナルの種類に対しシグナルハンドラを OS カーネルに登録することができる. プロセスは,実行中のプログラムに起因する例外や,プロセス外部からのイベントをシグナルとして受け取る. シグナルが受け取られると,OS カーネルに登録したシグナルハンドラが起動される.
プロセス外部からのイベントとしてよく使用されているのが,キーボードから Ctrl-C や Ctrl-Z によるプロセス実行の中止または中断である. これらのキーボードからの入力は,まず OS カーネルに含まれるデバイスを制御するデバイスドライバにより処理され,後にプロセスにシグナルとして通知される.
SIGNAL(3) BSD Library Functions Manual SIGNAL(3) NAME signal -- simplified software signal facilities LIBRARY Standard C Library (libc, -lc) SYNOPSIS #include <signal.h> void (*signal(int sig, void (*func)(int)))(int); or in the equivalent but easier to read typedef'd version: typedef void (*sig_t) (int); sig_t signal(int sig, sig_t func); DESCRIPTION This signal() facility is a simplified interface to the more general sigaction(2) facility. .... 一部省略 .... No Name Default Action Description 1 SIGHUP terminate process terminal line hangup 2 SIGINT terminate process interrupt program 3 SIGQUIT create core image quit program 4 SIGILL create core image illegal instruction 5 SIGTRAP create core image trace trap 6 SIGABRT create core image abort program (formerly SIGIOT) 7 SIGEMT create core image emulate instruction executed 8 SIGFPE create core image floating-point exception 9 SIGKILL terminate process kill program 10 SIGBUS create core image bus error 11 SIGSEGV create core image segmentation violation 12 SIGSYS create core image non-existent system call invoked 13 SIGPIPE terminate process write on a pipe with no reader 14 SIGALRM terminate process real-time timer expired 15 SIGTERM terminate process software termination signal 16 SIGURG discard signal urgent condition present on socket 17 SIGSTOP stop process stop (cannot be caught or ignored) 18 SIGTSTP stop process stop signal generated from keyboard 19 SIGCONT discard signal continue after stop 20 SIGCHLD discard signal child status has changed 21 SIGTTIN stop process background read attempted from control terminal 22 SIGTTOU stop process background write attempted to control terminal 23 SIGIO discard signal I/O is possible on a descriptor (see fcntl(2)) 24 SIGXCPU terminate process cpu time limit exceeded (see setrlimit(2)) 25 SIGXFSZ terminate process file size limit exceeded (see setrlimit(2)) 26 SIGVTALRM terminate process virtual time alarm (see setitimer(2)) 27 SIGPROF terminate process profiling timer alarm (see setitimer(2)) 28 SIGWINCH discard signal Window size change 29 SIGINFO discard signal status request from keyboard 30 SIGUSR1 terminate process User defined signal 1 31 SIGUSR2 terminate process User defined signal 2 The sig argument specifies which signal was received. The func procedure allows a .... 以下省略 ....
signal システムコールは,シグナルをサポートするために最初に作られたシステムコールである.
typedef void (*sig_t)(int); sig_t signal(int sig, sig_t func);
シグナルハンドラを変更するシグナルと新しいシグナルハンドラを引数に指定して呼び出すと,古いシグナルハンドラが返される. signal システムコールの第2引数と戻り値の型は sig_t であり,これは int 型を引数に受け取って値を返さない(つまり void型の)関数へのポインタ(関数のアドレス)の型である. すなわち,シグナルハンドラは以下のように宣言される関数であり,このような型の関数へのポインタが,signal システムコールの引数や戻り値になる. シグナルハンドラの引数は,送られてきたシグナルの番号である.
void signal_handler(int);
古いシグナルハンドラの値として以下の値が返されることもある:
シグナルと共に使われるシステムコールに pause がある. pause を呼び出したプロセスはシグナルを受け取るまで実行を停止する. シグナルを受け取ると,シグナルハンドラの実行後に pause からリターンする.
int pause(void);
以下は signal と pause を使ったプログラム例である.
1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <unistd.h> 4 #include <signal.h> 5 6 volatile sig_atomic_t sigint_count = 3; 7 8 void sigint_handler(int signum) 9 { 10 printf("sigint_handler: signum(%d), sigint_count(%d)\n", 11 signum, sigint_count); 12 13 if (--sigint_count <= 0) { 14 printf("sigint_handler: exiting ...\n"); 15 exit(1); 16 } 17 18 #if 0 /* For the original System V signal */ 19 signal(SIGINT, &sigint_handler); 20 #endif 21 } 22 23 int main(void) 24 { 25 signal(SIGINT, &sigint_handler); 26 27 for (;;) { 28 printf("main: sigint_count(%d), calling pause ...\n", 29 sigint_count); 30 31 pause(); 32 33 printf("main: returned from pause. sigint_count(%d)\n", 34 sigint_count); 35 } 36 37 return 0; 38 }
6行目の volatile sig_atomic_t の volatile という単語は見慣れないかもしれないが,その説明は後述する.
コンパイル,実行すると以下のようになる. Ctrl-C は ^C として表示される. 送られてきたシグナルの番号がシグナルハンドラ sigint_handler の引数になるため,SIGINT の番号である2が sigint_handler の引数として渡されているのがわかる.
$ ./a.outmain: sigint_count(3), calling pause ...
^Csigint_handler: signum(2), sigint_count(3) main: returned from pause. sigint_count(2) main: sigint_count(2), calling pause ...
^Csigint_handler: signum(2), sigint_count(2) main: returned from pause. sigint_count(1) main: sigint_count(1), calling pause ...
^Csigint_handler: signum(2), sigint_count(1) sigint_handler: exiting ... $
signal システムコールの最初の(しかしながら System V 系の UNIX まで継承された)仕様は,多くの問題を持っていた.
例えば read システムコールを呼び出し,入力待ちの状態で停止している間にシグナルが通知された場合,そのシグナルに対応するシグナルハンドラが起動される. この場合,その read システムコールはキャンセルされ EINTR というエラーが返される.
あるシグナルに対応したシグナルハンドラを実行中に,また同じシグナルが通知された場合,同じシグナルハンドラを起動すると,データの一貫性などに問題が生じることがある. そのため,シグナルハンドラは一度呼び出されるとデフォルトの動作にリセットされるようになっていた. そのため,シグナルハンドラが起動されるたびに signal システムコールでシグナルハンドラを設定し直す必要があった(上記のプログラムの18~20行目).
しかし,この方法ではデフォルトの動作がプロセスの終了であった場合,シグナルハンドラを実行中に同じシグナルを受けたらプロセスが終了してしまうことになる.
そこで,シグナルハンドラの実行中にそのシグナルを通知されたくない場合には,シグナルハンドラの先頭で,そのシグナルを無視するようにする(SIG_IGN をシグナルハンドラとして設定する)ようにすればよいように思われるが,この場合は
という問題がある.
System V とは別の系列の UNIX に BSD (Berkeley Software Distribution) というものがある. BSD UNIX では上記のような問題に対し signal の動作を変えることで対応しようとした.
システムコールはキャンセルされない. シグナルハンドラ実行後に,システムコールの実行は継続される(停止状態に戻る).
シグナルハンドラの実行中に同じシグナルが通知されたら,そのシグナルは保留される. 現在実行中のシグナルハンドラ処理の終了を待ち,終了後にもう一度シグナルハンドラを起動する. シグナルハンドラはリセットされない.
BSD UNIX の変更は機能するものであったが,System V UNIX の古い signal と同じ API を用いたため,混乱の原因となった. macOS は,マニュアルに書かれているように BSD UNIX ベースであるため,この signal の API を提供している. そのため上記のプログラムの18~20行目は不要になっている.
Linux カーネルが提供する signal システムコールは System V の API を持っている. しかし,glibc 2 以降では signal ライブラリ関数は signal システムコールを呼び出さず, 代わりに BSD UNIX と同じ動作をするようなフラグを与えて sigaction システムコールを呼び出している. これについては signal(2) マニュアルで説明されている. 結局,Linux でも macOS と同じく,上記のプログラムの18~20行目は不要である.
signal システムコールの混乱状態を解決するために,POSIX では sigaction という新しいシステムコールを導入した. sigaction で導入されたシグナルは,旧来のメカニズムのシグナルと区別するために POSIX シグナルと呼ばれることがある. POSIX 準拠のシステムであれば sigaction を使用すべきであり,signal はもはや使用すべきではない.
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
sigaction システムコールは,第1引数にシグナル番号,第2引数に設定するシグナルの動作を指定して呼び出す.sigaction システムコールは戻り値としてエラーコードを返し,第3引数にシグナルの古い設定を返す.
上記の問題点における POSIX シグナルのデフォルトの動作は次のようになっている.
この点については古い signal と同じである.システムコールはキャンセルされ EINTR というエラーが返される.
この点については BSD UNIX と同じである. シグナルハンドラの実行中に同じシグナルが通知されたら,そのシグナルは保留される.現在実行中のシグナルハンドラ処理の終了を待ち,終了後にもう一度シグナルハンドラを起動する. また,シグナルハンドラはリセットされない.
シグナルの動作は以下の struct sigaction を用いて設定する.
struct sigaction { union __sigaction_u __sigaction_u; /* signal handler */ sigset_t sa_mask; /* signal mask to apply */ int sa_flags; /* see signal options below */ }; union __sigaction_u { void (*__sa_handler)(int); void (*__sa_sigaction)(int, struct __siginfo *, void *); }; #define sa_handler __sigaction_u.__sa_handler #define sa_sigaction __sigaction_u.__sa_sigaction
古い signal のプログラムを,sigaction で sa_handler を使うように変更すると以下のようなプログラムになる. 基本的には全く同じであるが,若干異なっている.
1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <unistd.h> 4 #include <signal.h> 5 #include <string.h> 6 7 volatile sig_atomic_t sigint_count = 3; 8 9 void sigint_handler(int signum) 10 { 11 printf("sigint_handler: signum(%d), sigint_count(%d)\n", 12 signum, sigint_count); 13 14 if (--sigint_count <= 0) { 15 printf("sigint_handler: exiting ...\n"); 16 exit(1); 17 } 18 } 19 20 int main(void) 21 { 22 struct sigaction sa_sigint; 23 24 memset(&sa_sigint, 0, sizeof(sa_sigint)); 25 sa_sigint.sa_handler = sigint_handler; 26 sa_sigint.sa_flags = SA_RESTART; 27 28 if (sigaction(SIGINT, &sa_sigint, NULL) < 0) { 29 perror("sigaction"); 30 exit(1); 31 } 32 33 for (;;) { 34 printf("main: sigint_count(%d), calling pause ...\n", 35 sigint_count); 36 37 pause(); 38 39 printf("main: returned from pause. sigint_count(%d)\n", 40 sigint_count); 41 } 42 43 return 0; 44 }
上記のプログラムの実行結果は,古い signal を用いた場合と同一であるので,省略.
同じプログラムを,sigaction で sa_sigaction を使うように変更したプログラムは以下のようになる.
sigaction2.c1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <unistd.h> 4 #include <signal.h> 5 #include <string.h> 6 7 volatile sig_atomic_t sigint_count = 3; 8 9 void sigint_action(int signum, siginfo_t *info, void *ctx) 10 { 11 printf("sigint_handler(%d): sigint_count(%d) signum(%d) code(0x%x)\n", 12 signum, sigint_count, info->si_signo, info->si_code); 13 14 if (--sigint_count <= 0) { 15 printf("sigint_handler: exiting ...\n"); 16 exit(1); 17 } 18 } 19 20 int main(void) 21 { 22 struct sigaction sa_sigint; 23 24 memset(&sa_sigint, 0, sizeof(sa_sigint)); 25 sa_sigint.sa_sigaction = sigint_action; 26 sa_sigint.sa_flags = SA_RESTART | SA_SIGINFO; 27 28 if (sigaction(SIGINT, &sa_sigint, NULL) < 0) { 29 perror("sigaction"); 30 exit(1); 31 } 32 33 while (1) { 34 printf("main: sigint_count(%d), calling pause ...\n", 35 sigint_count); 36 37 pause(); 38 39 printf("main: returned from pause. sigint_count(%d)\n", 40 sigint_count); 41 } 42 43 return 0; 44 }
コンパイル,実行すると以下のようになる. Ctrl-C は ^C として表示される. 通知されたシグナルについて,より多くの情報を引数として受け取れる.その詳細については sigaction のマニュアルを参照.
$ ./a.outmain: sigint_count(3), calling pause ...
^Csigint_handler(2): sigint_count(3) signum(2) code(0x0) main: returned from pause. sigint_count(2) main: sigint_count(2), calling pause ...
^Csigint_handler(2): sigint_count(2) signum(2) code(0x0) main: returned from pause. sigint_count(1) main: sigint_count(1), calling pause ...
^Csigint_handler(2): sigint_count(1) signum(2) code(0x0) sigint_handler: exiting ... $
システムコールやライブラリ関数の実行がシグナルハンドラに割り込まれた場合,シグナルハンドラからのリターン後にそのシステムコールの実行が再開されるかリターンするかは,OSごとに厳密に仕様で定められている.その際に sigaction の引数の SA_RESTART フラグが及ぼす作用も,その仕様で定められている.また,再開かリターンかの挙動は,実行しているシステムコールやライブラリ関数の種類に依存している.仕様はかなり複雑になっている.このあたりの動きが重要になるプログラムの開発では,ぜひ signal(7) のマニュアルの該当部分("Interruption of system calls and library functions by signal handlers"の部分)を熟読してほしい.
なお通常,getchar() ライブラリ関数は内部で最終的には標準入力(端末)に対して read システムコールを実行する.よって,割り込まれた後の挙動も,端末に対する read システムコールのそれとなる.
また,その部分では,pause() システムコールは,割り込まれたら SA_RESTART フラグがあってもなくても必ずリターンする(再開されない)と記述されている.
受け取りたくないシグナルは無視することができる. その場合シグナルハンドラに SIG_IGN を設定する.
sigaction3.c1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <unistd.h> 4 #include <signal.h> 5 #include <string.h> 6 7 volatile sig_atomic_t sigint_count = 3; 8 9 int main(void) 10 { 11 struct sigaction sa_ignore; 12 13 memset(&sa_ignore, 0, sizeof(sa_ignore)); 14 sa_ignore.sa_handler = SIG_IGN; 15 16 if (sigaction(SIGINT, &sa_ignore, NULL) < 0) { 17 perror("sigaction"); 18 exit(1); 19 } 20 21 while (1) { 22 printf("main: sigint_count(%d), calling pause ...\n", 23 sigint_count); 24 25 pause(); 26 27 printf("main: returned from pause. sigint_count(%d)\n", 28 sigint_count); 29 } 30 31 return 0; 32 }
コンパイル,実行すると以下のようになる. Ctrl-C は ^C,Ctrl-Z は ^Z として表示される.
$ ./a.outmain: sigint_count(3), calling pause ...
^C
^C
^C
^Z [1]+ Stopped ./a.out $ jobs
[1]+ Stopped ./a.out $ kill -KILL %1
[1]+ Stopped ./a.out $
[1]+ Killed: 9 ./a.out $
以下のように,SIG_IGN の値は1である.また,デフォルトの動作を表す SIG_DFL の値は0である. sigaction は第3引数で指定された構造体にシグナルの古い設定を書き込むが,古い設定が無視やデフォルトであった場合は,これらの値がそこに書き込まれることになる.
#define SIG_DFL (void (*)(int))0 #define SIG_IGN (void (*)(int))1
プロセスへシグナルを送るためには kill システムコールを使用する. 引数に送り先のプロセス ID と送るシグナルを指定する.
int kill(pid_t pid, int sig);
以下は,コマンドライン引数にプロセス ID を受け取り,そのプロセスに対し SIGINT を送るプログラムである.
kill.c1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <signal.h> 4 5 int main(int argc, char *argv[]) 6 { 7 pid_t pid; 8 9 if (argc != 2) { 10 fprintf(stderr, "Usage: %s pid\n", argv[0]); 11 exit(1); 12 } 13 14 pid = atoi(argv[1]); 15 16 if (pid <= 0) { 17 fprintf(stderr, "Invalid pid: %d\n", pid); 18 exit(1); 19 } 20 21 if (kill(pid, SIGINT) < 0) { 22 perror("kill"); 23 exit(1); 24 } 25 26 return 0; 27 }
Ctrl-C を3回入力すると終了する上記の sigaction2.c で,シグナルハンドラ内で値が変更される変数は,以下のように宣言されている.
volatile sig_atomic_t sigint_count = 3;
sig_atomic_t は,割り込まれずに書き込みが可能な変数の型である. volatile は型修飾子と呼ばれるもので,型の前に付けて使用する. volatile を付けることによって,コンパイラによる最適化が抑止され,sigint_count に対する読み書きは,必ずメモリに対して行なわれるようになる(sigint_count がレジスタに割り当てられることがなくなる).
このような宣言が必要な理由は,シグナルハンドラの呼び出しが,関数呼び出しのようにプログラムの制御の流れに沿ったものではなく,プログラム外部のイベントに起因し非同期的に起こるものだからである. プログラムの制御の流れに沿っている場合,変数への読み書きはコンパイラの制御下にあるため,最適化により一時的にレジスタに置かれている変数の値が正しく参照される. しかし,シグナルハンドラは,本来のプログラムの制御の流れとは無関係に,プログラムの実行に割り込んで呼び出される. 最適化により変数の値が一時的にレジスタに置かれているところにシグナルハンドラの呼び出しが起こった場合,シグナルハンドラはその最新の値にアクセスすることはできない. また,シグナルハンドラが変数の値を書き換えたとしても,割り込まれたプログラムはその変更に気がつかないこともあり得る.
volatile と宣言された変数は必ずメモリに割り当てられるため,どういうタイミングでシグナルによる割り込みが起きても,必ず最新の値を参照することができる. また,sig_atomic_t は割り込まれずに書き込みが可能な変数の型を提供するため,変数の読み書きの途中で割り込まれることがない. 例えば,値を変数の下位バイトには書き込んだが上位バイトにまだ書き込んでいない瞬間に割り込まれることがない. そのため,一貫性のある値を読み書きすることができる.
シグナルハンドラは,本来のプログラムの制御の流れに割り込んで呼び出されるため,プログラムの処理に異常をもたらす場合もある. シグナルハンドラ内で呼び出すと異常をもたらすライブラリ関数やシステムコールは数多くあるため,シグナルハンドラ内で使用可能なライブラリ関数やシステムコールは制限されている. 例えば,printf はシグナルハンドラ内で使用可能ではない(外部サイトの参考資料). write は使用可能なので,メッセージを出力する必要がある場合には write を用いる. シグナルハンドラ内で使用可能な関数のリストについては sigaction のマニュアルを参照のこと.
ある一定時間ごとにシグナルを発生させ,現在の処理を継続しながら別の処理(例えばプロンプトを点滅させたり,アニメーションを実行したり,割り込みをサポートしていないデバイスに対しポーリングを行ったりというようなこと)がしたい場合にはインターバルタイマという機能が使える. インターバルタイマは setitimer システムコールを用いて設定できる.
int setitimer(int which, const struct itimerval *value, struct itimerval *ovalue);
インターバルタイマは第2引数で指定した時間が経つ(残り時間が0になる)とシグナルで通知する. 時間経過の計測方法について,以下の中から1つを第1引数で設定する.
第2引数で時間を指定するために使用する型 struct itimerval とその中で使われている型 struct timeval の定義は以下のようになっている.
struct itimerval { struct timeval it_interval; /* timer interval */ struct timeval it_value; /* current value */ }; struct timeval { time_t tv_sec; /* seconds since Jan. 1, 1970 */ suseconds_t tv_usec; /* and microseconds */ };
it_value に与えられた時間が経過すると,第1引数で指定されたシグナルが通知される. そして,it_interval の値が次の残り時間(it_value)として設定される. it_value が0に設定されたタイマは無効になる(時間計測およびシグナルの通知を停止する).
以下は,ITIMER_REAL を指定した setitimer によって作られるインターバルタイマにより,実際の時間で1秒ごとにシグナルを受け,10回シグナルを受け取ると終了するプログラムである. 10回シグナルを受け取ったら it_value と it_interval に0を設定し,タイマを無効にしている(その後すぐにプログラムが終了するので,無効化する意義はないが).
itimer.c1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <unistd.h> 4 #include <signal.h> 5 #include <string.h> 6 #include <sys/time.h> 7 8 volatile sig_atomic_t alrm_count = 10; 9 10 void alrm(int signum) 11 { 12 alrm_count--; 13 } 14 15 int main(void) 16 { 17 struct sigaction sa_alarm; 18 struct itimerval itimer; 19 20 memset(&sa_alarm, 0, sizeof(sa_alarm)); 21 sa_alarm.sa_handler = alrm; 22 sa_alarm.sa_flags = SA_RESTART; 23 24 if (sigaction(SIGALRM, &sa_alarm, NULL) < 0) { 25 perror("sigaction"); 26 exit(1); 27 } 28 29 itimer.it_value.tv_sec = itimer.it_interval.tv_sec = 1; 30 itimer.it_value.tv_usec = itimer.it_interval.tv_usec = 0; 31 32 if (setitimer(ITIMER_REAL, &itimer, NULL) < 0) { 33 perror("setitimer"); 34 exit(1); 35 } 36 37 while (alrm_count) { 38 pause(); 39 printf("%d: %d\n", alrm_count, time(NULL)); 40 } 41 42 itimer.it_value.tv_sec = itimer.it_interval.tv_sec = 0; 43 itimer.it_value.tv_usec = itimer.it_interval.tv_usec = 0; 44 45 if (setitimer(ITIMER_REAL, &itimer, NULL) < 0) { 46 perror("setitimer"); 47 exit(1); 48 } 49 50 return 0; 51 }
コンパイル,実行すると以下のようになる.time(NULL) によって取得される時刻が1秒ずつ増えていくことがわかる.
$ ./a.out9: 1487757391 8: 1487757392 7: 1487757393 6: 1487757394 5: 1487757395 4: 1487757396 3: 1487757397 2: 1487757398 1: 1487757399 0: 1487757400 $
2008年版の POSIX の仕様書に書かれた getitimer, setitimer のマニュアルによると,getitimer と setitimer は廃止予定であり,timer_gettime, timer_settime を使うことが推奨されているようである. timer_gettime と timer_settime は,この演習で使用している Linux サーバの環境では使えるが,macOS の環境では使えない.
kill システムコールでシグナルを送るプログラム kill.c と,シグナルを受け取るプログラム sigaction1.c を使用して,プロセスからプロセスへシグナルが送られるのを確かめなさい.
ヒント: 端末ウィンドウを2つ開き,片方のウィンドウでシグナルを受け取るプログラムを動かし,もう片方のウィンドウでシグナルを送るプログラムを動かす. シグナルを受け取るプロセスのIDは ps コマンドで調べるか,シグナルを受け取るプログラムを変更して自分のプロセスIDを表示させるようにする.
複数の異なるシグナルに対してシグナルハンドラを設定し,シグナルに応じたシグナルハンドラが起動されることを確かめなさい.
ヒント: シグナルは様々な方法で発生させることができる. kill コマンドや kill システムコールを使えば,好きなシグナルを好きなプロセスに送信することができる. キー入力や GUI 操作でシグナルを発生させることもできる. 例えば Ctrl-Z を入力すると SIGTSTP シグナルを発生させることができ,端末のウィンドウのサイズを変えると SIGWINCH シグナルを発生させることができる.
OS はプロセスによる様々な不正な処理の実行に対してシグナルを送信する. たとえば,この演習で使用している Linux は(そして macOS も),不正なメモリアクセスを行ったプロセスに SIGSEGV(segmentation violation, セグメンテーション違反)シグナルを送信し, ゼロによる整数除算を試みたプロセスに SIGFPE(floating point exception, 浮動小数点数例外)シグナルを送信する. 以下のプログラムを作成しなさい.
本来はシグナルハンドラの中で printf を呼び出してはいけないが,この問題に対する解答のプログラムでは呼び出してもよいものとする. もちろん,printf ではなく write などの呼び出してもよい関数を使うプログラムにするほうが望ましい.
メモ: 少なくともこの演習の計算機環境の Linux サーバでは整数のゼロ除算に対しても SIGFPE(浮動小数点数例外)シグナルが送信される. さらになんと,浮動小数点数のゼロ除算に対しては,浮動小数点数に関するシグナルは送信されない.
sleep は実はライブラリ関数である. シグナルの機能を用いて,sleep 相当の機能を持つ関数 mysleep を作りなさい.
mysleep は sleep と同じ戻り値を返すようにし,さらに,それを確かめる実行例を含めるようにすること. 現在時刻は time 関数により取得できる. 使用が終わり不要になったシグナルの設定はキャンセルし,もとの設定に戻すこと. nanosleep システムコールは使用しないこと. 細かい仕様が気になる人のために言っておくと,mysleep はリエントラント(再入可能)である必要はない.
ヒント: 現在時刻は time 関数や gettimeofday 関数によって取得できる. Linux では clock_gettime 関数の利用が推奨されている.
キーボードから1文字入力を読み込む関数 getchar にタイムアウト機能を追加した関数 mygetchar を作りなさい.
細かい仕様が気になる人のために言っておくと,mygetchar はリエントラント(再入可能)である必要はない.
ヒント: シグナルの設定に用いるフラグに注意しよう.
ヒント: Ctrl-D によって,端末からの入力の終わりを意味するデータすなわち EOF を入力することができる.
ヒント: Ctrl-C によって,フォアグラウンドのプロセスに SIGINT シグナルを送信することができる.
ヒント: getchar はファイルや端末からの入力の終わりの場合にも,シグナルなどにより文字の読み込みに失敗した場合にも,EOF を返す.
どちらの場合による EOF かを判定するためには,feof 関数や ferror 関数を使う.
練習問題(407)と同じように,親プロセスと子プロセスが交互に実行することにより,交互に1文字出力するプログラムを,シグナルを用いて書きなさい. fork で作る子プロセスは1つだけとする.