Previous Up Next

 4  シグナル

シグナル、あるいはソフトウェア割り込みはプログラムの処理を切り替えるためのプログラム外部からの非同期イベントです。シグナルはプログラムの実行中のどんなときにでも起こりえます。この点において、 read 関数やパイプ (5 章参照) のような外部からのメッセージを明示的に待つプロセス間通信とは異なっています。

シグナルによって伝わるのはシグナルの種類という最小限の情報だけです。シグナルはもともとプロセス間通信を目的とするものではありませんでしたが、外部装置 (つまりシステムや他のプロセス) の状態の原始的な情報を送ることでプロセス間通信を可能にしています。

4.1  デフォルトの動作

プロセスがシグナルを受け取ったときの動作としてありえるのは次の四つです:

シグナルにはいくつか種類があり、それぞれ特定のイベントと結びついています。表 4 にシグナル (の一部) とそのデフォルトの動作を示します。

名前イベントデフォルトの動作
sighupハングアップ (接続の終了)終了
sigint割り込み (ctrl-C)終了
sigquit終了 (ctrl-\)終了 & コアダンプ
sigfpe算術エラー (0 による除算)終了 & コアダンプ
sigkill中止 (無視できない)終了
sigsegv不正なメモリ参照終了 & コアダンプ
sigpipe読み込み先のいないパイプへの書き出し終了
sigalrmタイマー割り込み無視
sigtstp一時中断 (ctrl-Z)中断
sigcont中断したプロセスの再開無視
sigchld子プロセスが終了あるいは停止した無視
Table 4 — シグナル (一部) とそのデフォルトの動作

プロセスが受け取るシグナルはいくつかの方法で送られます:

4.2  シグナルの利用

システムコール kill を使うとプロセスにシグナルを送ることができます。

val kill : int -> int -> unit

第一引数はシグナルを送るプロセスの ID で、第二引数は送るシグナルの番号です。kill を呼んだユーザによって所有されていないプロセスにシグナルを送ろうとするとエラーとなります。プロセスは自分自身にシグナルを送ることができます。システムコール kill が値を返した場合、シグナルが目的のプロセスに届いたことが保証されます。

プロセスが同じシグナルを短い間に何度も受け取った場合、そのシグナルに対応するコードは一度しか実行しないことがあります。そのためプロセスは受け取ったシグナルの数を数えることはできず、数えられるのはシグナルに反応した回数となります。

システムコール alarm を使うとシステムクロックを使って割り込みを予約することができます。

val alarm : int -> int

alarm s という呼び出しはすぐに返りますが、その後少なくとも s 秒後 (最大で何秒後かについての保証はありません) に sigalrm シグナルが送られます。この呼び出しが返すのはこれまでの alarm によって設定されたアラームまでの残り秒数です。s0 の場合、これまでのアラームをキャンセルします。

4.3  シグナルに対する動作の変更

システムコール signal を使うとプロセスが指定した種類のシグナルを受け取ったときの動作を変更できます。

val signal : int -> signal_behavior -> signal_behavior

第一引数は動作を変更するシグナルの番号で、 signal_behavior 型の第二引数はそのシグナルに対応する動作を指定します。 signal_behavior には以下の値があります:

Signal_ignoreシグナルを無視する。
Signal_defaultデフォルトの動作を行う。
Signal_handle fシグナルを受け取ると f を実行する。

システムコール fork によってプロセスをフォークしてもシグナルに関する動作は引き継がれます。フォーク直後の子プロセスのシグナルへの動作は fork が実行された時点での親プロセスのものと同じです。システムコール execve は無視されているシグナルは無視されたままにし、それ以外のシグナルについては Signal_default に動作を変更します。

ログオフしたりセッションを終了した後でもバックグラウンドでタスク (大規模な計算、あるいは “スパイウェア” など)を実行したままにしておきたいことがあります。プロセスの sighup シグナル (ユーザが接続を終了すると送られます) に対するデフォルトの動作はプロセスの終了なので、このシグナルを無視するように変更すればプロセスを実行されたままにすることができます。Unix コマンド nohup はまさにこのことを行います:

nohup cmd arg1 ... argn

nohup はコマンド cmd arg1 ... argnfighup に影響されない状態で実行します (バックグラウンドで起動された全てのプロセスに対して自動的に nohup を実行するシェルもあります)。この nohup は 3 行で実装できます:

open Sys;; signal sighup Signal_ignore;; Unix.execvp argv.(1) (Array.sub argv 1 (Array.length argv - 1));;

システムコール execvpsighup が無視される状態を引き継ぎます。

* * *

異常動作をしたプログラムの注意深い終了処理。例えば tar のようなプログラムは、異常動作によって終了することになっても終了する前にファイルの重要な情報を書き込んだり壊れたファイルを削除することが望ましいです。以下のコードをプログラムの最初に書けばプログラムが終了するときの動作を設定することができます。

signal sigquit (Signal_handle quit); signal sigsegv (Signal_handle quit); signal sigfpe (Signal_handle quit);

ここで quit は以下のような形をしています:

let quit _ = (* ファイルの重要な情報の書き込みを試みる *) exit 100;;
* * *

ユーザが発した割り込みのキャプチャ。インタラクティブなプログラムではユーザが ctrl-C を押したときにメインループを抜けるようになっていることがあります。sigint シグナルを受け取ったときに例外を出すようにすれば実装できます。

exception Break;; let break _ = raise Break;; ... let main_loop () = signal sigint (Signal_handle break); while true do try (* ユーザのコマンドを読み込んで評価する *) with Break -> (* "stopped" と表示する *) done;;
* * *

アニメーションなどのメインプログラムと切り離された周期的なタスクの実行。例えば次のプログラムメインプログラムの動作 (計算、入出力) に関係なく 30 秒ごとに “ビープ” 音を鳴らします。

let beep _ = output_char stdout '\007'; flush stdout; ignore (alarm 30);; ... signal sigalrm (Signal_handle beep); ignore (alarm 30);;
* * *

チェックポイント

シグナルは非同期コミュニケーションに便利です — 実際それが存在理由です — が、この非同期であるという特徴のせいでシグナルはシステムプログラミングにおける難しい部分になっています。

シグナルハンドラは非同期に実行されるので、この関数とプロセスのメインプログラムとは擬似的に並列な状態で実行されます。シグナルハンドラが値を返せないことから、通常この関数は通常グローバル変数を変更します。したがってメインプログラムも同時にその変数を変更しようとした場合、競合状態に陥ること可能性があります。これに対する一つの解決法は次の節で説明するようにシグナルハンドラが変更する変数をメインプログラムが変更するときにはシグナルを一時的にブロックすることです。

厳密にいうと、 OCamlはシグナルを非同期に扱いません。OCamlは受け取ったシグナルを記録しますが、シグナルハンドラが実行されるのは特定の チェックポイント においてだけです。チェックポイントはハンドラが非同期に実行されると考えても良い程度に頻繁に設けられています。通常アロケーションやループ制御、システムとのやり取り (システムコールを含む) のときにチェックポイントが設けられます。ループが含まれず、アロケーションをせず、システムとのやり取りをしないプログラムについて、OCamlはメインプログラムとシグナルハンドラの実行が互い違いにならないことを保証しています。そのため例えばアロケートされない値 (整数および真偽値など。小数値は含まれません!) の参照セルは上記の状況でも競合状態に陥りません。

4.4  シグナルのマスク

シグナルはブロックできます。ブロックされたシグナルは無視されるわけではなく、後で届けられるよう待機状態になります。システムコール sigprocmask を使うとシグナルのマスクすることができます。

val sigprocmask : sigprocmask_command -> int list -> int list

sigprocmask cmd sigs はブロックするシグナルを変更し、この呼び出しの前にブロックされていたシグナルのリストを返します。返り値によってマスクを元の状態に戻すことが可能になります。引数 sigs はシグナルのリストであり、 sigprocmask_command 型の値 cmd によって呼び出しの効果が異なります:

SIG_BLOCKsigs 内のシグナルがブロックするシグナルのリストに追加される。
SIG_UNBLOCKsigs 内のシグナルがブロックするシグナルのリストから削除される。
SIG_SETMASKsigs 内のシグナルがブロックするシグナルとなる。

典型的な sigprocmask の利用法はあるシグナルをマスクすることです。

let old_mask = sigprocmask cmd sigs in (* 処理を行う *) let _ = sigprocmask SIG_SETMASK old_mask

次のパターンを使うと起こりがちなエラーを防ぐことができます。

let old_mask = sigprocmask cmd sigs in let treat () = (* 処理を行う *) in let reset () = ignore (sigprocmask SIG_SETMASK old_mask) in Misc.try_finalize treat () reset ()

4.5  シグナルとシステムコール

いくつかのシステムコールは無視されていないシグナルによって中断されます。このようなシステムコールは 遅い システムコールと呼ばれる、実行にいくらでも長い時間がかかりうるもの (例えば端末との i/oselectsystem など) です。割り込みが起こった場合、これらのシステムコールは実行を完了しないまま EINTR 例外を出します。

一方ファイル i/oは割り込みされません。ファイル i/o中に実行中のプロセスを中断して他のプロセスを実行することはありますが、ディスクが正常に機能しているならばこの中断は常に短時間になります。ゆえにデータのスループットはシステムのみに依存し、他のユーザのプロセスから影響を受けることはありません。

無視されたシグナルは届かず、マスクされたシグナルはマスクを解くまで届きません。しかしそれ以外の場合はシステムコールをマスクしていないシステムコールから守る必要があります。典型的な例は子プロセスの終了を待っている親プロセスです。子のプロセス ID を wait とすると親は waitpid [] pid を実行しますが、waitpid はブロックするシステムコールなので 遅い システムコールであり、シグナルによって中断される可能性があります。特に子プロセスが終了したときには sigchld シグナルが親に送られます。

Misc モジュールの restart_on_EINTR 関数はシステムコールが中断されたとき、すなわち EINTR 例外が出たときにシステムコールをやり直します。

let rec restart_on_EINTR f x = try f x with Unix_error (EINTR, _, _) -> restart_on_EINTR f x

子プロセスの終了をシグナルで中断されないように待つには、restart_on_EINTR (waitpid flags) pid を呼びます。

子プロセスの返り値が親プロセスにとって重要でない場合、 sigchld のシグナルハンドラによって子プロセスを非同期に回収することも可能です。ただし短い時間に何度も同じシグナルを受け取った場合シグナルハンドラが一度しか起動しないことがあるので、sigchld を受け取ったときに子プロセスがいくつ終了したかを知ることはできません。このことから、sigchld シグナルを処理するためには以下のライブラリ関数 Misc.free_chidlren が必要になります。

let free_children _ = try while fst (waitpid [ WNOHANG ] (-1)) > 0 do () done with Unix_error (ECHILD, _, _) -> ()

free_childrenwaitpid をノンブロッキングモード (WNOHANG オプション) で実行して実行を終了した子プロセスを回収する、という処理を子プロセスが全て実行中となる (waitpid が子のプロセス ID ではなく 0 を返す) か子プロセスがなくなる (ECHILD 例外が出される) まで繰り返します。

waitpidWNOHANG オプションでノンブロッキングとなっているので EINTR に対する処理は必要ありません。

* * *

Unix モジュールの system 関数は次のように単純に定義されています:

let system cmd = match fork () with | 0 -> begin try execv "/bin/sh" [| "/bin/sh"; "-c"; cmd |] with _ -> exit 127 end | id -> snd (waitpid [] id);;

C 標準ライブラリの system 関数の規格では、親プロセスは sigintsigquitを無視しコマンドの実行中は sigchld をマスクするようになっています。これによって親プロセスに影響を与えること無く子プロセスを中断したり終了させることが可能になります。

ここでは system 関数をより一般的な exec_as_system 関数の特殊化として定義します。この関数ではシェルを必ずしも起動されません。

1 let exec_as_system exec args = 2 let old_mask = sigprocmask SIG_BLOCK [ sigchld ] in 3 let old_int = signal sigint Signal_ignore in 4 let old_quit = signal sigquit Signal_ignore in 5 let reset () = 6 ignore (signal sigint old_int); 7 ignore (signal sigquit old_quit); 8 ignore (sigprocmask SIG_SETMASK old_mask) in 9 let system_call () = match fork () with 10 | 0 -> 11 reset (); 12 (try exec args with _ -> exit 127) 13 | k -> 14 snd (restart_on_EINTR (waitpid []) k) in 15 try_finalize system_call () reset ();; 16 17 let system cmd = 18 exec_as_system (execv "/bin/sh") [| "/bin/sh"; "-c"; cmd |];;

シグナルの変更は fork の実行の前に行う必要があることに注意してください。フォークを実行した後にシグナルを変更するようにすると、シグナルが変更しきる前にシグナル (例えばすぐに終了した子プロセスからの sigchld) を受け取ってしまう可能性があるためです。シグナルの変更は子プロセスではコマンドの実行の前に 11 行目でリセットされます。forkexec は無視するシグナルを保存し、 fork はシグナルに対する動作を保存します。exec は無視していないシグナルの動作をデフォルトに戻します。

最後に親プロセスはエラーが起きた場合でもシグナルの変更をリセットする必要があります。このために 15 行目では try_finalize が使われます。

* * *

4.6  時間の経過

時刻に対するレガシーなアプローチ

Unix の初期のバージョンから、時間は秒で測られてきました。そのため互換性が重要となるならば、常に秒単位で時間を計測するべきです。現在時刻は 1970 年 1 月 1 日 00:00:00 gmt からの経過秒数と定義されます。次の関数で取得できます:

val time : unit -> float

システムコール sleep は引数で指定した秒数だけプログラムの実行を止めます:

val time : unit -> float

しかしこの関数は原始的ではありません。さらに基本的なシステムコール alarm (前の節を参照) と sigsuspend を使うことで sleep を実装することができます。

val sigsuspend : int list -> unit

sigsuspend l はリスト l 内のシグナルを一時的に差し止め、無視も差し止めもされていないシグナルを受け取るまでプログラムの実行を停止します。値が返るときにシグナルマスクは元の値に戻ります。

sleep は以下のように実装できます:

1 let sleep s = 2 let old_alarm = signal sigalrm (Signal_handle (fun s -> ())) in 3 let old_mask = sigprocmask SIG_UNBLOCK [ sigalrm ] in 4 let _ = alarm s in 5 let new_mask = List.filter (fun x -> x <> sigalrm) old_mask in 6 sigsuspend new_mask; 7 let _ = alarm 0 in 8 ignore (signal sigalrm old_alarm); 9 ignore (sigprocmask SIG_SETMASK old_mask);;

初期状態では何もしないことが sigalrm シグナルに対する動作です。“何もしない” とはシグナルを無視することとは異なることに注意してください。sigalrm シグナルを受け取ったときにプロセスが起動することを保証するために、このシグナルは無視されないようにします。そのあと sigalrm 以外のマスクされているシグナルを全て差し止めてからスタンバイ状態に移行します。この変更はアラームのシグナルの後で取り消されます。sigsuspend がシグナルマスクを変更しないことから、9 行目は 2 行目の直後に置くこともできます。

* * *

現代的な時刻の取り扱い

現代的なバージョンの Unix では時刻をマイクロ秒単位で測定することができます。OCamlではマイクロ秒単位で測定した時刻は浮動小数点型で表されます。システムコール gettimeofday は現代的なシステムにおいて time の代わりとなるものです。

val gettimeofday : unit -> float

タイマー

現在出回っている Unix では各プロセスはそれぞれ違う時間を測定する 3 つのタイマーを持ちます。タイマーは interval_timer 型の値として確認できます:

ITIMER_REAL実時間 (sigalrm).
ITIMER_VIRTUALユーザ時間 (sigvtalrm).
ITIMER_PROFユーザ時間とシステム時間 (sigprof).

タイマーの状態は interval_timer_status 型のレコードによって表され、各フィールドは以下の時間を表します (どちらも float です):

二つのフィールドが 0 の場合タイマーはアクティブではありません。タイマーは次の関数で取得および変更できます:

val getitimer : interval_timer -> interval_timer_status val setitimer : interval_timer -> interval_timer_status -> interval_timer_status

settimer の返り値は変更前の値です。

練習問題 11

複数のタイマーを管理するために、以下のインターフェースを持つモジュールを書いてください:

module type Timer = sig open Unix type t val new_timer : interval_timer -> (unit -> unit) -> t val get_timer : t -> interval_timer_status val set_timer : t -> interval_timer_status -> interval_timer_status end

new_timer k f はタイプ k の新しいタイマーを作り f の実行を始めます。タイマー作成時にはアクティブではありません。set_timer t はタイマーの値を t に設定し古い値を返します。

* * *

日付の計算

Unix の現代的なバージョンには日付を処理するための関数も含まれています。tm 構造体はカレンダー (年、月など) によって表現でき、gmtimelocaltimemktime などによって他の単位と変換できます。

4.7  シグナルの問題点

シグナルは非同期なので、シグナルをプロセス間通信に使うと制限と困難がいくつかあります:

シグナルは制限された非同期通信であるにもかからわず、非同期通信にまつわる困難や問題は全て含みます。そのため可能であれば利用を避けるべきです。たとえば短い時間だけ待つためには select がアラームの代わりに使えます。ただしコマンドラインのインタープリターなどのシグナルを考えなければいけない状況もあります。

シグナルはおそらく Unix システムの中で最も有用でない概念です。古いバージョンの Unix (System V など) ではシグナルの動作は受け取った後に自動的にSignal_default にリセットされます。そのためシグナルハンドラは自分自身をもう一度設定し直す必要があり、以下のように書く必要がありました:

let rec beep _ = set_signal sigalrm (Signal_handle beep); output_char stdout '\007'; flush stdout; ignore (alarm 30);;

しかし問題はシグナルを受け取ってシグナルの動作が自動的に Signal_default に戻ってからset_signal が実行されるまでの短い間にシグナルを受け取った場合です。この場合シグナルはハンドラを呼びだすことなく、種類に応じて無視されるかプロセスを終了させます。

その他の Unix (bsd と Linux) はより良い動作をします。シグナルを受け取ってもその動作を置き換えず、シグナルを処理している間は他のシグナルは保留されます。


1
これらは端末のデフォルトのキーであり、端末の設定を返ることで変更できます。 2.13 節参照。

Previous Up Next