TT Lab
はじめる
学ぶ 学習パス コース

Linux基礎

シグナル: 捕まえられるものと、そうでないもの

TT Labで続きを見る

一言でいうと

シグナルはプロセスに送る短い通知で、そのうちSIGKILL(9)と SIGSTOP(19)の2つだけは、プログラムが捕捉することも、ブロックすることも、無視することもできません。ほかはすべて、アプリケーションが処理方法を変えられます。

なぜ必要なのか

「プロセスが死なない」という文は、実は少なくとも4つの異なる状況をひとまとめにした言い方です。

  1. kill を送ったのに変わらない -> アプリケーションが SIGTERM を捕捉して無視しているか、後始末の最中である
  2. kill -9 も効かない -> プロセスが D 状態(中断不可の待機)である。シグナルはキューにたまっていて、カーネルから戻ってきてから処理される
  3. すでに死んでいるのに一覧に見える -> ゾンビである。死んでいるので、殺すことはできない
  4. コンテナを止めたらデータが途切れた -> シグナルが PID 1 のシェルで止まり、アプリまで届かなかった

4つの状況で、対応はすべて異なります。そのため「死なない」で止まらずに、どの階層で止まっているのかを問う必要があります。

どう動くのか

正常な終了は、常に2段階です。 SIGTERM を送って後始末の時間を与え、猶予時間が過ぎても生きていれば SIGKILL を送ります。docker stop の既定の猶予は10秒で、systemd の TimeoutStopSec も同じ役割を果たします。シェルでは、timeout -k 10 30 명령(最後の語がご自身のコマンドを表します)の1行で同じポリシーを作れます。

SIGKILL が危険なのは、後始末の機会をまったく与えないからです。バッファはフラッシュされず、トランザクションは途中で切れ、ロックファイルや一時ファイルが残ります。DB やキューのコンシューマーにとって、SIGKILL は事実上、電源を引き抜くのと同じです。

よく使うシグナルの慣例も知っておくと役立ちます。番号はアーキテクチャによって異なることがあるので、スクリプトでは常に名前を使います。

名前 既定の動作 慣例的な用途
SIGHUP 終了 設定の再読み込み(本来の意味は端末の接続切断)
SIGINT 終了 Ctrl-C
SIGTERM 終了 丁寧な終了要求
SIGKILL 終了 捕捉できない。最後の手段
SIGSTOP / SIGCONT 停止 / 再開 しばらく止めてから再開
SIGUSR1 / SIGUSR2 終了 アプリ次第(nginx ではログの再オープン / バイナリの入れ替え)

ゾンビと孤児は、対で理解します。子が死ぬと、カーネルは終了ステータスを残しておき、親が wait でそれを回収してはじめて一覧から消えます。回収前の状態がゾンビ(Z)です。ゾンビはメモリをほとんど使いませんが、PID を占有するため、数千個たまると新しいプロセスを作れなくなります。逆に、親が先に死ぬと子は孤児になって PID 1 に引き取られ、PID 1 が代わりに回収してくれます。ゾンビを消す確実な方法が「親を終了させること」である理由がここにあります。

終了コードの規約も覚えておいてください。シグナルで死んだ場合、終了コードは 128 + 시그널 번호(128 にシグナル番号を足した値)です。SIGTERM は 143、SIGINT は 130 です。CI のログで 143 を見たら、「誰かが丁寧に終了を要求した」と読めば大丈夫です。

現場での姿

コンテナのシェル形式の罠。 Dockerfile に CMD myapp と書くと、/bin/sh -c が PID 1 になり、アプリはその子になります。ランタイムが送った SIGTERM はシェルが受け取り、シェルは子に転送しません。猶予時間が過ぎると、SIGKILL が cgroup 全体に適用され、アプリは何の後始末もできずに死にます。解決策は、exec 形式(CMD ["myapp"])にするか、init プロセスを置くことです。

pkill の射程。 pkill -f はコマンドライン全体をパターンマッチするので、広く使うと無関係なプロセスまで殺してしまいます。必ず同じパターンで先に pgrep -a -f を実行し、一覧を目で確認してから実行します。そして kill -9 -1 は絶対に実行してはいけません。権限が届くすべてのプロセスに SIGKILL を送ります。

trap の実行が遅れる理由。 シェルスクリプトで sleep 300 の実行中に届いたシグナルは、そのコマンドが終わったあとに処理されます。すぐに反応させたければ、sleep 300 & wait $! の形で書きます。

死なないプロセスと終わらない終了

kill を送ったのに死なない状況は、3つに分かれます。どれなのかがわかれば、次の一手が決まります。

シグナルを無視している。 プログラムが SIGTERM のハンドラを持ち、その中で後始末をしているうちに詰まっているか、最初から無視するように作られています。SIGKILL(9)はプロセスが捕捉できないので必ず死にますが、後始末を飛ばします — 開いているファイルを閉じず、ロックを解放せず、進行中の書き込みを捨てます。データベースやキューのコンシューマーに対して先に 9 を使うと、復旧が必要になります。

cat /proc/<pid>/status | grep -E 'SigBlk|SigIgn|SigCgt'

D 状態でカーネルの中で眠っている。 ps の状態が D(uninterruptible sleep)なら、ディスクや NFS の応答を待っているところで、9 を送っても死にません。 カーネルがそのシステムコールから戻ってきてはじめてシグナルを処理するからです。このとき直すべきなのはプロセスではなく、その下にあるストレージです。

ps -eo pid,stat,wchan:20,cmd | awk '$2 ~ /D/'

すでに死んでいるのに親が回収していない。 状態が Z(ゾンビ)なら、そのプロセスはすでに終わっており、終了コードだけが残っています。ゾンビ自体はリソースをほとんど使いませんが、たまると PID が尽きます。殺すべき対象はゾンビではなく親です。コンテナでゾンビがたまるのは、PID 1 が回収しないからで、--init や tini がその役目を埋めます。

終了の順序を自分で決める。 サービスなら、SIGTERM を受け取ったら、新しいリクエストの受け付けをやめ、処理中のものを終わらせ、そのあとで終了する必要があります。この順序がないと、デプロイのたびにリクエストが途切れます。時間が経っても終わらなければ、そのときに強制的に殺すのが正しいやり方です。

timeout -s TERM -k 30s 5m ./worker    # TERM 뒤 30초 안에 안 끝나면 KILL

シグナルはプロセスグループに届く。 シェルでの Ctrl+C は、フォアグラウンドのプロセスグループ全体に SIGINT を送ります。スクリプトが子を残して死ぬ問題は、たいてい子が別のグループにいるからです。kill -- -<pgid> でグループ全体に送ります。

次のラボですること

バックグラウンドのプロセスを起動して PID と親 PID を確認し、停止・終了・強制終了をそれぞれ送って状態がどう変わるかを見ます。SIGTERM を捕捉するスクリプトを作って、SIGKILL は捕捉されないことを自分で証明し、ゾンビを1つ作ってみたあと、最後にパターンに合うプロセスを安全に片付けるスクリプトを書きます。