シグナル(英: signal)とは、Unix系(POSIX標準に類似の)オペレーティングシステム (OS) における、限定的なプロセス間通信であり、プロセスに対し非同期でイベントの発生を伝える機構である。シグナルが送信された際、OSは宛先プロセスの正常な処理の流れに割り込む。どんな不可分でない処理の間でも割り込むことができる。受信プロセスが以前にシグナルハンドラを登録しておけば、シグナル受信時にそのルーチンが実行される。さもなくば、デフォルトのシグナル処理が行われる。(同様なものは他のTSSなどでも開発されてはいるが、UNIXのシグナルは)1970年ごろベル研究所でUNIXに実装された。後にPOSIXである程度は標準化されているが、標準化が諦められているような振舞などもいくつかあり、特に他の幾つかの要素(fork等)とマルチスレッドとシグナルが絡むと実装毎の対処にプログラミングが大変になることがある。プロセスはI/O待ちなど、カーネルの内部で処理がブロックしている場合などで割り込み不可状態になることがあり、その場合は如何なるシグナルを送っても無効になる。
POSIX準拠のシグナルにあっては、プロセスへシグナルが配送され、シグナルハンドラの実行を開始および終了する際、以下の挙動を保証している。
これらの仕様は、配送されたシグナルがシグナルマスクの設定に従って確実にシグナルハンドラを起動し、かつ不用意なシグナルハンドラへの再入を防ぐことを目的としている。このため、上記を満たすシグナルの実装を「信頼できるシグナル」と呼ぶことがある。POSIX以前の実装では上記の挙動が一部欠落していたり、アトミックに実行されないため、シグナルハンドラが期待通りに実行されなかったり、シグナルハンドラへの再入設定のためプロセス側で繊細な追加処理が必要になるなどの問題がしばしば生じていた。
以下のような操作によりシグナルが送信される。
raise(3)
abort()
signal()やsigaction()(英語版)システムコールは「シグナルハンドラ」を設定するのに使われる。シグナルハンドラが設定されていないシグナルの場合、デフォルトのハンドラが使われる。さもなくば、シグナルは捉えられ、シグナルハンドラが呼び出される。プロセスはハンドラを設定しなくとも2種類のデフォルト動作を指定できる。シグナルを無視するか(SIG_IGN)、デフォルトのハンドラを使うか(SIG_DFL)である。SIGKILLとSIGSTOPは、捉えることもハンドラで処理することもできないシグナルである。
signal()
sigaction()(英語版)
シグナルハンドラが暗黙的に呼び出すシステムコールとしてsigreturn()がある。これはシグナルハンドラの終了後にカーネルへ処理を戻し、POSIXが要求するシグナルマスクおよびスタックの巻き戻しを実行してから割り込まれたプロセスへ戻ることを目的としている。通常はカーネルがシグナルハンドラを呼び出す際、カーネルがプロセス上にシグナルハンドラのラッパーコードを用意し、シグナルハンドラの終了後に自動的にsigreturn()を呼び出すよう実装しているため、明示的にsigreturn()を呼び出す必要はない。なお、シグナルハンドラがlongjmp()等を用いてシグナル処理開始時とは別の箇所へジャンプする場合は、longjmp()等がsigreturn()と等価な処理を行う。
sigreturn()
longjmp()
シグナルの重要な挙動として、シグナルを受信可能としてスリープしているプロセスは、一般にシグナルを受信するとスリープを終了する。シグナルハンドラの実行が必要な場合は自然な挙動だが、デフォルト動作によりプロセスが終了する場合も同じ挙動になる。これはUNIXのプロセスはすべて自ら終了するものであり、他のプロセスに終了させられることはない仕様となっていることに依る。ただし、終了するプロセスはユーザモードに戻らないため、カーネルの挙動を無視するとあたかもプロセスがシグナルにより「殺された」ように見える。この挙動による副作用として、シグナルを受信したプロセスのPCBやカーネルスタックなどがスワップアウトされていた場合、それらを再度メモリ上に読み直さなければシグナル受信に起因するプロセス終了処理ができない。カーネルがメモリ不足のためにプロセスへシグナルを送信した場合、この副作用はメモリ需要を一時的ながら増加させてしまうリスクがある。
上記の例外として、以下の場合がある。
SIGSTOP
SIGCONT
いずれの場合も、シグナル受信の影響はプロセスがスリープしている原因が変化するだけであり、プロセスがスケジュールされないことに変わりはない。このため、上記に該当する場合はシグナルを送信したプロセスが送信先プロセスの状態を直接更新し、それをもってシグナル受信処理とする。
ハンドラによるシグナル処理は競合状態に弱い。シグナルは非同期イベントなので、あるシグナルをハンドラで処理中に別のシグナル(同じ種類ということもある)がそのプロセスに送られてくることがある。このような状態を防ぐため、sigprocmask()(英語版) を使ってシグナル配送のブロック/アンブロックが可能である。
sigprocmask()(英語版)
シグナルは処理中のシステムコールを中断することがあり、その際にアプリケーションは非透過的な再実行をしなければならない場合がある。この場合、実行中のシステムコールはEINTRというエラーを返し、要求した処理はシグナル受信によって中断されて結果を得られていないことを示す。この場合、処理を続行するには再度同じシステムコールを実行しなければならない。一方、4.2BSDにて、システムコールがシグナル受信のために中断後、ユーザプロセスの介在なく直ちに再開できる場合は内部でエラーERESTARTを返し、シグナルハンドラの実行後システムコールの呼び出し元には戻らず、透過的にシステムコールを継続できるようになった。ERESTARTはカーネル内部でのみ使用されるエラー値であり、ユーザプロセスへは返さない。
シグナルハンドラは通常の処理に割り込んで呼び出されるので、不要な副作用を起こさないように注意が必要である。以下は具体例。
特に、再入不可能な処理の実行中にシグナルを受信し、ハンドラがlongjmp()等を呼び出した場合、ジャンプ後も再入不可能な処理を実行中の状態となってしまうため、回復が極めて困難になることに注意を要する。これを避けるには再入不可能な処理中のシグナルマスクを適切に設定したり、再入不可能な処理が時間を要する場合は処理を安全に中断できる箇所を作った上でsigwaitinfo()、sigsuspend()、sigwait()、sigtimedwait()等を呼び出して受信したシグナルを確認および処理する機会を作る必要がある。
sigwaitinfo()
sigsuspend()
sigwait()
sigtimedwait()
例として、複数のファイルを順次更新するバッチ処理を行うソフトウェアを考える。ファイル更新中に処理を中断するとファイルが破損する一方、個々のファイルは独立しているので、あるファイルの処理が終了した直後であれば処理の中断が可能とする。この条件下では、ファイルを1つ処理する毎にシグナル受信の機会を設けることにより、ファイルの破損を防ぐ条件下で迅速にシグナルを処理することができる。各ファイルの処理中はシグナルをマスクし、ファイル破損につながる状況下でのシグナル処理を防ぐ。
プロセスの実行によってハードウェア例外が発生することがある。例えば、プロセスがゼロ除算を行おうとしたときや、TLBミスを引き起こしたときなどである。Unix系OSでは、ハードウェア例外が発生するとコンテキストを自動的に切り替えてカーネルの例外ハンドラを実行開始する。ページフォールトなどの一部の例外の場合、カーネルはそのイベントを処理するのに十分な情報を持っているので、プロセスの実行を再開させることができる。しかし他の例外ではカーネルはうまく処理できず、代わりに例外を発生させた処理を行っていたプロセスに例外処理を委任しなければならない。シグナルはこの委任のための機構としても機能し、カーネルからプロセスに対してその例外に対応したシグナルを送信する。例えば、x86 CPU でゼロ除算を行おうとした場合 divide error 例外が発生し、カーネルがそのプロセスにSIGFPEというシグナルを送信する。同様に、あるプロセスが自身の仮想アドレス空間の範囲外のメモリアドレスにアクセスしようとした場合、カーネルはSIGSEGVシグナルを送信する。ハードウェア例外の種類はCPUのアーキテクチャによって異なるので、ある例外が発生したときどういうシグナルが送信されるかは厳密にはCPUアーキテクチャやカーネルの実装に依存する。
Single UNIX Specification では、以下のシグナルを <signal.h> で定義すべきものとして指定している。
シグナル受信時のデフォルト動作は以下のような処理がある[3]:
下記表のシグナル番号は Linux x86 の場合であり[4]、他のOS・他のCPUでは異なる。Linux ARM もシグナル番号は同じ。
注:アスタリスク付の項目は、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が行った。
sigaction()
siglongjmp()
signal(7)