Bash スクリプトで Linux シグナルを使用する方法

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コマンドをスクリプト内で使用できます。このスクリプトは、SIGINTSIGQUIT、および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 つの異なるシグナル(SIGHUPSIGINT、および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()と呼ばれます。

SIGUSR1SIGUSR2は、スクリプトにカスタムシグナルを送信できるように提供されるシグナルです。それらをどのように解釈して反応するかは、すべてあなた次第です。

シグナルハンドラーはさておき、スクリプトの本体は馴染み深いもののはずです。ターミナルウィンドウにプロセス ID をエコーし、いくつかの変数を作成します。変数sigusr1_countSIGUSR1が処理された回数を記録し、sigint_countSIGINTが処理された回数を記録します。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 スクリプトは予期せず終了しても後始末をさせることができます。これにより、ファイルシステムがクリーンになります。また、次回スクリプトを実行したときの不安定性を防ぐことができ、スクリプトの目的によってはセキュリティホールを防ぐことさえできます。