未処理のスクリプト中断がもたらす隠れたコスト
誰もが経験したことがあるでしょう。50GBのデータベースバックアップを実行中に、出力先パスのタイポに気づいたとします。すぐにプロセスを止めるために Ctrl+C を押します。スクリプトは終了しますが、20GBの不完全なアーカイブ、/tmp に残された古いロックファイル、そしてリモートサーバーへのアクティブなマウントポイントが残ってしまいます。これは単に散らかっているだけではなく、ディスク容量の枯渇や自動化パイプラインの破損を招く原因となります。
過去数年間にわたり15台の Linux VPS インスタンスを管理してきた中で、私はスクリプトが自然に正常終了することは滅多にないということを学びました。クリーンアップの方法を明示的に指示しない限り、スクリプトは何もしてくれません。プロセスが予期せず終了したとき、OSとコードの間で通信が行われないと「ゾンビ」状態が発生します。これを解決するには、Linuxがシグナルを使用してプロセスとどのように対話するかを理解する必要があります。
Linuxシグナル:スクリプトの神経系
シグナルとは、実行中のプログラムに特定の動作をトリガーさせるために送られるソフトウェア割り込みです。これはOSがスクリプトに対して「おい、注目しろ!」と叫ぶようなものです。たとえば、Ctrl+C を押すと、ターミナルは SIGINT (Signal Interrupt) を送信します。システムの再起動時には、カーネルが SIGTERM (Signal Terminate) を送信し、アプリがデータを保存して開いているファイルを閉じるための数秒間の猶予を与えます。
ほとんどの開発者が意識すべきなのは、以下の6つのシグナルです。
- SIGHUP (1): ハングアップ。プロセス全体を停止させずに、サービスに設定ファイルの再読み込みを指示するために使用します。
- SIGINT (2): 割り込み。
Ctrl+Cによって送信される標準的なシグナルです。 - SIGQUIT (3): 終了。割り込みに似ていますが、通常はデバッグのためにコアダンプを強制出力します。
- SIGKILL (9): 最終手段。プロセスを即座に強制終了します。これをキャッチ、ブロック、または無視することはできません。
- SIGTERM (15): 停止の丁寧な依頼。
killコマンドによって送信されるデフォルトのシグナルです。 - EXIT (0): Bash特有の特別なシグナル。スクリプトがクラッシュしたか、強制終了されたか、正常に完了したかにかかわらず、終了時に常にトリガーされます。
システムのシグナルライブラリを確認する
マシンがどのシグナルをサポートしているかを確認するために、特別なツールは必要ありません。UbuntuからArchまで、ほぼすべてのディストリビューションの util-linux パッケージに含まれている kill コマンドで簡単に確認できます。最近の Linux カーネルの多くは、64種類の異なるシグナルをサポートしています。
お使いのマシンで全リストを表示するには、以下を実行します:
kill -l
特定のシグナル名に対応するID(またはその逆)が必要な場合は、以下を使用します:
kill -l SIGINT
# 出力: 2
プロセスを管理する際、正常に終了させるには kill -15 <PID> を使用します。私たちのゴールは、その要求を無視するのではなく、適切に受け取るスクリプトを書くことです。
trapコマンドの実装
trap コマンドは Bash の組み込みコマンドで、シグナルをインターセプトし、スクリプトが終了する前に特定の関数を実行します。ロジックはシンプルで、trap '実行するコマンド' シグナル名 という形式です。
1. リソースクリーンアップの自動化
「残されたファイル」の問題を解決しましょう。終了箇所ごとに rm コマンドを散りばめる代わりに、単一のクリーンアップ関数を定義します。これにより、たとえ50行目でスクリプトが失敗したとしても、一時ファイルが確実に削除されるようになります。
#!/bin/bash
# 一意な一時ファイルを作成
TEMP_FILE=$(mktemp /tmp/backup_log.XXXXXX)
echo "$TEMP_FILE にログを記録中..."
# クリーンアップロジックを定義
cleanup() {
echo -e "\n終了シグナルを受信しました。$TEMP_FILE を削除しています..."
rm -f "$TEMP_FILE"
}
# EXITシグナルをキャッチ
trap cleanup EXIT
# 10秒間のタスクをシミュレート
sleep 10
echo "タスク完了!"
スクリプトが正常に終了しても、強制的に停止させられても、cleanup 関数が実行されます。これにより /tmp ディレクトリは常にクリーンな状態に保たれます。
2. ユーザーによる停止とシステムによる停止を区別する
時には、スクリプトが停止した「理由」を知る必要があります。ユーザーが手動でタスクを中断したのか、それともメンテナンスのためにシステムがシャットダウンしたのかによって、ログ出力を変えたい場合などです。
#!/bin/bash
trap 'echo "ユーザーにより中断されました (Ctrl+C)"; exit 1' SIGINT
trap 'echo "システムシャットダウンが要求されました"; exit 1' SIGTERM
echo "プロセスID: $$"
while true; do sleep 1; done
ここで重要なのは、トラップ内に exit 1 を含めていることです。シグナルをキャッチしても明示的に exit を呼び出さない場合、スクリプトは中断した場所から再開しようとします。終了シグナルが届いた際に、そのような動作を望むことは稀でしょう。
3. 子プロセスのクリーンアップ
複雑な自動化における一般的な悩みは「孤立(ゾンビ)」プロセスです。メインスクリプトがバックグラウンドタスクを開始した後に終了してしまうと、そのバックグラウンドタスクがCPUを消費し続けながら永続的に走り続けてしまうことがあります。trap を使用して、親が終了したときに子プロセスも終了させるようにしましょう。
#!/bin/bash
# バックグラウンドタスクを開始 (例: ログの監視やプロキシ)
sleep 100 &
CHILD_PID=$!
cleanup() {
echo "子プロセス $CHILD_PID をクリーンアップしています..."
kill $CHILD_PID 2>/dev/null
}
trap cleanup EXIT SIGINT SIGTERM
wait $CHILD_PID
トラップのテストとデバッグ
テストされていないコードは、すでに壊れているも同然です。トラップを検証するには、2つのターミナルウィンドウを開きます。1つ目でスクリプトを実行し、2つ目からシグナルを送信します。
# ターミナル 2
pgrep -f my_script.sh
kill -SIGTERM <PID>
トラップが即座に発動しない場合、それは Bash が sleep や外部バイナリなどの「ブロッキング」コマンドの終了を待っているためである可能性が高いです。スクリプトの応答性を高めるには、重いコマンドをバックグラウンドで実行し、wait を使用してください。wait コマンドは、シグナルをキャッチすると即座に中断されるというユニークな特性を持っています。
現在シェルで有効なトラップを確認するには、単に次のように入力します:
trap
本番環境で使えるベストプラクティス
シグナル処理を適切に行うことは、その場しのぎのスクリプトとプロフェッショナルな自動化ツールの分かれ道です。システムを安定させるために、以下のルールに従ってください:
- クリーンアップロジックは簡潔にする: トラップの実行は2〜3秒以内に収めるべきです。ネットワークスタックがすでにシャットダウンしている可能性があるため、複雑なAPIコールやネットワークリクエストは避けてください。
- SIGKILLをキャッチしようとしない: それは不可能です。スクリプトが
kill -9を受けている場合、通常はSIGTERMハンドラーが遅すぎるか、どこかでスタックしていることを意味します。 - 重要なセクションでは「無視」を使用する: 破損が許されない1KBの設定ファイルを書き込む場合などは、
trap '' SIGINTを使用して書き込み中の割り込みを無視し、終了後にtrap - SIGINTで元に戻します。 - トリガーをログに記録する: どのシグナルが終了のきっかけになったかを常に記録してください。cronジョブが失敗した際、それがタイムアウトなのか手動の停止なのかが分からずデバッグに何時間も費やすのを防げます。
これらのテクニックをマスターすることで、Linux の自動化はより堅牢になり、ファイルシステムは整理され、プロセスはプロフェッショナルな精度で通信できるようになります。

