電子・情報工学系 追川 修一 <shui @ cs.tsukuba.ac.jp>
このページは,次の URL にあります.
http://www.coins.tsukuba.ac.jp/~syspro/2005/No5.html
システムプログラムのホームページ(2005年度)
http://www.coins.tsukuba.ac.jp/~syspro/2005/
からもリンクが張ってあります.
プロセスは,オペレーティングシステムがコンピュータ(プロセッサ)を抽象化し,使いやすくしたものである. コンピュータは入出力機器からのイベント通知を割り込みという仕組みで受け取る. プロセスには,割り込みに相当するイベント通知のメカニズム(ソフトウェア割り込み)として,シグナルが提供されている.
割り込みは入出力機器からのイベント通知のために考え出されたものである. 入出力機器はコンピュータにI/Oコントローラを通して接続されている. 以下の図は単純化したコンピュータの構成を示している.
コンピュータは,に加え,キーボード,マウス,グラフィックスディスプレイ,HDD,ネットワークインタフェースなどの入出力機器(I/Oデバイス)からなる. 入出力機器はI/Oコントローラを通して制御される. プロセッサ,メインメモリ,I/Oコントローラはシステムバスに接続されており,システムバスを通してお互いにデータをやり取りしている.
I/Oコントローラは,システムバスと入出力機器の間でデータの橋渡しをする. 入出力機器は様々な種類のコンピュータに対応するため,標準的なインタフェースを提供している. キーボードやマウスにはPS/2(最近ではUSB)というインタフェースがよく使われている. HDDはATA,SATAやSCSIというインタフェースがよく使われている. 一方,システムバスはプロセッサ固有のものである. 例えば Apple Macintosh に使われている PowerPC というプロセッサのシステムバスと,Intel Pentium 4 のシステムバスは全く異なるものである. Intel Pentium 4 と称されていても,いくつかの異なるシステムバスが使用されている. 入出力機器の標準的なインタフェースに対応するように,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デバイスはプロセッサの処理速度と比較すると非常に遅いため,ポーリングで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(7) Linux Programmer's Manual SIGNAL(7) 名前 signal - 有効なシグナルの一覧 説明 Linux がサポートするシグナルの一覧を以下に示す。シグナル番号の幾つかは アーキティクチャ依存である。最初に、POSIX.1に定義されているシグナルを示す。 シグナル 値 動作 コメント -------------------------------------------------------------------------- SIGHUP 1 A 制御しているターミナル(controlling terminal) のハングアップを検出した。あるいは、制御して いるプロセス(controlling process)が死んだ。 SIGINT 2 A キーボードからの割り込み(Interrupt) SIGQUIT 3 A キーボードによる中止(Quit) SIGILL 4 A 不当な命令 SIGABRT 6 C abort(3)からの中断(Abort)信号 SIGFPE 8 C 浮動小数点例外 SIGKILL 9 AEF Killシグナル SIGSEGV 11 C 不当なメモリ参照 SIGPIPE 13 A パイプ破壊: 読み手の無いパイプへの書き出し SIGALRM 14 A alarm(1)からのタイマーシグナル ... "動作" の欄の記号は以下のような意味を持つ。 A デフォルトの動作は、プロセスの終了 (terminate the process)。 B デフォルトの動作は、このシグナルの無視( ignore the signal)。 C デフォルトの動作は、プロセスの終了とコアダンプ(dump core)。 D デフォルトの動作は、プロセスの一旦停止 (stop the process)。 E キャッチできないシグナル。 F 無視できないシグナル ...
SIGQUIT 3 C Quit from keyboard SIGILL 4 C Illegal Instruction
日本語のマニュアルページの情報は古かったり,誤訳で意味が不明であったりすることがあるため,そのような場合は英語のマニュアルページを参照すべきである. 英語のマニュアルページは以下のようにすると表示させることができる.
% env LANG=C man 7 signal
signal システムコールは,シグナルをサポートするために最初に作られたシステムコールである.
typedef void (*sighandler_t)(int); sighandler_t signal(int signum, sighandler_t sighandler);
シグナルハンドラを変更するシグナルと新しいシグナルハンドラを引数に指定して呼び出すと,古いシグナルハンドラが返される. signal システムコールの引数と戻り値の型は sighandler_t であり,これは int 型を引数に取り値を返さない(つまり void型の)関数へのポインタ(アドレス)という意味である. 即ち,シグナルハンドラは以下のようにプロトタイプ宣言されるような関数ということになり,このような関数へのポインタが,signal システムコールの引数や戻り値になる. シグナルハンドラの引数は,送られてきたシグナルの番号である.
void signal_handler(int);
古いシグナルハンドラの値としては次が返されることもある:
シグナルと共に使われるシステムコールに pause がある. pause を呼び出すとシグナルを受け取るまでプロセスの実行をブロックする. シグナルを受け取ると,シグナルハンドラの実行後に,pauseから戻る.
int pause(void);
以下は signal と pause を使ったプログラム例である.
1 #include <stdio.h> 2 #include <signal.h> 3 4 int sigint_count = 3; 5 6 void 7 sigint_handler(int signum) 8 { 9 printf("sigint_handler(%d): sigint_count(%d)\n", 10 signum, sigint_count); 11 12 if (--sigint_count <= 0) { 13 printf("sigint_handler: exiting ... \n"); 14 exit(1); 15 } 16 17 #if 0 /* For the original System V signal */ 18 signal(SIGINT, &sigint_handler); 19 #endif 20 } 21 22 main() 23 { 24 signal(SIGINT, &sigint_handler); 25 26 for (;;) { 27 printf("main: sigint_count(%d), calling pause ....\n", 28 sigint_count); 29 30 pause(); 31 32 printf("main: return from pause. sigint_count(%d)\n", 33 sigint_count); 34 } 35 }
コンパイル,実行すると以下のようになる. Ctrl-C は表示されない. 送られてきたシグナルの番号がシグナルハンドラ sigint_handler の引数になるため,SIGINT の番号である 2 が sigint_handler の引数として渡されているのがわかる.
% ./a.out main: sigint_count(3), calling pause .... sigint_handler(2): sigint_count(3) main: return from pause. sigint_count(2) main: sigint_count(2), calling pause .... sigint_handler(2): sigint_count(2) main: return from pause. sigint_count(1) main: sigint_count(1), calling pause .... sigint_handler(2): sigint_count(1) sigint_handler: exiting ... %
signal システムコールの最初の(しかしながら System V 系の UNIX まで継承された)仕様は,多くの問題を持っていた.
例えば read システムコールを呼び出し,入力待ちの状態でブロックしている時に,シグナルが通知された場合,シグナルは処理され,そのシグナルに対応するシグナルハンドラが起動される. この場合,ブロック状態になっていたシステムコールはキャンセルされ EINTR というエラーが返されるようになっていた.
あるシグナルに対応したシグナルハンドラを実行中に,また同じシグナルが通知された場合,同じシグナルハンドラを起動してしまうと,データの一貫性などに問題が生じてしまう. そのため,シグナルハンドラは一度呼び出されるとデフォルトの動作にリセットされるようになっていた. そのため,シグナルハンドラが起動される度に signal システムコールでシグナルハンドラを設定し直す必要があった(上記プログラムの17〜19行目).
しかしながら,この方法ではデフォルトの動作がプロセスの終了であった場合,シグナルハンドラを実行中に同じシグナルを受けたらプロセスが終了してしまうことになる.
そこでシグナルを無視するようにシグナルハンドラの先頭で SIG_IGN を設定すれば良いように思われるがが,この場合は
System V とは別の系列のUNIXにBSD (Berkeley Software Distribution) というものがあった. BSD UNIXでは上記のような問題に対し signal の動作を変えることで対応しようとした.
システムコールはキャンセルされない. シグナルハンドラ実行後に,システムコールの実行は継続される(ブロック状態に戻る).
シグナルハンドラの実行中に同じシグナルが通知されたら,そのシグナルは保留される. 現在実行中のシグナルハンドラ処理の終了を待ち,もう一度シグナルハンドラを起動する. シグナルハンドラはリセットされない.
signal システムコールの混乱状態を解決するために,POSIX では sigaction という新しいシステムコールを導入した. sigaction で導入されたシグナルのことを,旧来のシグナルメカニズムと区別するためにPOSIXシグナルと呼ばれることがある. POSIX準拠のシステムであれば sigaction を使用すべきであり,signal はもはや使用すべきではない.
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
sigaction システムコールは,第1引数にシグナル番号,第2引数に設定するシグナルの動作を指定して呼び出すと,第3引数に古い設定が返される.
上記の問題点におけるPOSIXシグナルのデフォルトの動作は次のようになっている.
古い signal と同じく,システムコールはキャンセルされ EINTR というエラーが返される.
この点ついてはBSD UNIXと同じでる. シグナルハンドラの実行中に同じシグナルが通知されたら,そのシグナルは保留され,現在実行中のシグナルハンドラ処理の終了を待ち,もう一度シグナルハンドラを起動する. また,シグナルハンドラはリセットされない.
シグナルの動作は以下の 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); }
古い signal のプログラムを,sigaction で sa_handler を使うように変更すると以下のようになる. 基本的には全く同じであるが,若干異なっている.
1 #include <stdio.h> 2 #include <signal.h> 3 4 int sigint_count = 3; 5 6 void 7 sigint_handler(int signum) 8 { 9 printf("sigint_handler(%d): sigint_count(%d)\n", 10 signum, sigint_count); 11 12 if (--sigint_count <= 0) { 13 printf("sigint_handler: exiting ... \n"); 14 exit(1); 15 } 16 } 17 18 main() 19 { 20 struct sigaction sa_sigint; 21 22 memset(&sa_sigint, 0, sizeof(sa_sigint)); 23 sa_sigint.sa_handler = sigint_handler; 24 sa_sigint.sa_flags = SA_RESTART; 25 26 if (sigaction(SIGINT, &sa_sigint, NULL) < 0) { 27 perror("sigaction"); 28 exit(1); 29 } 30 31 for (;;) { 32 printf("main: sigint_count(%d), calling pause ....\n", 33 sigint_count); 34 35 pause(); 36 37 printf("main: return from pause. sigint_count(%d)\n", 38 sigint_count); 39 } 40 }
上記プログラムの実行結果は,古い signal を用いた場合と同一であるので,省略.
同じプログラムを,sigaction で sa_sigaction を使うように変更したプログラムは以下のようになる.
1 #include <stdio.h> 2 #include <signal.h> 3 4 int sigint_count = 3; 5 6 void 7 sigint_action(int signum, siginfo_t *info, void *ctx) 8 { 9 printf("sigint_handler(%d): sigint_count(%d) signo(%d) code(0x%x)\n", 10 signum, sigint_count, info->si_signo, info->si_code); 11 12 if (--sigint_count <= 0) { 13 printf("sigint_handler: exiting ... \n"); 14 exit(1); 15 } 16 } 17 18 main() 19 { 20 struct sigaction sa_sigint; 21 22 memset(&sa_sigint, 0, sizeof(sa_sigint)); 23 sa_sigint.sa_sigaction = sigint_action; 24 sa_sigint.sa_flags = SA_RESTART | SA_SIGINFO; 25 26 if (sigaction(SIGINT, &sa_sigint, NULL) < 0) { 27 perror("sigaction"); 28 exit(1); 29 } 30 31 while (1) { 32 printf("main: sigint_count(%d), calling pause ....\n", 33 sigint_count ); 34 35 pause(); 36 37 printf("main: return from pause. sigint_count(%d)\n", 38 sigint_count ); 39 } 40 }
コンパイル,実行すると以下のようになる. Ctrl-C は表示されない. 通知されたシグナルについてより多くの情報を引数として受け取れるが,その詳細については SIGACTION(2) を参照.
% ./a.out main: sigint_count(3), calling pause .... sigint_handler(2): sigint_count(3) signo(2) code(0x80) main: return from pause. sigint_count(2) main: sigint_count(2), calling pause .... sigint_handler(2): sigint_count(2) signo(2) code(0x80) main: return from pause. sigint_count(1) main: sigint_count(1), calling pause .... sigint_handler(2): sigint_count(1) signo(2) code(0x80) sigint_handler: exiting ... %シグナルの無視
受け取りたくないシグナルは無視することができる. その場合シグナルハンドラに SIG_IGN を設定する.
1 #include <stdio.h> 2 #include <signal.h> 3 4 int sigint_count = 3; 5 6 main() 7 { 8 struct sigaction sa_ignore; 9 10 memset(&sa_ignore, 0, sizeof(sa_ignore)); 11 sa_ignore.sa_handler = SIG_IGN; 12 13 if (sigaction(SIGINT, &sa_ignore, NULL) < 0) { 14 perror("sigaction"); 15 exit(1); 16 } 17 18 while (1) { 19 printf("main: sigint_count(%d), calling pause() ....\n", 20 sigint_count ); 21 22 pause(); 23 24 printf("main: return from pause(). sigint_count(%d)\n", 25 sigint_count ); 26 } 27 }
コンパイル,実行すると以下のようになる. Ctrl-C, Ctrl-Z は表示されない.
% ./a.out main: sigint_count(3), calling pause() .... Suspended % jobs [1] + Suspended ./a.out % kill %1 % [1] Terminated ./a.out
以下のように,SIG_IGN の値は 1 であり,またデフォルトの動作を表す SIG_DFL の値は 0 である. sigaction は古い設定を返すが,シグナルを無視またはデフォルトの設定の場合は,これらの値が sa_handler に入ってくる.
#define SIG_DFL ((__sighandler_t) 0) /* Default action. */ #define SIG_IGN ((__sighandler_t) 1) /* Ignore signal. */
プロセスへシグナルを送るためには kill システムコールを使用する. 引数に送り先のプロセスIDと送るシグナルを指定する.
int kill(pid_t pid, int sig);
以下は,引数にプロセスIDを指定し,そのプロセスに対し SIGINT を送るプログラムである.
1 #include <stdio.h> 2 #include <signal.h> 3 #include <sys/types.h> 4 5 main(int argc, char *argv[]) 6 { 7 pid_t pid; 8 9 if (argc != 2) { 10 printf("Usage: %s pid\n", argv[0]); 11 exit(1); 12 } 13 14 pid = atoi(argv[1]); 15 16 if (pid <= 0) { 17 printf("Invalid pid: %d\n", pid); 18 exit(1); 19 } 20 21 if (kill(pid, SIGINT) < 0) { 22 perror("kill"); 23 exit(1); 24 } 25 }
ある一定時間ごとに割り込みを発生させ,現在の処理を継続しながら別の処理,例えばプロンプトを点滅させたり,アニメーションを実行したり,又は割り込みをサポートしていないデバイスに対しポーリングを行ったりというようなこと,がしたい場合にはインターバルタイマという機能を使用する. インターバルタイマは setitimer システムコールを用いて設定できる.
int setitimer(int which, const struct itimerval *value, struct itimerval *ovalue);
インターバルタイマは第2引数で指定した時間が経つ(残り時間が 0 になる)とシグナルで通知するが,どのような時間なのかを,第1引数の which で以下のタイマーから1つを設定する.
第2引数で時間を指定するために使用する struct itimerval とその中で使われている struct timeval の定義は以下のようになっている.
struct itimerval { struct timeval it_interval; /* next value */ struct timeval it_value; /* current value */ }; struct timeval { long tv_sec; /* seconds */ long tv_usec; /* microseconds */ };
it_value が 0 になると,which で指定されるタイマに対応するシグナルが通知される. そして,it_interval に値が次の時間として設定される. it_value, it_interval どちらの値も 0 になったタイマは停止する.
以下は,ITIMER_REAL で実際の時間で 1 秒ごとにシグナルを受け,10回シグナルを受け取ると終了するプログラムである.
1 #include <stdio.h> 2 #include <signal.h> 3 #include <time.h> 4 #include <sys/time.h> 5 6 void alrm() 7 { 8 printf("alrm: %d\n", time(NULL)); 9 } 10 11 main() 12 { 13 struct sigaction sa_alarm; 14 struct itimerval itimer; 15 int counter = 10; 16 17 memset(&sa_alarm, 0, sizeof(sa_alarm)); 18 sa_alarm.sa_handler = alrm; 19 sa_alarm.sa_flags = SA_RESTART; 20 21 if (sigaction(SIGALRM, &sa_alarm, NULL) < 0) { 22 perror("sigaction"); 23 exit(1); 24 } 25 26 itimer.it_value.tv_sec = itimer.it_interval.tv_sec = 1; 27 itimer.it_value.tv_usec = itimer.it_interval.tv_usec = 0; 28 29 if (setitimer(ITIMER_REAL, &itimer, NULL) < 0) { 30 perror("setitimer"); 31 exit(1); 32 } 33 34 while (counter--) { 35 pause(); 36 printf("main: %d\n", time(NULL)); 37 } 38 }
コンパイル,実行するといかのようになる.
% ./a.out alrm: 1115995143 main: 1115995143 alrm: 1115995144 main: 1115995144 alrm: 1115995145 main: 1115995145 alrm: 1115995146 main: 1115995146 alrm: 1115995147 main: 1115995147 alrm: 1115995148 main: 1115995148 alrm: 1115995149 main: 1115995149 alrm: 1115995150 main: 1115995150 alrm: 1115995151 main: 1115995151 alrm: 1115995152 main: 1115995152 %
kill システムコールでシグナルを送るプログラムとシグナルを受け取るプログラムを使用して,プロセスからプロセスへシグナルが送られるのを確かめなさい.
ヒント:端末ウィンドウを2つ開き,1つでシグナルを受け取るプログラムを動かし,もう1つでシグナルを送るプログラムを動かす. シグナルを送る相手のプロセスのIDは ps コマンドで調べるか,プログラムを変更して自分のプロセスIDを表示させるようにする.
複数の異なるシグナルに対しシグナルハンドラを設定し,シグナルに応じたシグナルハンドラが起動されることを確かめなさい.
不正なメモリアクセスは SIGSEGV により通知される. どのアドレスに対してアクセスがあったのかを表示するプログラムを作成しなさい.
ヒント:SIGACTION(2) の siginfo_t の情報を参照.
sleep は実はライブラリ関数である. sleep 相当の機能を持つ関数 mysleep を作りなさい.
ヒント:setitimer, sigaction, pause を使用する.
キーボードから1文字入力を読み込む関数 getchar にタイムアウト機能を追加した関数 mygetchar を作りなさい. mygetchar は,ある一定時間内にキー入力があればそれを返し,なければ -2 を返すような関数であるとする. (-2 なのは EOF が -1 であるため). タイムアウト時間は予め決められた時間(例えば10秒)でもよいし,引数で指定できるようにしても良い.
ヒント:SA_RESTART はどういう意味か?
練習問題(29)と同じように,親プロセスと子プロセスが交互に文字を出力するプログラムを,シグナルを用いて書きなさい. fork で作る子プロセスは1つだけとする.