- 物理機リージョン/AZ
- ハイパーバイザVM exit で捕まえる
- VMゲストOS
- コンテナnamespace / cgroup
- プロセスアプリ / DB今ここ
本コース2本目『割り込み — 頼んでいないのに止められる』で、CPU が外から止められる割り込みを見ました。今回はその「プロセス版」。届く先がプロセスになると何が変わるかを見ます。
まず結論
シグナルは、実行中のコードと無関係にプロセスへ届く通知です。シグナルごとに既定の動作が決まっていて、多くは受け取った側のコードを一行も通さずにプロセスを終わらせます。
既定を変えられるシグナルと、変えられないシグナルがあります。そして bash の `trap` は、変えられないシグナルも文句を言わずに受理します。受理されたことと、効いていることは別です。
シェルスクリプトに `trap 'echo cleanup' KILL` と書きました。このスクリプトに SIGKILL を送ると、どうなるでしょうか。
当てることが目的ではありません。外した予測ほど記憶に残るので、直感で選んでください。
届く先がプロセスになった割り込み
本コース2本目で、CPU が外から割り込まれて処理を中断することを見ました。**シグナルはその「プロセス版」**です。届く先が CPU ではなくプロセスになります。
signal(7) は、各シグナルが現在の扱い(disposition)を持ち、それが「届いたときにどう振る舞うか」を決めると書いています。扱いを変えていない場合が既定動作で、定義は5種類です。
Action 定義(原文)Term Default action is to terminate the process.Ign Default action is to ignore the signal.Core Default action is to terminate the process and dump core (see core(5)).Stop Default action is to stop the process.Cont Default action is to continue the process if it is currently stopped.
プロセス側が選べる振る舞いは、同じ man に3つだけです。既定の動作を行う / 無視する / ハンドラで捕まえる(原文は perform the default action / ignore the signal / catch the signal with a signal handler)。
注意したいのは、既定動作の多くが Term、つまり終了だということです。扱いを変えていなければ、届いた時点でコードは一行も走らずに終わります。ログが残らないのはそのためです。
実測: SIGTERM は 143、SIGKILL は 137
bash に自分の子プロセスを作らせ、そこへシグナルを送って終了コードを見ます。
bash version: 5.2.37(1)-release=== [1] TERM to my own sleep ===child pid=7exit code after SIGTERM = 143=== [2] KILL to my own sleep ===child pid=8exit code after SIGKILL = 137=== [3] trap TERM (catchable) vs trap KILL (refused) ===bash: line 10: 8 Killed sleep 100CAUGHT-SIGTERMexit code with TERM handler = 7--- try to trap SIGKILL/SIGSTOP in bash ---trap returned: 0=== [4] signal numbers ===159128+15 = 143 ; 128+9 = 137
143 と 137 の出どころは Bash の規約です。マニュアルの逐語。
When a command terminates on a fatal signal whose number is N, Bash uses the value 128+N as the exit status.
主語は Bash です。カーネルが返す数ではありません。 仮想化編『cgroup で資源を配る — 絞られるものと殺されるもの』で、OOM killer に殺されたコンテナの 137 を同じ理由で読みました。今回はそれを送る側から見ています。
カーネルが親に渡すのは終了ステータス語で、WIFEXITED / WEXITSTATUS / WIFSIGNALED / WTERMSIG によって「正常終了か、シグナル死か」を別々のビットとして読む形です。128+N は、それをシェルが1バイトに押し込む符号化です。
そして === [3] === です。SIGTERM に trap 'echo CAUGHT-SIGTERM; exit 7' TERM を仕掛けると、終了コードは 143 ではなく 7。捕まえられたシグナルは 128+N になりません。
`signal(7)` は、プロセスがシグナルの扱いとして選べる振る舞いを3つ挙げています。その3つはどれですか。
運べるもの、変えられないもの
よく見る2つの言い方を直します。
1つめ。「シグナルは番号だけを運ぶ」は、標準シグナルに限った話です。 signal(7) は標準シグナルについて Standard signals do not queue. と書き、ブロック中に同じシグナルが複数回生成されても1つだけが保留になると説明しています。一方、Linux は 32〜64 番の 33 種のリアルタイムシグナルを持ち、こちらはキューに積まれ、sigqueue(3) で送れば整数またはポインタを添えられます。さらに標準シグナルでも SA_SIGINFO を使えば、送信元の PID や実 UID(si_pid / si_uid)は取れます。
2つめ。番号は普遍ではありません。 同じ signal(7) に番号表があり、多くのシグナルがアーキテクチャによって違う値を持つと明記されています。
Signal x86/ARM等 Alpha/SPARC MIPS PARISCSIGKILL 9 9 9 9SIGTERM 15 15 15 15SIGSTOP 19 17 23 24SIGUSR1 10 30 16 16SIGCHLD 17 20 18 18SIGBUS 7 10 10 10
SIGKILL と SIGTERM は4アーキテクチャすべてで同じなので、9 と 15、つまり 137 と 143 は安全に書けます。しかし SIGSTOP は 19 / 17 / 23 / 24 とバラバラです。 「捕まえられないのは SIGKILL(9)と SIGSTOP(19)」と番号付きで並べると、後半が環境依存の値になります。番号を出すなら SIGKILL と SIGTERM に留め、SIGSTOP は名前で書くのが安全です。
実測: 受理されるのに効かない
signal(7) には、例外を定める1文があります。
The signals SIGKILL and SIGSTOP cannot be caught, blocked, or ignored.
捕捉・ブロック・無視のいずれもできない。 問題は、この制約がどこで現れるかです。
### Does a bash 'trap KILL' actually protect the process? (it must NOT)readybash: line 5: 7 Killed bash -c 'trap "echo HANDLER-RAN; exit 7" KILL; echo ready; sleep 100'exit code = 137 (143/137 = killed by signal; 7 = handler ran)### C-level: does signal()/sigaction() reject SIGKILL? (EINVAL expected)sigaction(SIGKILL) rc=-1 errno=22 (Invalid argument)sigaction(SIGSTOP) rc=-1 errno=22 (Invalid argument)sigaction(SIGTERM) rc=0 errno=0 (No error information)
上半分。trap "echo HANDLER-RAN; exit 7" KILL は文法エラーになりません。前の実測の trap returned: 0 が示すとおり、bash はこれを終了コード 0 で受理します。 それでも SIGKILL を送ると HANDLER-RAN は出力されず、終了コードは 137 です。
下半分は C レベルです。sigaction を3回呼ぶと、SIGKILL と SIGSTOP は rc=-1, errno=22、つまり EINVAL(Invalid argument)で拒否され、SIGTERM だけが rc=0 で成功します。こちらは失敗が返るので、その場で気づけます。
差は「制約の有無」ではなく、伝わり方です。C は登録時に拒否し、bash は登録を通して送信時に無視します。
「捕まえられないシグナルは SIGKILL(9)と SIGSTOP(19)の2つです」という説明の問題点はどれですか。
SIGTERM — 設定が受理され、実際に効く
既定動作は Term(終了)。扱いを変えなければ 128+15 = 143 で終わります。`trap` で捕まえるとハンドラが走り、終了コードはハンドラが決めた値に。C でも `sigaction` が成功します。
やったこと 実測の結果trap 無しで送る 終了コード 143(= 128+15)trap TERM を設定 trap returned: 0(受理)trap 有りで送る CAUGHT-SIGTERM が出て、終了コード 7C の sigaction(SIGTERM) rc=0 errno=0(成功)
SIGKILL — 設定は受理されるが、効かない
`signal(7)` が捕捉・ブロック・無視のいずれも不可と定めています。bash の `trap` はそれでも受理し、送るとハンドラを飛ばして終了します。C では登録の時点で EINVAL。
やったこと 実測の結果trap 無しで送る 終了コード 137(= 128+9)trap KILL を設定 trap returned: 0(受理される)trap 有りで送る HANDLER-RAN は出ず、終了コード 137C の sigaction(SIGKILL) rc=-1 errno=22(EINVAL)
どちらも「シグナルが来たらこう振る舞え」という設定を書き、どちらもエラー無く受理されました。それでも片方だけが効きます。この差はどこから来るのでしょうか。
設定を書いた時点と、シグナルが届いた時点。2つの時点で何が確認されたかを分けて考えてみてください。
シグナルが届いてから、何が決まるか
- ・2段目の左に落ちたら以降の選択肢は無い。`trap` を書いても経路は変わらない
- ・4段目の 128+N は Bash の規約。カーネルは終了ステータス語を渡すだけ
図の2段目が分岐点です。ここで左に落ちたプロセスには、その後の選択肢がありません。後片付けのコードを書いたかどうかと無関係に、走る機会が無いということです。trap を追加してもこの分岐は動きません。
この章のまとめ
- シグナルは実行中のコードと無関係にプロセスへ届く通知。既定動作は Term / Ign / Core / Stop / Cont の5種で、選べるのは「既定を行う / 無視する / ハンドラで捕まえる」の3つ。
- 「番号だけを運ぶ」は標準シグナル限定。リアルタイムシグナル(32〜64)は
sigqueue(3)で値を運べ、キューにも積まれる。標準シグナルでもSA_SIGINFOなら送信元 PID / UID は取れる。 - SIGKILL と SIGSTOP は捕捉・ブロック・無視のいずれもできない。 ただし番号を並べて書かないこと。SIGKILL は全アーキテクチャで 9 だが、SIGSTOP は 19 / 17 / 23 / 24 と変わる。
- 終了コード 143 / 137 は Bash の規約(128+N)。カーネルが返す数ではない。捕まえられた場合は 128+N にならない(実測で 7)。
- 設定が受理されたことと、効いていることは別。
trap ... KILLは終了コード 0 で受理されるのに走らない。C のsigaction(SIGKILL)は EINVAL。確かめるべきは、送ったときに挙動が変わるかどうか。
自分の現場で確かめる
読んだだけでは身につきません。手元の環境で実際に見てみてください。
理解度チェック
読んだ直後は「分かった」と感じやすいものです。実際に解いて確かめてください。
「シグナルは番号だけを運び、それ以外のデータは一切伝えられない」。この説明の正確さはどれですか。
用語集(6語)
- シグナル
- 実行中のコードと無関係にプロセスへ届く通知。シグナルごとに現在の扱い(disposition)があり、届いたときの振る舞いを決める。
- 既定動作 (default action)
- 扱いを変えていないときの振る舞い。`signal(7)` は Term(終了)/ Ign(無視)/ Core(終了してコアダンプ)/ Stop(停止)/ Cont(停止中なら再開)の5種を定義している。
- シグナルハンドラ
- シグナル到達時に呼ばれる、プログラマが定義した関数。`sigaction(2)` または `signal(2)` で登録する。
- 標準シグナル / リアルタイムシグナル
- 標準シグナルはキューに積まれない。リアルタイムシグナル(Linux では 32〜64 の33種)はキューに積まれ、`sigqueue(3)` で整数やポインタを添えられる。
- EINVAL
- 引数が不正であることを示すエラー番号(22)。`sigaction` に SIGKILL や SIGSTOP を渡すとこの値が errno に入る。
- 終了コード 143 / 137
- 致命的なシグナル N に 128+N を返すシェルの規約による値。SIGTERM は 15 なので 143、SIGKILL は 9 なので 137。カーネルが返す数ではない。