シグナル、あるいはソフトウェア割り込みはプログラムの処理を切り替えるためのプログラム外部からの非同期イベントです。シグナルはプログラムの実行中のどんなときにでも起こりえます。この点において、 read 関数やパイプ (5 章参照) のような外部からのメッセージを明示的に待つプロセス間通信とは異なっています。
シグナルによって伝わるのはシグナルの種類という最小限の情報だけです。シグナルはもともとプロセス間通信を目的とするものではありませんでしたが、外部装置 (つまりシステムや他のプロセス) の状態の原始的な情報を送ることでプロセス間通信を可能にしています。
プロセスがシグナルを受け取ったときの動作としてありえるのは次の四つです:
シグナルにはいくつか種類があり、それぞれ特定のイベントと結びついています。表 4 にシグナル (の一部) とそのデフォルトの動作を示します。
名前 | イベント | デフォルトの動作 |
sighup | ハングアップ (接続の終了) | 終了 |
sigint | 割り込み (ctrl-C ) | 終了 |
sigquit | 終了 (ctrl-\ ) | 終了 & コアダンプ |
sigfpe | 算術エラー (0 による除算) | 終了 & コアダンプ |
sigkill | 中止 (無視できない) | 終了 |
sigsegv | 不正なメモリ参照 | 終了 & コアダンプ |
sigpipe | 読み込み先のいないパイプへの書き出し | 終了 |
sigalrm | タイマー割り込み | 無視 |
sigtstp | 一時中断 (ctrl-Z ) | 中断 |
sigcont | 中断したプロセスの再開 | 無視 |
sigchld | 子プロセスが終了あるいは停止した | 無視 |
プロセスが受け取るシグナルはいくつかの方法で送られます:
ctrl-C
と打つことで sigint
シグナルをターミナルがフォアグラウンドで実行しているプロセスに送ることができる。同様に ctrl-\
は sigquit
シグナルを送る1。端末が自分自身を終了するか、ネットワークのリンクが切れることで端末が終了したとき、 sighup
シグナル送られる。kill
で送る。kill
を使うと特定のプロセスに特定のシグナルを送ることができる。例えば kill -KILL 194
は sigkill
シグナルを ID が 194 のプロセスに送り、プロセスを停止させるkill
で送る (一つ前のケースと同じ)。sigfpe
が送られる。sigchld
シグナルが送られる。システムコール kill を使うとプロセスにシグナルを送ることができます。
第一引数はシグナルを送るプロセスの ID で、第二引数は送るシグナルの番号です。kill
を呼んだユーザによって所有されていないプロセスにシグナルを送ろうとするとエラーとなります。プロセスは自分自身にシグナルを送ることができます。システムコール kill
が値を返した場合、シグナルが目的のプロセスに届いたことが保証されます。
プロセスが同じシグナルを短い間に何度も受け取った場合、そのシグナルに対応するコードは一度しか実行しないことがあります。そのためプロセスは受け取ったシグナルの数を数えることはできず、数えられるのはシグナルに反応した回数となります。
システムコール alarm を使うとシステムクロックを使って割り込みを予約することができます。
alarm s
という呼び出しはすぐに返りますが、その後少なくとも s
秒後 (最大で何秒後かについての保証はありません) に sigalrm
シグナルが送られます。この呼び出しが返すのはこれまでの alarm
によって設定されたアラームまでの残り秒数です。s
が 0
の場合、これまでのアラームをキャンセルします。
システムコール signal を使うとプロセスが指定した種類のシグナルを受け取ったときの動作を変更できます。
第一引数は動作を変更するシグナルの番号で、 signal_behavior 型の第二引数はそのシグナルに対応する動作を指定します。 signal_behavior
には以下の値があります:
Signal_ignore | シグナルを無視する。 |
Signal_default | デフォルトの動作を行う。 |
Signal_handle f | シグナルを受け取ると f を実行する。
|
システムコール fork によってプロセスをフォークしてもシグナルに関する動作は引き継がれます。フォーク直後の子プロセスのシグナルへの動作は fork
が実行された時点での親プロセスのものと同じです。システムコール execve は無視されているシグナルは無視されたままにし、それ以外のシグナルについては Signal_default
に動作を変更します。
ログオフしたりセッションを終了した後でもバックグラウンドでタスク (大規模な計算、あるいは “スパイウェア” など)を実行したままにしておきたいことがあります。プロセスの sighup
シグナル (ユーザが接続を終了すると送られます) に対するデフォルトの動作はプロセスの終了なので、このシグナルを無視するように変更すればプロセスを実行されたままにすることができます。Unix コマンド nohup
はまさにこのことを行います:
nohup
はコマンド cmd arg1 ... argn
を fighup
に影響されない状態で実行します (バックグラウンドで起動された全てのプロセスに対して自動的に nohup
を実行するシェルもあります)。この nohup
は 3 行で実装できます:
システムコール execvp は sighup
が無視される状態を引き継ぎます。
異常動作をしたプログラムの注意深い終了処理。例えば tar
のようなプログラムは、異常動作によって終了することになっても終了する前にファイルの重要な情報を書き込んだり壊れたファイルを削除することが望ましいです。以下のコードをプログラムの最初に書けばプログラムが終了するときの動作を設定することができます。
ここで quit
は以下のような形をしています:
ユーザが発した割り込みのキャプチャ。インタラクティブなプログラムではユーザが ctrl-C
を押したときにメインループを抜けるようになっていることがあります。sigint
シグナルを受け取ったときに例外を出すようにすれば実装できます。
アニメーションなどのメインプログラムと切り離された周期的なタスクの実行。例えば次のプログラムメインプログラムの動作 (計算、入出力) に関係なく 30 秒ごとに “ビープ” 音を鳴らします。
シグナルは非同期コミュニケーションに便利です — 実際それが存在理由です — が、この非同期であるという特徴のせいでシグナルはシステムプログラミングにおける難しい部分になっています。
シグナルハンドラは非同期に実行されるので、この関数とプロセスのメインプログラムとは擬似的に並列な状態で実行されます。シグナルハンドラが値を返せないことから、通常この関数は通常グローバル変数を変更します。したがってメインプログラムも同時にその変数を変更しようとした場合、競合状態に陥ること可能性があります。これに対する一つの解決法は次の節で説明するようにシグナルハンドラが変更する変数をメインプログラムが変更するときにはシグナルを一時的にブロックすることです。
厳密にいうと、 OCamlはシグナルを非同期に扱いません。OCamlは受け取ったシグナルを記録しますが、シグナルハンドラが実行されるのは特定の チェックポイント においてだけです。チェックポイントはハンドラが非同期に実行されると考えても良い程度に頻繁に設けられています。通常アロケーションやループ制御、システムとのやり取り (システムコールを含む) のときにチェックポイントが設けられます。ループが含まれず、アロケーションをせず、システムとのやり取りをしないプログラムについて、OCamlはメインプログラムとシグナルハンドラの実行が互い違いにならないことを保証しています。そのため例えばアロケートされない値 (整数および真偽値など。小数値は含まれません!) の参照セルは上記の状況でも競合状態に陥りません。
シグナルはブロックできます。ブロックされたシグナルは無視されるわけではなく、後で届けられるよう待機状態になります。システムコール sigprocmask を使うとシグナルのマスクすることができます。
sigprocmask cmd sigs
はブロックするシグナルを変更し、この呼び出しの前にブロックされていたシグナルのリストを返します。返り値によってマスクを元の状態に戻すことが可能になります。引数 sigs
はシグナルのリストであり、 sigprocmask_command 型の値 cmd
によって呼び出しの効果が異なります:
SIG_BLOCK | sigs 内のシグナルがブロックするシグナルのリストに追加される。 |
SIG_UNBLOCK | sigs 内のシグナルがブロックするシグナルのリストから削除される。 |
SIG_SETMASK | sigs 内のシグナルがブロックするシグナルとなる。
|
典型的な sigprocmask
の利用法はあるシグナルをマスクすることです。
次のパターンを使うと起こりがちなエラーを防ぐことができます。
いくつかのシステムコールは無視されていないシグナルによって中断されます。このようなシステムコールは 遅い システムコールと呼ばれる、実行にいくらでも長い時間がかかりうるもの (例えば端末との i/oや select、 system など) です。割り込みが起こった場合、これらのシステムコールは実行を完了しないまま EINTR
例外を出します。
一方ファイル i/oは割り込みされません。ファイル i/o中に実行中のプロセスを中断して他のプロセスを実行することはありますが、ディスクが正常に機能しているならばこの中断は常に短時間になります。ゆえにデータのスループットはシステムのみに依存し、他のユーザのプロセスから影響を受けることはありません。
無視されたシグナルは届かず、マスクされたシグナルはマスクを解くまで届きません。しかしそれ以外の場合はシステムコールをマスクしていないシステムコールから守る必要があります。典型的な例は子プロセスの終了を待っている親プロセスです。子のプロセス ID を wait
とすると親は waitpid [] pid
を実行しますが、waitpid
はブロックするシステムコールなので 遅い システムコールであり、シグナルによって中断される可能性があります。特に子プロセスが終了したときには sigchld
シグナルが親に送られます。
Misc
モジュールの restart_on_EINTR
関数はシステムコールが中断されたとき、すなわち EINTR
例外が出たときにシステムコールをやり直します。
子プロセスの終了をシグナルで中断されないように待つには、restart_on_EINTR (waitpid flags) pid
を呼びます。
子プロセスの返り値が親プロセスにとって重要でない場合、 sigchld
のシグナルハンドラによって子プロセスを非同期に回収することも可能です。ただし短い時間に何度も同じシグナルを受け取った場合シグナルハンドラが一度しか起動しないことがあるので、sigchld
を受け取ったときに子プロセスがいくつ終了したかを知ることはできません。このことから、sigchld
シグナルを処理するためには以下のライブラリ関数 Misc.free_chidlren
が必要になります。
free_children
は waitpid
をノンブロッキングモード (WNOHANG
オプション) で実行して実行を終了した子プロセスを回収する、という処理を子プロセスが全て実行中となる (waitpid
が子のプロセス ID ではなく 0 を返す) か子プロセスがなくなる (ECHILD
例外が出される) まで繰り返します。
waitpid
が WNOHANG
オプションでノンブロッキングとなっているので EINTR
に対する処理は必要ありません。
Unix
モジュールの system
関数は次のように単純に定義されています:
C 標準ライブラリの system
関数の規格では、親プロセスは sigint
と sigquit
を無視しコマンドの実行中は sigchld
をマスクするようになっています。これによって親プロセスに影響を与えること無く子プロセスを中断したり終了させることが可能になります。
ここでは system
関数をより一般的な exec_as_system
関数の特殊化として定義します。この関数ではシェルを必ずしも起動されません。
シグナルの変更は fork
の実行の前に行う必要があることに注意してください。フォークを実行した後にシグナルを変更するようにすると、シグナルが変更しきる前にシグナル (例えばすぐに終了した子プロセスからの sigchld
) を受け取ってしまう可能性があるためです。シグナルの変更は子プロセスではコマンドの実行の前に 11 行目でリセットされます。fork
と exec
は無視するシグナルを保存し、 fork
はシグナルに対する動作を保存します。exec
は無視していないシグナルの動作をデフォルトに戻します。
最後に親プロセスはエラーが起きた場合でもシグナルの変更をリセットする必要があります。このために 15 行目では try_finalize
が使われます。
Unix の初期のバージョンから、時間は秒で測られてきました。そのため互換性が重要となるならば、常に秒単位で時間を計測するべきです。現在時刻は 1970 年 1 月 1 日 00:00:00
gmt からの経過秒数と定義されます。次の関数で取得できます:
システムコール sleep は引数で指定した秒数だけプログラムの実行を止めます:
しかしこの関数は原始的ではありません。さらに基本的なシステムコール alarm
(前の節を参照) と sigsuspend
を使うことで sleep
を実装することができます。
sigsuspend l
はリスト l
内のシグナルを一時的に差し止め、無視も差し止めもされていないシグナルを受け取るまでプログラムの実行を停止します。値が返るときにシグナルマスクは元の値に戻ります。
sleep
は以下のように実装できます:
初期状態では何もしないことが sigalrm
シグナルに対する動作です。“何もしない” とはシグナルを無視することとは異なることに注意してください。sigalrm
シグナルを受け取ったときにプロセスが起動することを保証するために、このシグナルは無視されないようにします。そのあと sigalrm
以外のマスクされているシグナルを全て差し止めてからスタンバイ状態に移行します。この変更はアラームのシグナルの後で取り消されます。sigsuspend
がシグナルマスクを変更しないことから、9 行目は 2 行目の直後に置くこともできます。
現代的なバージョンの Unix では時刻をマイクロ秒単位で測定することができます。OCamlではマイクロ秒単位で測定した時刻は浮動小数点型で表されます。システムコール gettimeofday は現代的なシステムにおいて time
の代わりとなるものです。
現在出回っている Unix では各プロセスはそれぞれ違う時間を測定する 3 つのタイマーを持ちます。タイマーは interval_timer 型の値として確認できます:
ITIMER_REAL | 実時間 (sigalrm ). |
ITIMER_VIRTUAL | ユーザ時間 (sigvtalrm ). |
ITIMER_PROF | ユーザ時間とシステム時間 (sigprof ).
|
タイマーの状態は interval_timer_status 型のレコードによって表され、各フィールドは以下の時間を表します (どちらも float
です):
it_interval
フィールドはタイマーの周期を表す。
it_value
フィールドはタイマーの現在の値を表す。この値が 0
になった場合 sigvtalrm
シグナルが送られ、タイマーの値は it_interval
にリセットされる。
二つのフィールドが 0
の場合タイマーはアクティブではありません。タイマーは次の関数で取得および変更できます:
settimer
の返り値は変更前の値です。
複数のタイマーを管理するために、以下のインターフェースを持つモジュールを書いてください:
new_timer k f
はタイプ k
の新しいタイマーを作り f
の実行を始めます。タイマー作成時にはアクティブではありません。set_timer t
はタイマーの値を t
に設定し古い値を返します。
Unix の現代的なバージョンには日付を処理するための関数も含まれています。tm 構造体はカレンダー (年、月など) によって表現でき、gmtime や localtime、 mktime などによって他の単位と変換できます。
シグナルは非同期なので、シグナルをプロセス間通信に使うと制限と困難がいくつかあります:
シグナルは制限された非同期通信であるにもかからわず、非同期通信にまつわる困難や問題は全て含みます。そのため可能であれば利用を避けるべきです。たとえば短い時間だけ待つためには select がアラームの代わりに使えます。ただしコマンドラインのインタープリターなどのシグナルを考えなければいけない状況もあります。
シグナルはおそらく Unix システムの中で最も有用でない概念です。古いバージョンの Unix (System V など) ではシグナルの動作は受け取った後に自動的にSignal_default
にリセットされます。そのためシグナルハンドラは自分自身をもう一度設定し直す必要があり、以下のように書く必要がありました:
しかし問題はシグナルを受け取ってシグナルの動作が自動的に Signal_default
に戻ってからset_signal
が実行されるまでの短い間にシグナルを受け取った場合です。この場合シグナルはハンドラを呼びだすことなく、種類に応じて無視されるかプロセスを終了させます。
その他の Unix (bsd と Linux) はより良い動作をします。シグナルを受け取ってもその動作を置き換えず、シグナルを処理している間は他のシグナルは保留されます。