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

LFCS — Linux Foundation認定システム管理者

プロセス、シグナル、そして/procがファイルシステムである理由

TT Labで続きを見る

一言でいうと

「プロセスが死なない」という症状は、4つの異なる原因から生じます。シグナルを捕捉して無視している、捕捉できない状態(D)にある、シグナルが見当違いの相手に届いた、そもそも死んでいるのに親が刈り取っていない(ゾンビ)、のどれかです。見分ける方法は、すべて/procにあります。

なぜ必要なのか

デプロイスクリプトがkillを送ったのに、プロセスが残っています。ここからすぐにkill -9に向かう手が問題です。SIGKILLは、アプリケーションに後始末の機会を与えません。開いているファイルのバッファはフラッシュされず、トランザクションは途中で途切れ、ロックファイルは残ります。データベースやキューのコンシューマーにSIGKILLを送ることは、電源を引き抜くのと同じです。

正しい手順は、いつも2段階です。SIGTERMを送り、猶予期間を与え、それでも生きていれば、そのときにSIGKILLです。コンテナランタイムやKubernetesが行うことも、まさにこれで、猶予期間は設定値です。

どう動くのか

状態の1文字が、診断の出発点です。

コード 意味 実務上の含意
R 実行中、または実行待ち CPUを使っているか、もらうために並んでいる
S 割り込み可能な待機 正常な待機。シグナルで起こせる
D 割り込み不可の待機 SIGKILLもすぐには効かない。たいていディスクやNFS
Z ゾンビ すでに死んでいて、親が刈り取っていない状態
T 停止中 SIGSTOPまたはSIGTSTPを受け取った

Dが見えたら、シグナルでは解決しません。カーネルが何を待っているかを、/proc/<PID>/wchanや/proc/<PID>/stackで見る必要があります。

SIGKILLとSIGSTOPは、捕捉することも、ブロックすることも、無視することもできません。残りのシグナルは、すべてアプリケーションが処理方法を変えられます。そのため、kill -TERMが効かないプログラムが存在するのであり、それはバグではなく設計かもしれません。どのシグナルを捕捉しているかは、/proc/<PID>/statusのSigCgt・SigBlk・SigIgnのビットマスクに、そのまま書かれています。

/procがファイルシステムである理由。カーネルの内部データ構造をユーザー空間に見せるには、新しいシステムコールをたくさん作るか、すでにあるインターフェースを再利用する必要があります。Unixは2番目を選びました。そのため、プロセス情報はcatで読め、開いているファイルディスクリプターは/proc/<PID>/fd/の下にシンボリックリンクとして見え、そのリンクをそのままcpすると、削除されたファイルまで復元できます。リソースの上限は/proc/<PID>/limitsにあるので、自分のシェルのulimitではなく、そのプロセスが実際に受け取った上限を確認できます。

niceとulimitは、どちらも継承されます。nice値は-20から19までで、低いほど優先度が高いです。一般ユーザーは値を上げることしかできず、下げられません。ulimitのsoft上限は、hard上限の下であれば自由に調整できますが、hard上限を上げられるのはrootだけです。そして、両方の値とも子プロセスにそのまま継承されるので、シェルで一度下げておいた上限が、その下で起動したすべてのプロセスについて回ります。「なぜこのデーモンだけファイルを開けないのか」の答えが、ここにある場合が多いです。

現場での姿

筆者がまとめたsystemdの問題事例のうち、3つが特によく出ます。1つ目は、Restart=on-failureだけを設定して再起動の制限を置かないと、設定ミスでクラッシュするサービスが無限再起動に入り、CPUを消費してログでディスクを埋めます。制限を設定するStartLimitIntervalSecとStartLimitBurstは、[Service]ではなく、[Unit]セクションのディレクティブなので、間違ったセクションに入れると、静かに無視されます。

2つ目は、ExecStart=ではパイプとリダイレクトが効きません。systemdがシェルを経由せずに直接実行するからです。シェルの機能が必要なら、シェルを明示的に実行する必要があり、ログは標準出力をジャーナルに送るほうがよいです。3つ目は、終了コードに原因が書かれています。203/EXECは、実行パスが間違っているか、実行権限がないことで、217/USERは、User=に書いたアカウントが存在しないことです。

コンテナ側の落とし穴も1つあります。コマンドをシェル形式で書くと、シェルがPID 1になり、アプリケーションはその子になります。このとき、SIGTERMはシェルに送られ、シェルはそれを子に伝えないので、アプリケーションは猶予を一度も受けられず、SIGKILLで死にます。

次のラボですること

このラボ環境では、systemctlが動作しません。そのため、サービスのラボはユニットファイルを正確に書くことに集中します。セクション構成と、実行・再起動のディレクティブ、タイマーユニット、/etc/cron.dとユーザーcrontabの形式の違い、cronの短いPATHの問題、ログローテーションの設定、そして、ドロップインのオーバーライドでリスト型のディレクティブを空にする定型句まで扱います。続くラボでは、起動順序とGRUBの設定を整理し、/procを直接読み、バックグラウンドのプロセスにシグナルを送って状態の変化を確認します。