Crystal でシグナル処理
※:この記事は,Crystal Advent Calendar 2018 の8日目です。
プログラムの実行中にキーボードからkill
コマンドが実行されたりした際,プログラムに対してシステムからシグナルが発行されます。
放っておけば,システム標準の動作が行われる(プログラムが強制終了したり)わけですが,時として飛んできたシグナルをトラップして処理したい状況があります。
例えば,
そうしたシグナル処理を安全に行うために,Crysltalには Signal 型が用意されています。
Signal 型
Crystal の Signal 型は列挙型(enum)として定義されており,以下のシグナルがメンバーとして登録されているほか,シグナルハンドラの設定/解除するなど,いくつかのインスタンスメソッドが定義されています。
enum Signal : Int32 # ... HUP = 1 INT = 2 QUIT = 3 ILL = 4 TRAP = 5 IOT = 6 ABRT = 6 FPE = 8 KILL = 9 BUS = 7 SEGV = 11 SYS = 31 PIPE = 13 ALRM = 14 TERM = 15 URG = 23 STOP = 19 TSTP = 20 CONT = 18 CHLD = 17 TTIN = 21 TTOU = 22 IO = 29 XCPU = 24 XFSZ = 25 VTALRM = 26 USR1 = 10 USR2 = 12 WINCH = 28 # ... end
シグナルハンドラの設定
Signal 型の各メンバーに対して,Signal#trap
メソッドを使用すると対応するシグナルの受信時の処理を行うシグナルハンドラを指定することができます。
# Ctrl+C(SIGINT)時に何か後始末(cleanup)をしてから終了する Signal::INT.trap do cleanup exit(0) end # SIGHUP 時に設定を再読み込み(reload_config)する Signal::HUP.trap do reload_config end
このようにしてシグナルハンドラを設定したした場合,基本的にシグナルハンドラが未設定の場合のシステム標準動作は行われなくなりますので注意してください。
例えば,上の例の Signal::INT.trap do ... end
内で exit
を呼び出さなかった場合,
設定したシグナルハンドラの解除
Signal#trap
メソッドで指定されたハンドラは,Signal#reset
メソッドで解除することもできます。
例えば,以下のコードを実行すると,最初の10秒間は
Signal::INT.trap do puts "Ctrl+C received" end sleep(10) Signal::INT.reset sleep(10) puts "finish"
シグナルの無視
Signal#ignore
メソッドを利用すると,該当のシグナルを受け取った際になにも処理をせず,システム標準の動作もさせず無視することができます。
以下のコードを実行した場合,何度
Signal::INT.ignore # do something ...
シグナル処理の例外動作
前述した Signal#trap
,Signal#reset
,Signal#ignore
の動作にはいくつか例外が存在します。
例えば, SIGKILL(Signal::KILL)はシグナルハンドラ内で明示的に exit
などが呼び出されなかったとしても,そのハンドラの処理が終了すればプログラムが強制終了されます。また Signal::KILL.ignore
などとしても強制終了を免れることはできません。
一方,SIGCHLD(Signal::CHLD)はシグナルハンドラが設定されていたとしても,それより先にCrystalの標準的なSIGCHLDハンドラの動作(終了した子プロセスの刈り取り処理など)が実行されます。また,Signal::CHLD.reset
や Signal::CHLD.ignore
を呼び出した場合も,このCrystal標準のSIGCHLDハンドラは実行されます。これは,Process.wait
メソッドがこの標準SIGCHLDハンドラの動作に依存していることも理由の1つですが,子プロセスのゾンビ化を防ぐ意味もあります。