シグナル (Unix)
シグナル(signal)とは、Unix系(POSIX標準に類似の)オペレーティングシステム (OS) における、限定的なプロセス間通信の形式を使って、プロセスに対し、非同期で、イベントの発生を伝える機構である。シグナルが送信された際、OSは宛先プロセスの正常な処理の流れに割り込む。どんな不可分でない処理の間でも割り込むことができる。受信プロセスが以前にシグナルハンドラを登録しておけば、シグナル受信時にそのルーチンが実行される。さもなくば、デフォルトのシグナル処理が行われる。(同様なものは他のTSSなどでも開発されてはいるが、UNIXのシグナルは)1970年ごろベル研究所でUNIXに実装された。後にPOSIXである程度は標準化されているが、標準化が諦められているような振舞などもいくつかあり、特に他の幾つかの要素(fork等)とマルチスレッドとシグナルが絡むと実装毎の対処にプログラミングが大変になることがある。プロセスはI/O待ちなど、カーネルの内部で処理がブロックしている場合などで割り込み不可状態になることがあり、その場合は如何なるシグナルを送っても無効になる。
以下のような操作によりシグナルが送信される。
ユーザーがあるプロセスの端末のキーを押下したとき、端末がシグナルを発生する。
CTRL+C(古いUNIXでは DEL キー)を押下すると、SIGINT を送信し、デフォルトではそのプロセスを終了させる。
CTRL+Z を押下すると、SIGTSTP を送信し、デフォルトではプロセスの実行を中断(一時停止)させる。
CTRL+backslash を押下すると、SIGQUIT を送信し、デフォルトではプロセスを終了させコアダンプさせる。
(なお、これらのキーの組み合わせは <tt>stty</tt> コマンドで変更可能である)
<tt>kill(2)</tt> システムコールを使うと、権限があれば指定したプロセス(群)に指定したシグナルを送信できる。同様に <tt>kill(1)</tt> コマンドでユーザーがプロセス(群)にシグナルを送信することもできる。また、raise(3) を使えばカレントプロセス(またはスレッド)に指定したシグナルを送信できる。
ゼロ除算 (SIGFPE)、セグメンテーション違反 (SIGSEGV) などの例外によってもシグナルが発生する。経験の浅いプログラマはポインタに不正アドレスを入れてしまい、SIGSEGVを発生させることが多い。これらはデフォルトではプログラムを終了させ、コアダンプを生じる。
カーネルはプロセスに何らかのイベントを通知するためにシグナルを発生させることができる。例えば、プロセスがパイプに書き込んだとき読み込み側プロセスが既にパイプをクローズしている場合にSIGPIPEが送信される。この場合、デフォルトではそのプロセスは終了となるが、パイプをシェルが構築した場合、終了させるのが最も便利である。
signal()やSigactionシステムコールは「シグナルハンドラ」を設定するのに使われる。シグナルハンドラが設定されていないシグナルの場合、デフォルトのハンドラが使われる。さもなくば、シグナルは捉えられ、シグナルハンドラが呼び出される。プロセスはハンドラを設定しなくとも2種類のデフォルト動作を指定できる。シグナルを無視するか(SIG_IGN)、デフォルトのハンドラを使うか(SIG_DFL)である。SIGKILLとSIGSTOPは、捉えることもハンドラで処理することもできないシグナルである。
=== 問題 ===
ハンドラによるシグナル処理は競合状態に弱い。シグナルは非同期イベントなので、あるシグナルをハンドラで処理中に別のシグナル(同じ種類ということもある)がそのプロセスに送られてくることがある。このような状態を防ぐため、sigprocmask (Unix) を使ってシグナル配送のブロック/アンブロックが可能である。
シグナルは処理中のシステムコールを中断することがあり、アプリケーションは非透過的な再実行をしなければならない。つまり、実行中のシステムコールはEINTRというエラーを返し、要求した処理はシグナル受信によって中断されて結果を得られていないことを示す。そのため、処理を続行するには再度同じシステムコールを実行しなければならない。
シグナルハンドラは通常の処理に割り込んで呼び出されるので、不要な副作用を起こさないように注意が必要である。例えば、大域変数である <tt>errno</tt> を変化させてはならない。シグナルマスクを変化させてはならない。シグナル処理方法を変化させてはならない。その他のプロセスの大域的な属性を変化させてはならない。<tt>malloc</tt>、<tt>printf</tt>といった非同期シグナル安全(async‐signal‐safe)でない関数を使うのも安全ではない。
プロセスの実行によってハードウェア例外が発生することがある。例えば、プロセスがゼロ除算を行おうとしたときや、TLBミスを引き起こしたときなどである。Unix系OSでは、ハードウェア例外が発生するとコンテキストを自動的に切り替えてカーネルの例外ハンドラを実行開始する。ページフォールトなどの一部の例外の場合、カーネルはそのイベントを処理するのに十分な情報を持っているので、プロセスの実行を再開させることができる。しかし他の例外ではカーネルはうまく処理できず、代わりに例外を発生させた処理を行っていたプロセスに例外処理を委任しなければならない。シグナルはこの委任のための機構としても機能し、カーネルからプロセスに対してその例外に対応したシグナルを送信する。例えば、x86 CPU でゼロ除算を行おうとした場合 divide error 例外が発生し、カーネルがそのプロセスにSIGFPEというシグナルを送信する。同様に、あるプロセスが自身の仮想アドレス空間の範囲外のメモリアドレスにアクセスしようとした場合、カーネルはSIGSEGVシグナルを送信する。ハードウェア例外の種類はCPUのアーキテクチャによって異なるので、ある例外が発生したときどういうシグナルが送信されるかは厳密にはCPUアーキテクチャやカーネルの実装に依存する。
Single UNIX Specification では、以下のシグナルを <tt><signal.h></tt> で定義すべきものとして指定している。
シグナル受信時のデフォルト動作は以下のような処理がある:
T: プロセスの異常終了。<tt>exit()</tt> システムコールを実行したのと同様の終了の仕方だが、<tt>wait()</tt>または<tt>waitpid()</tt>でそのプロセスの終了を待ち合わせていたプロセスには、シグナル受信で終了したことを示す異常終了コードが返される。
A: プロセスの異常終了。設定されていればコアダンプを生成する。
I: シグナルを無視する。
S: プロセスの実行を中断(一時停止)する。
C: 中断されていたプロセスの実行を再開する。中断されていないプロセスでは無視する。
下記表のシグナル番号は Linux x86 の場合であり、他のOS・他のCPUでは異なる。Linux ARM もシグナル番号は同じ。
注:アスタリスク付の項目は、X/Open System Interfaces (XSI) による拡張を示す。(SUS) とある部分はSUSにある表現の引用(を和訳したもの)。
上述以外に、プロセスは擬似シグナル(番号0)を送信することもできる。これは実際にはシグナルを送信せずにシグナル送信時のエラーチェックをし、例えば宛先プロセスが存在するかどうかをチェックするのに便利である。