シグナル (Unix)

シグナル（英: signal）とは、Unix系（POSIX標準に類似の）オペレーティングシステム (OS) における、限定的なプロセス間通信であり、プロセスに対し非同期でイベントの発生を伝える機構である。シグナルが送信された際、OSは宛先プロセスの正常な処理の流れに割り込む。どんな不可分でない処理の間でも割り込むことができる。受信プロセスが以前にシグナルハンドラを登録しておけば、シグナル受信時にそのルーチンが実行される。さもなくば、デフォルトのシグナル処理が行われる。（同様なものは他のTSSなどでも開発されてはいるが、UNIXのシグナルは）1970年ごろベル研究所でUNIXに実装された。後にPOSIXである程度は標準化されているが、標準化が諦められているような振舞などもいくつかあり、特に他の幾つかの要素（fork等）とマルチスレッドとシグナルが絡むと実装毎の対処にプログラミングが大変になることがある。プロセスはI/O待ちなど、カーネルの内部で処理がブロックしている場合などで割り込み不可状態になることがあり、その場合は如何なるシグナルを送っても無効になる。 POSIX準拠のシグナルにあっては、プロセスへシグナルが配送され、シグナルハンドラの実行を開始および終了する際、以下の挙動を保証している。 シグナルハンドラの実行開始時に、配送されたシグナルのマスク、プロセスがあらかじめ追加指定したシグナルのマスク、およびプロセスが指定したスタックがある場合はそれへの切替をすべてアトミックに実行する。すなわち、シグナルハンドラ実行の準備としてそれらの処理を行っている最中に別のシグナルが配送されても、少なくともシグナルハンドラの実行開始までは別シグナルの処理を行わない。 シグナルハンドラ実行開始後の挙動は、シグナルマスクの設定に依存する。 シグナルハンドラが実行を終了すると、実行開始に際して変更したシグナルマスクおよびスタックの変更をシグナル配送前の状態へ巻き戻す。シグナルハンドラ実行開始時と同じく、巻き戻しの処理もアトミックに行う。 これらの仕様は、配送されたシグナルがシグナルマスクの設定に従って確実にシグナルハンドラを起動し、かつ不用意なシグナルハンドラへの再入を防ぐことを目的としている。このため、上記を満たすシグナルの実装を「信頼できるシグナル」と呼ぶことがある。POSIX以前の実装では上記の挙動が一部欠落していたり、アトミックに実行されないため、シグナルハンドラが期待通りに実行されなかったり、シグナルハンドラへの再入設定のためプロセス側で繊細な追加処理が必要になるなどの問題がしばしば生じていた。 以下のような操作によりシグナルが送信される。 ユーザーがあるプロセスの端末のキーを押下したとき、端末がシグナルを発生する。 CTRL+C（古いUNIXでは DEL キー）を押下すると、SIGINT を送信し、デフォルトではそのプロセスを終了させる。 CTRL+Z を押下すると、SIGTSTP を送信し、デフォルトではプロセスの実行を中断（一時停止）させる。 CTRL+\ を押下すると、SIGQUIT を送信し、デフォルトではプロセスを終了させコアダンプさせる。 （なお、これらのキーの組み合わせは stty（英語版） コマンドで変更可能である） 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)でない関数を使うのは安全ではない。 再入不可能な処理を実行する場合、それが終了するまでシグナルをマスクし、シグナル受信処理を遅らせる。 大域変数 errno の変更など、割り込まれた処理が予期していない変更を含む処理をしない。 ただし、シグナルマスクやハンドラをシグナルハンドラの実行中に一時的に変更することは可能。シグナルマスクはシグナルハンドラの終了時にカーネル等が暗黙的にリストアする。ハンドラを変更した場合はシグナルハンドラの実行終了時に明示的にリストアする必要がある。 シグナル受信のフラグはしばしば大域変数として実装する必要があるが、処理を中断しても問題ない箇所でシグナルを受信し、かつシグナルをマスクした上で更新ないしは読み出すのであれば安全な実装となる。 特に、再入不可能な処理の実行中にシグナルを受信し、ハンドラがlongjmp()等を呼び出した場合、ジャンプ後も再入不可能な処理を実行中の状態となってしまうため、回復が極めて困難になることに注意を要する。これを避けるには再入不可能な処理中のシグナルマスクを適切に設定したり、再入不可能な処理が時間を要する場合は処理を安全に中断できる箇所を作った上でsigwaitinfo()、sigsuspend()、sigwait()、sigtimedwait()等を呼び出して受信したシグナルを確認および処理する機会を作る必要がある。 例として、複数のファイルを順次更新するバッチ処理を行うソフトウェアを考える。ファイル更新中に処理を中断するとファイルが破損する一方、個々のファイルは独立しているので、あるファイルの処理が終了した直後であれば処理の中断が可能とする。この条件下では、ファイルを1つ処理する毎にシグナル受信の機会を設けることにより、ファイルの破損を防ぐ条件下で迅速にシグナルを処理することができる。各ファイルの処理中はシグナルをマスクし、ファイル破損につながる状況下でのシグナル処理を防ぐ。 プロセスの実行によってハードウェア例外が発生することがある。例えば、プロセスがゼロ除算を行おうとしたときや、TLBミスを引き起こしたときなどである。Unix系OSでは、ハードウェア例外が発生するとコンテキストを自動的に切り替えてカーネルの例外ハンドラを実行開始する。ページフォールトなどの一部の例外の場合、カーネルはそのイベントを処理するのに十分な情報を持っているので、プロセスの実行を再開させることができる。しかし他の例外ではカーネルはうまく処理できず、代わりに例外を発生させた処理を行っていたプロセスに例外処理を委任しなければならない。シグナルはこの委任のための機構としても機能し、カーネルからプロセスに対してその例外に対応したシグナルを送信する。例えば、x86 CPU でゼロ除算を行おうとした場合 divide error 例外が発生し、カーネルがそのプロセスにSIGFPEというシグナルを送信する。同様に、あるプロセスが自身の仮想アドレス空間の範囲外のメモリアドレスにアクセスしようとした場合、カーネルはSIGSEGVシグナルを送信する。ハードウェア例外の種類はCPUのアーキテクチャによって異なるので、ある例外が発生したときどういうシグナルが送信されるかは厳密にはCPUアーキテクチャやカーネルの実装に依存する。 Single UNIX Specification では、以下のシグナルを <signal.h> で定義すべきものとして指定している。 シグナル受信時のデフォルト動作は以下のような処理がある: T: プロセスの異常終了。exit() システムコールを実行したのと同様の終了の仕方だが、wait()またはwaitpid()でそのプロセスの終了を待ち合わせていたプロセスには、シグナル受信で終了したことを示す異常終了コードが返される。 A: プロセスの異常終了。設定されていればコアダンプを生成する。 I: シグナルを無視する。 S: プロセスの実行を中断（一時停止）する。 C: 中断されていたプロセスの実行を再開する。中断されていないプロセスでは無視する。 下記表のシグナル番号は Linux x86 の場合であり、他のOS・他のCPUでは異なる。Linux ARM もシグナル番号は同じ。 注：アスタリスク付の項目は、X/Open System Interfaces (XSI) による拡張を示す。(SUS) とある部分はSUSにある表現の引用（を和訳したもの）。 上述以外に、プロセスは擬似シグナル（番号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 マニュアル 標準Cライブラリ 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