Linux カーネルは、プロセスに反応する必要があるイベントに関するシグナルを送信します。適切に動作するスクリプトは、シグナルをエレガントかつ堅牢に処理し、Ctrl+C を押しても後始末を行うことができます。その方法をご紹介します。
シグナルとプロセス
シグナルは、スクリプト、プログラム、デーモンなどのプロセスに送信される、短くて高速な一方向メッセージです。シグナルは、プロセスに何らかのイベントが発生したことを知らせます。ユーザーが Ctrl+C を押した可能性があるか、アプリケーションがアクセス権のないメモリに書き込もうとした可能性があります。
プロセスの作成者が特定のシグナルが送信される可能性を想定している場合、そのシグナルを処理するためのルーチンをプログラムまたはスクリプトに記述できます。このようなルーチンは、シグナルハンドラーと呼ばれます。シグナルをキャッチまたはトラップし、それに応じて何らかのアクションを実行します。
Linux は、後述のとおり、多くのシグナルを使用しますが、スクリプティングの観点からは、関心のあるシグナルのサブセットはごくわずかです。特に、複雑なスクリプトでは、スクリプトにシャットダウンを指示するシグナルをトラップし(可能な場合)、正常にシャットダウンを実行する必要があります。
たとえば、一時ファイルを作成したり、ファイアウォールのポートを開いたりするスクリプトには、シャットダウン前に一時ファイルを削除したり、ポートを閉じたりする機会を与えることができます。スクリプトがシグナルを受信した瞬間に終了すると、コンピューターが予測不可能な状態になる可能性があります。
スクリプトでシグナルを処理する方法を以下に示します。
シグナルの確認
一部の Linux コマンドには不可解な名前が付けられています。シグナルをトラップするコマンドはそうではありません。それはtrap
と呼ばれます。また、trap
と-l
(リスト)オプションを併用して、Linux で使用されるシグナルの全リストを表示することもできます。
trap -l
番号付きリストは 64 で終わっていますが、実際には 62 のシグナルがあります。シグナル 32 と 33 はありません。これらは Linux では実装されていません。リアルタイムスレッドを処理するためのgcc
コンパイラーの機能に置き換えられています。シグナル 34(SIGRTMIN
)からシグナル 64(SIGRTMAX
)までのすべてがリアルタイムシグナルです。
Unix 系オペレーティングシステムによって異なるリストが表示されます。たとえば、OpenIndiana では、シグナル 32 と 33 が存在し、合計数を 73 にする追加のシグナルがいくつかあります。
シグナルは、名前、番号、または短縮名で参照できます。短縮名は、単に先頭の「SIG」を取り除いた名前です。
シグナルは、さまざまな理由で発生します。解読できれば、その目的は名前に含まれています。シグナルの影響は、いくつかのカテゴリのいずれかに分類されます。
- 終了: プロセスが終了します。
- 無視: シグナルはプロセスに影響を与えません。これは情報のみのシグナルです。
- コア: ダンプコアファイルが作成されます。これは通常、プロセスがメモリ違反などの何らかの方法で違反したために実行されます。
- 停止: プロセスが停止します。つまり、一時停止され、終了されません。
- 続行: 停止したプロセスに実行を続行するように指示します。
これらは、最も頻繁に遭遇するシグナルです。
- SIGHUP: シグナル 1。リモートホスト(SSH サーバーなど)への接続が予期せず切断された場合、またはユーザーがログアウトした場合。このシグナルを受信したスクリプトは、正常に終了するか、リモートホストに再接続を試行することを選択できます。
- SIGINT: シグナル 2。ユーザーが Ctrl+C の組み合わせを押してプロセスを強制終了した場合、または
kill
コマンドがシグナル 2 で使用された場合。技術的には、これは終了シグナルではなく割り込みシグナルですが、シグナルハンドラーのない割り込まれたスクリプトは通常終了します。 - SIGQUIT: シグナル 3。ユーザーが Ctrl+D の組み合わせを押してプロセスを強制終了した場合、または
kill
コマンドがシグナル 3 で使用された場合。 - SIGFPE: シグナル 8。プロセスがゼロ除算などの不正な(不可能な)数学演算を実行しようとしました。
- SIGKILL: シグナル 9。これはギロチンのシグナルと同等です。キャッチしたり無視したりすることはできず、即座に発生します。プロセスはすぐに終了します。
- SIGTERM: シグナル 15。これは、
SIGKILL
のより配慮されたバージョンです。SIGTERM
はプロセスに終了するように指示しますが、トラップすることができ、プロセスは終了する前にクリーンアッププロセスを実行できます。これにより、正常にシャットダウンできます。これは、kill
コマンドによって発生するデフォルトのシグナルです。
コマンドラインでのシグナル
シグナルをトラップする方法の 1 つは、シグナルの番号または名前と、シグナルを受信した場合に発生する応答を指定してtrap
を使用することです。これはターミナルウィンドウで示すことができます。
このコマンドは、SIGINT
シグナルをトラップします。応答は、ターミナルウィンドウにテキスト行を出力することです。「\n
」フォーマット指定子を使用できるように、echo
で-e
(エスケープを有効にする)オプションを使用しています。
trap 'echo -e "\nCtrl+c Detected."' SIGINT
Ctrl+C の組み合わせを押すたびに、1 行のテキストが出力されます。
シグナルにトラップが設定されているかどうかを確認するには、-p
(トラップの印刷)オプションを使用します。
trap -p SIGINT
オプションを指定せずにtrap
を使用すると、同じことが行われます。
シグナルをトラップされていない通常の状態にリセットするには、ハイフン「-
」とトラップされたシグナルの名前を使用します。
trap - SIGINT
trap -p SIGINT
trap -p
コマンドから出力が得られないということは、そのシグナルにトラップが設定されていないことを示しています。
スクリプトでのシグナルのトラップ
同じ一般的な形式のtrap
コマンドをスクリプト内で使用できます。このスクリプトは、SIGINT
、SIGQUIT
、およびSIGTERM
の 3 つの異なるシグナルをトラップします。
#!/bin/bash trap "echo I was SIGINT terminated; exit" sIGINT trap "echo I was SIGQUIT terminated; exit" SIGQUIT trap "echo I was SIGTERM terminated; exit" SIGTERM echo $$ counter=0 while true do echo "Loop number:" $((++counter)) sleep 1 done
3 つのtrap
ステートメントはスクリプトの先頭にあります。それぞれのシグナルに対する応答内にexit
コマンドを含めていることに注意してください。これは、スクリプトがシグナルに反応してから終了することを意味します。
テキストをエディターにコピーして「simple-loop.sh」という名前のファイルに保存し、chmod
コマンドを使用して実行可能にします。自分のコンピューターで作業を続ける場合は、この記事のすべてのスクリプトに対してこれを行う必要があります。それぞれの場合に、適切なスクリプトの名前を使用するだけです。
chmod +x simple-loop.sh
スクリプトの残りの部分は非常に簡単です。スクリプトのプロセス ID を知る必要があるため、スクリプトにそれをエコーさせます。$$
変数にはスクリプトのプロセス ID が保持されます。
counter
という名前の変数を作成して、それをゼロに設定します。
while
ループは、強制的に停止されない限り、永遠に実行されます。counter
変数をインクリメントし、画面にエコーし、1 秒間スリープします。
スクリプトを実行して、さまざまなシグナルを送信してみましょう。
./simple-loop.sh
「Ctrl+C」を押すと、メッセージがターミナルウィンドウに出力され、スクリプトが終了します。
もう一度実行して、kill
コマンドを使用してSIGQUIT
シグナルを送信してみましょう。別のターミナルウィンドウからこれを行う必要があります。自分のスクリプトによって報告されたプロセス ID を使用する必要があります。
./simple-loop.sh
kill -SIGQUIT 4575
想定どおり、スクリプトはシグナルの到着を報告してから終了します。最後に、この点を証明するために、SIGTERM
シグナルでもう一度実行してみます。
./simple-loop.sh
kill -SIGTERM 4584
スクリプトで複数のシグナルをトラップし、それぞれに個別に反応できることを確認しました。これを単なる興味深いものから有用なものへと昇格させるステップは、シグナルハンドラーを追加することです。
スクリプトでのシグナルの処理
応答文字列をスクリプト内の関数の名前に置き換えることができます。trap
コマンドは、シグナルが検出されるとその関数を呼び出します。
このテキストをエディターにコピーして「grace.sh」という名前のファイルに保存し、chmod
で実行可能にします。
#!/bin/bash trap graceful_shutdown SIGINT SIGQUIT SIGTERM graceful_shutdown() { echo -e "\nRemoving temporary file:" $temp_file rm -rf "$temp_file" exit } temp_file=$(mktemp -p /tmp tmp.XXXXXXXXXX) echo "Created temp file:" $temp_file counter=0 while true do echo "Loop number:" $((++counter)) sleep 1 done
スクリプトは、1 つのtrap
ステートメントを使用して、3 つの異なるシグナル(SIGHUP
、SIGINT
、およびSIGTERM
)のトラップを設定します。応答は、graceful_shutdown()
関数の名前です。この関数は、3 つのトラップされたシグナルのいずれかが受信されたときに呼び出されます。
このスクリプトは、mktemp
を使用して「/tmp」ディレクトリに一時ファイルを作成します。ファイル名のテンプレートは「tmp.XXXXXXXXXX」なので、ファイル名は「tmp.」の後に 10 個のランダムな英数字が続きます。ファイル名は画面にエコーされます。
スクリプトの残りの部分は前のスクリプトと同じで、counter
変数と無限のwhile
ループがあります。
./grace.sh
ファイルが終了を引き起こすシグナルを送信されると、graceful_shutdown()
関数が呼び出されます。これにより、一時ファイルが削除されます。現実の状況では、スクリプトに必要なクリーンアップを実行できます。
また、トラップしたシグナルをすべてまとめて、1 つの関数で処理しました。シグナルを個別にトラップして、専用のハンドラー関数に送信できます。
このテキストをコピーして「triple.sh」という名前のファイルに保存し、chmod
コマンドを使用して実行可能にします。
#!/bin/bash trap sigint_handler SIGINT trap sigusr1_handler SIGUSR1 trap exit_handler EXIT function sigint_handler() { ((++sigint_count)) echo -e "\nSIGINT received $sigint_count time(s)." if [[ "$sigint_count" -eq 3 ]]; then echo "Starting close-down." loop_flag=1 fi } function sigusr1_handler() { echo "SIGUSR1 sent and received $((++sigusr1_count)) time(s)." } function exit_handler() { echo "Exit handler: Script is closing down..." } echo $$ sigusr1_count=0 sigint_count=0 loop_flag=0 while [[ $loop_flag -eq 0 ]]; do kill -SIGUSR1 $$ sleep 1 done
スクリプトの先頭で 3 つのトラップを定義します。
- 1 つは
SIGINT
をトラップし、sigint_handler()
というハンドラーがあります。 - 2 つ目は
SIGUSR1
というシグナルをトラップし、sigusr1_handler()
というハンドラーを使用します。 - 3 つ目のトラップは
EXIT
シグナルをトラップします。このシグナルは、スクリプトが終了するときにスクリプト自体によって発生します。EXIT
のシグナルハンドラーを設定すると、スクリプトが終了したときに必ず呼び出される関数(シグナルSIGKILL
で終了しない限り)を設定できます。ハンドラーはexit_handler()
と呼ばれます。
SIGUSR1
とSIGUSR2
は、スクリプトにカスタムシグナルを送信できるように提供されるシグナルです。それらをどのように解釈して反応するかは、すべてあなた次第です。
シグナルハンドラーはさておき、スクリプトの本体は馴染み深いもののはずです。ターミナルウィンドウにプロセス ID をエコーし、いくつかの変数を作成します。変数sigusr1_count
はSIGUSR1
が処理された回数を記録し、sigint_count
はSIGINT
が処理された回数を記録します。loop_flag
変数はゼロに設定されています。
while
ループは無限ループではありません。loop_flag
変数がゼロ以外の値に設定されている場合は、ループを停止します。while
ループの各スピンはkill
を使用してSIGUSR1
シグナルをこのスクリプトに送信し、スクリプトのプロセス ID に送信します。スクリプトはそれ自体にシグナルを送信できます!
sigusr1_handler()
関数はsigusr1_count
変数をインクリメントし、メッセージをターミナルウィンドウに送信します。
SIGINT
シグナルが受信されるたびに、siguint_handler()
関数はsigint_count
変数をインクリメントし、その値をターミナルウィンドウにエコーします。
sigint_count
変数が 3 と等しい場合、loop_flag
変数は 1 に設定され、シャットダウンプロセスが開始されたことをユーザーに知らせるメッセージがターミナルウィンドウに送信されます。
loop_flag
がゼロと等しくなくなったため、while
ループは終了し、スクリプトは終了します。しかし、そのアクションにより自動的にEXIT
シグナルが発生し、exit_handler()
関数が呼び出されます。
./triple.sh
Ctrl+C を 3 回押すと、スクリプトは終了し、自動的にexit_handler()
関数が呼び出されます。
シグナルの読み取り
シグナルをトラップし、わかりやすいハンドラー関数で処理することで、Bash スクリプトは予期せず終了しても後始末をさせることができます。これにより、ファイルシステムがクリーンになります。また、次回スクリプトを実行したときの不安定性を防ぐことができ、スクリプトの目的によってはセキュリティホールを防ぐことさえできます。
コメントする