電柱日報

日々の由無し事

Crystal でシグナル処理

※:この記事は,Crystal Advent Calendar 2018 の8日目です。

プログラムの実行中にキーボードからCtrl+Cが押されたり,バックグラウンドで動作中にkill コマンドが実行されたりした際,プログラムに対してシステムからシグナルが発行されます。

放っておけば,システム標準の動作が行われる(プログラムが強制終了したり)わけですが,時として飛んできたシグナルをトラップして処理したい状況があります。

例えば,Ctrl+C入力時に終了する前に何か後処理をしたい場合や,SIGHUPの受信時に設定を再読み込みしたいような場合などなど。

そうしたシグナル処理を安全に行うために,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 を呼び出さなかった場合,Ctrl+Cが押されるごとに後始末処理は行われますがその時点ではプログラムは終了しなくなります。

設定したシグナルハンドラの解除

Signal#trap メソッドで指定されたハンドラは,Signal#reset メソッドで解除することもできます。

例えば,以下のコードを実行すると,最初の10秒間はCtrl+Cを入力しても "Ctrl+C received" とだけ表示されてプログラムは終了しませんが,その後の10秒間はCtrl+Cによって通常通りプログラムが強制終了されます。

Signal::INT.trap do
  puts "Ctrl+C received"
end

sleep(10)

Signal::INT.reset

sleep(10)

puts "finish"

シグナルの無視

Signal#ignore メソッドを利用すると,該当のシグナルを受け取った際になにも処理をせず,システム標準の動作もさせず無視することができます。

以下のコードを実行した場合,何度 Ctrl+C を入力してもプログラムの動作に一切の影響を与えることはありません。

Signal::INT.ignore

# do something ...

シグナル処理の例外動作

前述した Signal#trapSignal#resetSignal#ignoreの動作にはいくつか例外が存在します。

例えば, SIGKILLSignal::KILL)はシグナルハンドラ内で明示的に exit などが呼び出されなかったとしても,そのハンドラの処理が終了すればプログラムが強制終了されます。また Signal::KILL.ignore などとしても強制終了を免れることはできません。

一方,SIGCHLDSignal::CHLD)はシグナルハンドラが設定されていたとしても,それより先にCrystalの標準的なSIGCHLDハンドラの動作(終了した子プロセスの刈り取り処理など)が実行されます。また,Signal::CHLD.resetSignal::CHLD.ignore を呼び出した場合も,このCrystal標準のSIGCHLDハンドラは実行されます。これは,Process.wait メソッドがこの標準SIGCHLDハンドラの動作に依存していることも理由の1つですが,子プロセスのゾンビ化を防ぐ意味もあります。