シグナル (Unix)
シグナル(英: signal)とは、Unix系(POSIX標準に類似の)オペレーティングシステム (OS) における、限定的なプロセス間通信であり、プロセスに対し非同期でイベントの発生を伝える機構である。シグナルが送信された際、OSは宛先プロセスの正常な処理の流れに割り込む。どんな不可分でない処理の間でも割り込むことができる。受信プロセスが以前にシグナルハンドラを登録しておけば、シグナル受信時にそのルーチンが実行される。さもなくば、デフォルトのシグナル処理が行われる。(同様なものは他のTSSなどでも開発されてはいるが、UNIXのシグナルは)1970年ごろベル研究所でUNIXに実装された。後にPOSIXである程度は標準化されているが、標準化が諦められているような振舞などもいくつかあり、特に他の幾つかの要素(fork等)とマルチスレッドとシグナルが絡むと実装毎の対処にプログラミングが大変になることがある。プロセスはI/O待ちなど、カーネルの内部で処理がブロックしている場合などで割り込み不可状態になることがあり、その場合は如何なるシグナルを送っても無効になる。
POSIX準拠のシグナルにあっては、プロセスへシグナルが配送され、シグナルハンドラの実行を開始および終了する際、以下の挙動を保証している。
- シグナルハンドラの実行開始時に、配送されたシグナルのマスク、プロセスがあらかじめ追加指定したシグナルのマスク、およびプロセスが指定したスタックがある場合はそれへの切替をすべてアトミックに実行する。すなわち、シグナルハンドラ実行の準備としてそれらの処理を行っている最中に別のシグナルが配送されても、少なくともシグナルハンドラの実行開始までは別シグナルの処理を行わない。
- シグナルハンドラ実行開始後の挙動は、シグナルマスクの設定に依存する。
- シグナルハンドラが実行を終了すると、実行開始に際して変更したシグナルマスクおよびスタックの変更をシグナル配送前の状態へ巻き戻す。シグナルハンドラ実行開始時と同じく、巻き戻しの処理もアトミックに行う。
これらの仕様は、配送されたシグナルがシグナルマスクの設定に従って確実にシグナルハンドラを起動し、かつ不用意なシグナルハンドラへの再入を防ぐことを目的としている。このため、上記を満たすシグナルの実装を「信頼できるシグナル」と呼ぶことがある。POSIX以前の実装では上記の挙動が一部欠落していたり、アトミックに実行されないため、シグナルハンドラが期待通りに実行されなかったり、シグナルハンドラへの再入設定のためプロセス側で繊細な追加処理が必要になるなどの問題がしばしば生じていた。
シグナル送信
[編集]以下のような操作によりシグナルが送信される。
- ユーザーがあるプロセスの端末のキーを押下したとき、端末がシグナルを発生する。
- kill(2) システムコールを使うと、権限があれば指定したプロセス(群)に指定したシグナルを送信できる。同様に kill(1) コマンドでユーザーがプロセス(群)にシグナルを送信することもできる。また、
raise(3)
を使えばカレントプロセス(またはスレッド)に指定したシグナルを送信できる。 - ゼロ除算 (SIGFPE)、セグメンテーション違反 (SIGSEGV) などの例外によってもシグナルが発生する。経験の浅いプログラマはポインタに不正アドレスを入れてしまい、SIGSEGVを発生させることが多い。これらはデフォルトではプログラムを終了させ、コアダンプを生じる。
- カーネルはプロセスに何らかのイベントを通知するためにシグナルを発生させることができる。例えば、プロセスがパイプに書き込んだとき読み込み側プロセスが既にパイプをクローズしている場合にSIGPIPEが送信される。この場合、デフォルトではそのプロセスは終了となるが、パイプをシェルが構築した場合、終了させるのが最も便利である。
- SIGABRT は、
abort()
の実行により送信される。UNIXであれば一般的なシグナルの範疇で扱うが、abort()
はC89やそれ以前の各種Cライブラリでもサポートされているため、UNIX以外の環境ではOSの代替機能に頼るか、MS-DOSなどOSのサポートが全くない場合はシグナルの機能を純粋にライブラリのみで実装する。
シグナル処理
[編集]signal()
やsigaction()
システムコールは「シグナルハンドラ」を設定するのに使われる。シグナルハンドラが設定されていないシグナルの場合、デフォルトのハンドラが使われる。さもなくば、シグナルは捉えられ、シグナルハンドラが呼び出される。プロセスはハンドラを設定しなくとも2種類のデフォルト動作を指定できる。シグナルを無視するか(SIG_IGN)、デフォルトのハンドラを使うか(SIG_DFL)である。SIGKILLとSIGSTOPは、捉えることもハンドラで処理することもできないシグナルである。
シグナルハンドラが暗黙的に呼び出すシステムコールとしてsigreturn()
がある。これはシグナルハンドラの終了後にカーネルへ処理を戻し、POSIXが要求するシグナルマスクおよびスタックの巻き戻しを実行してから割り込まれたプロセスへ戻ることを目的としている。通常はカーネルがシグナルハンドラを呼び出す際、カーネルがプロセス上にシグナルハンドラのラッパーコードを用意し、シグナルハンドラの終了後に自動的にsigreturn()
を呼び出すよう実装しているため、明示的にsigreturn()
を呼び出す必要はない。なお、シグナルハンドラがlongjmp()
等を用いてシグナル処理開始時とは別の箇所へジャンプする場合は、longjmp()
等がsigreturn()
と等価な処理を行う。
シグナルの重要な挙動として、シグナルを受信可能としてスリープしているプロセスは、一般にシグナルを受信するとスリープを終了する。シグナルハンドラの実行が必要な場合は自然な挙動だが、デフォルト動作によりプロセスが終了する場合も同じ挙動になる。これはUNIXのプロセスはすべて自ら終了するものであり、他のプロセスに終了させられることはない仕様となっていることに依る。ただし、終了するプロセスはユーザモードに戻らないため、カーネルの挙動を無視するとあたかもプロセスがシグナルにより「殺された」ように見える。この挙動による副作用として、シグナルを受信したプロセスのPCBやカーネルスタックなどがスワップアウトされていた場合、それらを再度メモリ上に読み直さなければシグナル受信に起因するプロセス終了処理ができない。カーネルがメモリ不足のためにプロセスへシグナルを送信した場合、この副作用はメモリ需要を一時的ながら増加させてしまうリスクがある。
上記の例外として、以下の場合がある。
- シグナル受信可能としてスリープ中のプロセスに対する
SIGSTOP
。 - 上記により停止し、かつ停止中にスリープが終了しなかったプロセスに対する
SIGCONT
。
いずれの場合も、シグナル受信の影響はプロセスがスリープしている原因が変化するだけであり、プロセスがスケジュールされないことに変わりはない。このため、上記に該当する場合はシグナルを送信したプロセスが送信先プロセスの状態を直接更新し、それをもってシグナル受信処理とする。
問題
[編集]ハンドラによるシグナル処理は競合状態に弱い。シグナルは非同期イベントなので、あるシグナルをハンドラで処理中に別のシグナル(同じ種類ということもある)がそのプロセスに送られてくることがある。このような状態を防ぐため、sigprocmask()
を使ってシグナル配送のブロック/アンブロックが可能である。
シグナルは処理中のシステムコールを中断することがあり、その際にアプリケーションは非透過的な再実行をしなければならない場合がある。この場合、実行中のシステムコールはEINTRというエラーを返し、要求した処理はシグナル受信によって中断されて結果を得られていないことを示す。この場合、処理を続行するには再度同じシステムコールを実行しなければならない。一方、4.2BSDにて、システムコールがシグナル受信のために中断後、ユーザプロセスの介在なく直ちに再開できる場合は内部でエラーERESTARTを返し、シグナルハンドラの実行後システムコールの呼び出し元には戻らず、透過的にシステムコールを継続できるようになった。ERESTARTはカーネル内部でのみ使用されるエラー値であり、ユーザプロセスへは返さない。
シグナルハンドラは通常の処理に割り込んで呼び出されるので、不要な副作用を起こさないように注意が必要である。以下は具体例。
- シグナルハンドラは再入可能な処理のみ行う。特に、malloc、printfといった非同期シグナル安全(async‐signal‐safe)でない関数を使うのは安全ではない[1][2]。
- 再入不可能な処理を実行する場合、それが終了するまでシグナルをマスクし、シグナル受信処理を遅らせる。
- 大域変数 errno の変更など、割り込まれた処理が予期していない変更を含む処理をしない。
- ただし、シグナルマスクやハンドラをシグナルハンドラの実行中に一時的に変更することは可能。シグナルマスクはシグナルハンドラの終了時にカーネル等が暗黙的にリストアする。ハンドラを変更した場合はシグナルハンドラの実行終了時に明示的にリストアする必要がある。
- シグナル受信のフラグはしばしば大域変数として実装する必要があるが、処理を中断しても問題ない箇所でシグナルを受信し、かつシグナルをマスクした上で更新ないしは読み出すのであれば安全な実装となる。
特に、再入不可能な処理の実行中にシグナルを受信し、ハンドラがlongjmp()
等を呼び出した場合、ジャンプ後も再入不可能な処理を実行中の状態となってしまうため、回復が極めて困難になることに注意を要する。これを避けるには再入不可能な処理中のシグナルマスクを適切に設定したり、再入不可能な処理が時間を要する場合は処理を安全に中断できる箇所を作った上でsigwaitinfo()
、sigsuspend()
、sigwait()
、sigtimedwait()
等を呼び出して受信したシグナルを確認および処理する機会を作る必要がある。
例として、複数のファイルを順次更新するバッチ処理を行うソフトウェアを考える。ファイル更新中に処理を中断するとファイルが破損する一方、個々のファイルは独立しているので、あるファイルの処理が終了した直後であれば処理の中断が可能とする。この条件下では、ファイルを1つ処理する毎にシグナル受信の機会を設けることにより、ファイルの破損を防ぐ条件下で迅速にシグナルを処理することができる。各ファイルの処理中はシグナルをマスクし、ファイル破損につながる状況下でのシグナル処理を防ぐ。
ハードウェア例外との関係
[編集]プロセスの実行によってハードウェア例外が発生することがある。例えば、プロセスがゼロ除算を行おうとしたときや、TLBミスを引き起こしたときなどである。Unix系OSでは、ハードウェア例外が発生するとコンテキストを自動的に切り替えてカーネルの例外ハンドラを実行開始する。ページフォールトなどの一部の例外の場合、カーネルはそのイベントを処理するのに十分な情報を持っているので、プロセスの実行を再開させることができる。しかし他の例外ではカーネルはうまく処理できず、代わりに例外を発生させた処理を行っていたプロセスに例外処理を委任しなければならない。シグナルはこの委任のための機構としても機能し、カーネルからプロセスに対してその例外に対応したシグナルを送信する。例えば、x86 CPU でゼロ除算を行おうとした場合 divide error 例外が発生し、カーネルがそのプロセスにSIGFPEというシグナルを送信する。同様に、あるプロセスが自身の仮想アドレス空間の範囲外のメモリアドレスにアクセスしようとした場合、カーネルはSIGSEGVシグナルを送信する。ハードウェア例外の種類はCPUのアーキテクチャによって異なるので、ある例外が発生したときどういうシグナルが送信されるかは厳密にはCPUアーキテクチャやカーネルの実装に依存する。
個々のシグナル
[編集]Single UNIX Specification では、以下のシグナルを <signal.h> で定義すべきものとして指定している。
シグナル受信時のデフォルト動作は以下のような処理がある[3]:
- T: プロセスの異常終了。exit() システムコールを実行したのと同様の終了の仕方だが、wait()またはwaitpid()でそのプロセスの終了を待ち合わせていたプロセスには、シグナル受信で終了したことを示す異常終了コードが返される。
- A: プロセスの異常終了。設定されていればコアダンプを生成する。
- I: シグナルを無視する。
- S: プロセスの実行を中断(一時停止)する。
- C: 中断されていたプロセスの実行を再開する。中断されていないプロセスでは無視する。
下記表のシグナル番号は Linux x86 の場合であり[4]、他のOS・他のCPUでは異なる。Linux ARM もシグナル番号は同じ。
シグナル名 | 説明 | デフォルト動作 | 解説 | Linux x86での シグナル番号[4] |
---|---|---|---|---|
SIGABRT | プロセスが中断された | A | abort()をプロセス自身が実行することによって発生する。ハンドラによるキャッチ可能。ブロック不可。シグナルハンドラから戻るとプロセスは終了させられる。何らかの不正な状況に陥ったが通常の処理の流れでは終了前のクリーンアップ処理ができず、別途トラブルシューティングが必要な場合に使用。 | 6 |
SIGALRM | alarm() によるシグナル | T | alarm()システムコールで、設定した実時間タイマーがタイムアウトしたことを知らせる。 | 14 |
SIGBUS | 「未定義メモリ領域へのアクセス」(SUS) によるバスエラー | A | 以下のような不正なメモリ操作により発生。ハンドラでキャッチできるが、ハンドラから戻った後の動作はシステムに依存する(通常プロセスの終了)。
|
7 |
SIGCHLD | 子プロセスが終了、停止(または再開*)した | I | 子プロセスの状態変化に応じて発生。親プロセスは無視することもできる。 | 17 |
SIGCONT | 停止していれば再開 | C | プロセスグループを参照。 | 18 |
SIGFPE | 浮動小数点例外 -- 「不正な算術操作」(SUS) | A | 浮動小数点演算でゼロ除算やオーバーフローなどが発生したときに発生。Signaling NaNが発生したときも同様。また、整数のオーバーフローなどでも発生する。ハンドラでキャッチ可能だが、適切に処理しないとプロセスがハングアップ(無限ループ)に陥る可能性がある。(ここでいう適切な処理とは、シグナルハンドラが受け取る例外発生時のレジスタの内容を書き換えて例外が発生しないようにすることであり、例外発生箇所のコードを解析してどのレジスタが使われていたのかを調べたり、内容を書き換えることで計算結果が不正にならないか判断したりといった非常に高度な対応が必要とされる。また、Signaling NaN 以外では処理続行はほぼ不可能) | 8 |
SIGHUP | ハングアップ | T | 本来は端末の回線が切れたときに発生。現在では擬似端末をクローズしたときにその端末から起動されたプロセスグループに送られる。シェルの nohup 機能を使えばバックグラウンドのプロセスがSIGHUPを受け付けないようにできる。 | 1 |
SIGILL | 不正命令 | A | 通常、命令でないメモリ領域にジャンプしたときに発生(コールスタックのリターンアドレスが破壊されたときなど)。他に特権レベルが高くないと実行できない命令を実行しようとしたときなどにも発生する。 | 4 |
SIGINT | 割り込み | T | 端末から割り込みキー(通常 CTRL + C)を押下したときに発生。 | 2 |
SIGKILL | 強制終了(kill) | T | killコマンドなどで明示的に発生させる。キャッチしたり無視したりできない。ゾンビプロセスは既に終了しているのでSIGKILLを受けても消滅しない。スリープ中プロセスはスリープ解除されたときにSIGKILLを受け付ける。initはSIGKILLを無視できる。 | 9 |
SIGPIPE | 読み手のいないパイプへの書き込み | T | 13 | |
SIGPOLL*, SIGIO | poll可能イベント | T | ファイル記述子に対してfcntl()システムコールでシグナル発生を指示すると、ポール可能イベントに伴ってSIGPOLLが発生する。一般に通信ポートに対応するファイル識別子に使用し、完全な非同期I/Oを実現する。ただし、コードが複雑化することから、最近では専用の非同期I/Oシステムコールを使用することが推奨されている。 | 29 |
SIGPROF* | プロファイリングタイマーがタイムアウト | T | プロファイラで使用。このときのタイマーはプロセスの全実行時間に対応する(CPUモードに関わらず計時する)。 | 27 |
SIGPWR | 電源喪失 | T | 30 | |
SIGQUIT | 終了とコアダンプ | A | 通常、端末からの終了キー(CTRL + \)で発生。 | 3 |
SIGSEGV | セグメンテーション違反 | A | ページフォールトのうち不正なメモリアクセスによるものの場合に発生。しかし、例えばヒープ領域の拡張をSIGSEGV発生を受けてオンデマンドで行うライブラリなどもある。 | 11 |
SIGSTKFLT | 数値演算プロセッサにおけるスタックフォルト | A | 16 | |
SIGSTOP | 実行中断 | S | プロセスグループの実行中断のためのシグナル。キャッチも無視もできない。SIGCONT シグナルで実行再開する。 | 19 |
SIGSYS*, SIGUNUSED | 不正 システムコール | A | 一般にシステムコールの番号や引数が不正だったときは単にエラーを返すことで済むので、このシグナルは滅多に使われない。 | 31 |
SIGTERM | 強制終了 | T | killコマンドがデフォルトで発生するシグナル。しかし、このシグナルをキャッチしたり無視したりすることも可能。 | 15 |
SIGTRAP* | トレース/ブレークポイントによるトラップ | A | 何らかのデバッグ機能で使用される(アーキテクチャによって異なる)。 | 5 |
SIGTSTP | 端末からの中断シグナル | S | フォアグラウンドのプロセスグループを実行中断させる(通常、CTRL + Z 押下)。 | 20 |
SIGTTIN | バックグラウンドプロセスが端末から読もうとした | S | バックグラウンドのプロセスグループがユーザー入力待ちとなって停止。シェルの機能を使ってフォアグラウンドにすることで入力が可能。 | 21 |
SIGTTOU | バックグラウンドプロセスが端末に書き込もうとした | S | バックグラウンドのプロセスグループが端末への表示待ちとなって停止。シェルの機能を使ってフォアグラウンドにすることで表示可能。 | 22 |
SIGURG | ソケット上に緊急データ(TCPの帯域外データ)がある | I | 非同期I/O機能で使用。 | 23 |
SIGUSR1 | ユーザー定義シグナル 1 | T | POSIXでは未定義。たとえばddコマンドが受信すると操作の状況が表示される。 | 10 |
SIGUSR2 | ユーザー定義シグナル 2 | T | POSIXでは未定義。 | 12 |
SIGVTALRM* | 仮想時間をカウントするタイマによるシグナル -- 「仮想タイマのタイムアウト」(SUS) | T | プロファイラなどで使用。このときのタイマーはプロセスのユーザーモードでの実行時間を計時するもの。SIGPROFと組み合わせると、プロセスのカーネルモードでの実行時間がわかる。 | 26 |
SIGWINCH | ウィンドウ リサイズ シグナル | I | xtermなどサイズが可変な制御端末にて、端末サイズが変更されたことを知らせる。テキストベースのGUIを実装したソフトウェアにて、端末サイズの変更に合わせてUIを再描画するために用いる。 | 28 |
SIGXCPU* | CPU時間制限を越えた | A | プロセス実行時間(スリープしていた時間やスケジューリング待ち時間は除く)がある値を超えると発生。 | 24 |
SIGXFSZ* | ファイルサイズ制限を越えた | A | ファイルサイズがファイルシステム(あるいはオペレーティングシステム)の制限を越えようとしたとき、それを引き起こしたプロセスに送信される。 | 25 |
注:アスタリスク付の項目は、X/Open System Interfaces (XSI) による拡張を示す。(SUS) とある部分はSUS[3]にある表現の引用(を和訳したもの)。
上述以外に、プロセスは擬似シグナル(番号0)を送信することもできる。これは実際にはシグナルを送信せずにシグナル送信時のエラーチェックをし、例えば宛先プロセスが存在するかどうかをチェックするのに便利である。
仕様の歴史
[編集]プロセスのシグナル処理を設定するためのAPIであるsignal()
が初めて実装されたのはAT&T UNIX V4である。この頃のシグナルは端末からの簡単なプロセス制御(主に手動によるプロセス終了)を目的としていた。シグナルマスクはサポートされておらず、シグナルハンドラの再入防止のためにシグナルハンドラの実行開始時にシグナル処理をデフォルト動作へ戻す仕様となっていた。シグナルハンドラへの再入を可能とするためには、シグナルハンドラ内でシグナルハンドラの再設定が必要だった。この頃のシグナルの仕様は、以下の4.2BSDにおけるシグナル実装以降「信頼できないシグナル」と呼ばれている。
シグナルがより汎用的なプロセス通信および制御に利用されるようになると、シグナルハンドラの再設定にて生じる競合が問題となった。最初の改善として、SVR3は原始的なシグナルマスクを実装し、ある一つのシグナルのマスクおよび解除、さらにシグナルマスクの操作とシグナル待機のアトミックな実行をサポートした。続いて4.2BSDは複数のシグナルに対するまとまったシグナルマスク操作およびシグナルハンドラ内での複数のシグナルマスクを実装し、プロセス制御のために複数種のシグナルを安全に使えるようにした。また、シグナルハンドラにおける専用スタックやプロセスグループ宛のシグナル送信、シグナル受信後の透過的なシステムコール再開もサポートし、単一ユーザスレッドにおける「信頼できるシグナル」のセマンティクスを固めた。一方で、SVR3、4.2BSDのシグナルAPIは相互の互換性、旧実装との後方互換性のいずれも欠けており、移植性の問題を起こした。
移植性の問題はSVR4にて、sigaction()
を中心とした形でAPIを整理することにより解決した。signal()
を含めた旧実装との後方互換性は、sigaction()
のオプション機能、後方互換性のためのシステムコールやライブラリ関数としての再実装により吸収した。その後、シグナル標準化の作業はPOSIXへ引き継がれ、マルチスレッドにおけるシグナル仕様の拡張などはPOSIXが行った。
脚注
[編集]- ^ “SIG30-C. シグナルハンドラ内では非同期安全な関数のみを呼び出す”. JPCERT/CC セキュアコーディングスタンダード. JPCERT/CC. 2019年2月11日閲覧。
- ^ 脚注前項のリンク先記述には「
longjmp()
もPOSIXのsiglongjmp()
も、シグナルハンドラ内から呼び出してはならない。」とあるが、これは無条件に成立するものではなく、割り込まれた処理に依存するので不正確である。リンク先記述にあるサンプルコードは静的変数へのアクセスがあるためシグナルに対して安全ではないとしているが、適合コード例の他に、シグナルをマスクした上でその静的変数へアクセスすればlongjmp()
を残したままにすることができる。POSIX.1-2008 TC2はlongjmp()
およびsiglongjmp()
を、async-signal-safeではない関数や処理へシグナルが割り込んだ場合のリスクにかかる注意を添えた上でasync-signal-safeな関数リストに追加している。 - ^ a b “signal.h”. IEEE Std 1003.1, 2004 Edition. 2012年7月10日閲覧。
- ^ a b
signal(7)
– JM Project Linux Overview, Conventions and Miscellanea マニュアル
関連項目
[編集]外部リンク
[編集]signal(7)
– JM Project Linux Overview, Conventions and Miscellanea マニュアル- システムプログラム(第5週) 筑波大学講義
- Introduction to Unix Signals Programming
- Another Introduction to Unix Signals Programming
- UNIX and Reliable POSIX Signals by Baris Simsek
- Signal Handlers by Henning Brauer