シグナルには、大きく2つの用途があります。
シグナルのライフサイクル
シグナルはさまざまなタイミングで発生(raise)します。0除算エラーや、メモリの範囲外アクセス(セグメント違反)は、CPUレベルで発生し、それを受けてカーネルがシグナルを生成します。アプリケーションプロセスで生成(generate)されるシグナルもあります。 生成されたシグナルは、対象となるプロセスに送信(send)されます。プロセスは、シグナルを受け取ると、現在の処理を中断して受け取ったシグナルの処理を行います。
プロセスは、受け取ったシグナルを無視するか、捕捉して処理(handle)します。 デフォルトの処理は、無視か、プロセスの終了です。 プロセスがシグナルを受け取った場合の処理内容は、事前に登録してカスタマイズ できます。プロセスを終了しない場合は、シグナルを受け取る前に行っていたタスク を継続します。
シグナルの一覧取得
# Linux man 7 signal # mac/BSD man signal
ハンドルできないシグナル
強制力を持ち、アプリケーションではハンドルできないシグナルがあります。
- SIGKILL:プロセスを強制終了
- SIGSTOP:プロセスを一時停止して、バックグラウンドジョブにする
サーバーアプリケーションでハンドルするシグナル
サーバーアプリケーション側で独自のハンドラを書くことが比較的多いシグナルとしては、次のようなものがあります。
- SIGTERM:kill()システムコールやkillコマンドがデフォルトで送信するシグナルで、プロセスを終了させるもの
- SIGHUP:通常は、後述するコンソールアプリケーション用のシグナルだが、「ターミナルを持たないデーモンでは絶対に受け取ることはない」ので、サーバーアプリケーションでは別の意味で使われる。具体的には、設定ファイルの再読み込みを外部から指示する用途で使われることがデファクトスタンダードとなっている
その他Ctrl + Cでプログラムを停止したり、ウィンドウサイズ調整したりする際もシグナルが送られている。
Go言語におけるシグナルの種類
Go 言語では syscall パッケージ内でシグナルを定義しています。たとえば、syscall.SIGINTのように定義されています。なお、SIGINTとSIGKILLの2つに関 してはosパッケージで次のようにエイリアスが設定されていて、全OSで使えることが保証されています。
var (
Interrupt Signal = syscall.SIGINT
Kill Signal = syscall.SIGKILL
)
次のシグナルはPOSIX系OSで使えるシグナルの一覧です。
- ハンドル不可・外部からのシグナルは無視 :SIGFPE、SIGSEGV、SIGBUSが該当。 算術エラー、メモリ範囲外アクセス、その他のハードウェア例外を表す、致命度の高いシグナル。Go言語では、自分のコード中で発生した場合にはpanicに変換して処理される。外部から送付することはできず、ハンドラを定義しても呼ばれない
- ハンドル不可 :SIGKILL、SIGSTOPが該当。Go言語に限らず、C言語でもハンドルできないシグナル
- ハンドル可能・終了ステータス1 :SIGQUIT、SIGABRTが該当
- ハンドル可能・パニック、レジスタ一覧表示、終了ステータス2:SIGILL、SIGTRAP、SIGEMT、SIGSYSが該当
シグナルのハンドラを書く
シグナルはフォアグラウンドのプロセスに最初に送信されます。したがって、自作のコードでシグナルのハンドラを書き、それをgo runを使って実行す ると、シグナルは自作コードのプロセスではなくgoコマンドのプロセスに送信されてしまいます。これを避けるため、シグナルをハンドルするコードはgo runでは実行せず、go buildコマンドで実行ファイルを作成してから実行してください。
package main import ( "fmt" "os" "os/signal" "syscall" ) func main() { signals := make(chan os.Signal, 1) signal.Notify(signals, syscall.SIGINT, syscall.SIGTERM) s := <-signals switch s { case syscall.SIGINT: fmt.Println("SIGINT") case syscall.SIGTERM: fmt.Println("SIGTERM:") } }
go build -o signal main.go ./signal // Ctrl +C、Zで停止するとコンソールにSIGINTなど表示される シグナルを無視するコード。最初の10秒だけCtrl + Cを無視する。
package main import ( "fmt" "os/signal" "syscall" "time" ) func main() { fmt.Println("Accept Ctrl + C for 10 second") time.Sleep(time.Second * 10) signal.Ignore(syscall.SIGINT, syscall.SIGHUP) fmt.Println("Ignore Ctrl + C for 10 second") time.Sleep(time.Second * 10) }
go build -o ignore_signal main.go ./ignore_signal Accept Ctrl + C for 10 second Ignore Ctrl + C for 10 second
シグナルのハンドラをデフォルトに戻す
signal.Reset(syscall.SIGINT, syscall.SIGHUP)
シグナルの送付を停止させる
signal.Stop(signals)
シグナルを他のプロセスに送る
package main import ( "fmt" "os" "strconv" ) func main() { if len(os.Args) < 2 { fmt.Printf("usage: %s [pid]\n", os.Args[0]) return } pid, err := strconv.Atoi(os.Args[1]) if err != nil { panic(err) } process, err := os.FindProcess(pid) if err != nil { panic(err) } process.Signal(os.Kill) process.Kill() }
os/execパッケージを使った高級なインタフェースでプロセスを起動した場合は、Processフィールドにos.Process構造体が格納されているので、この変数経由で送信できます。
cmd := exec.Command("child") cmd.Start() // シグナル送信 cmd.Process.Signal(os.Interrupt)
シグナルの応用例(Server::Starter)
いきなりシャットダウンしてしまうと、アクセス中のユーザーに正しく結果を返すことができません。かといって、自然にユーザーが途切れるまで待つわけにもいきません。複数台のサーバーを利用している場合には、さらに難しくなります。この課題はグレイスフル・リスタートと呼ばれています。 グレイスフル・リスタートを実現するための補助ツールとして広く利用されている仕組みに、奥一穂さんが作成したServer::Starterがあります。Server::Starterは、サーバーの再起動が必要になったときに、「新しいサーバーを起動して新しいリクエストをそちらに流しつつ、古いサーバーのリクエストが完了したら正しく終了させる」ための仕組みです。Server::Starterを利用できるようにサーバーを作れば、サービス停止時間ゼロでサーバーの再起動が可能になります。
Go版のServer::Starterインストール
go get github.com/lestrrat/go-server-starter/cmd/start_server
Server::Starterの使い方
カレントディレクトリにあるserverというサーバープログラムを、Server::Starter で起動するには、次のようにstart_serverというコマンドを使います。
start_server --port 8080 --pid-file app.pid -- ./server
Server::Starter 対応のウェブサーバーのための最小限のコード
サーバーをgoroutineで起動し、SIGTERMシグナルを受け取ったら外部から停止するメソッドを呼び出すようにすれば、簡単に実現できます。
package main import ( "context" "fmt" "github.com/lestrrat/go-server-starter/listener" "net/http" "os" "os/signal" "syscall" ) func main() { signals := make(chan os.Signal, 1) signal.Notify(signals, syscall.SIGTERM) listeners, err := listener.ListenAll() if err != nil { panic(err) } server := http.Server{ Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "server pid: %d %v\n", os.Getpid(), os.Environ()) }), } go server.Serve(listeners[0]) <-signals server.Shutdown(context.Background()) }
Go言語ランタイムにおけるシグナルの内部実装
マルチスレッドのプログラムだと、シグナルはその中のどれかのスレッドに届けられます。マルチスレッドのプログラムでは、リソースアクセス時にロックを取得するスレッドがどれかわからないと容易にブロックしてプログラムがおかしくなってしまうため、シグナル処理用のスレッドとそれ以外のスレッドを分けるのが定石です。Go言語でもそのようになっています。主なコードはruntime/signal_unix.goにあります。