Bash スクリプトで Linux シグナルを使用する方法
公開: 2022-08-09
Linux カーネルは、プロセスが対応する必要があるイベントに関するシグナルをプロセスに送信します。 適切に動作するスクリプトは、シグナルをエレガントかつ堅牢に処理し、Ctrl+C を押しても背後でクリーンアップできます。 方法は次のとおりです。
シグナルとプロセス
シグナルは、スクリプト、プログラム、デーモンなどのプロセスに送信される短くて高速な一方向メッセージです。 それらは、起こったことをプロセスに知らせます。 ユーザーが Ctrl+C を押したか、アプリケーションがアクセス権のないメモリに書き込もうとした可能性があります。
プロセスの作成者が、特定のシグナルがプロセスに送信される可能性があることを予測している場合、そのシグナルを処理するルーチンをプログラムまたはスクリプトに書き込むことができます。 このようなルーチンは、シグナル ハンドラと呼ばれます。 シグナルをキャッチまたはトラップし、それに応じて何らかのアクションを実行します。
後述するように、Linux は多くのシグナルを使用しますが、スクリプトの観点からは、関心のあるシグナルの小さなサブセットしかありません。特に、重要なスクリプトでは、シャットダウンするスクリプトは (可能な場合) トラップされ、適切なシャットダウンが実行されます。
たとえば、一時ファイルを作成したり、ファイアウォール ポートを開いたりするスクリプトには、一時ファイルを削除したり、シャットダウンする前にポートを閉じたりする機会を与えることができます。 スクリプトが信号を受信した瞬間に停止した場合、コンピューターは予測不能な状態になる可能性があります。
独自のスクリプトでシグナルを処理する方法を次に示します。
信号に会う
一部の Linux コマンドにはわかりにくい名前が付いています。 シグナルをトラップするコマンドはそうではありません。 それはtrap
と呼ばれます。 -l
(リスト) オプションを指定してtrap
を使用して、Linux が使用するシグナルの完全なリストを表示することもできます。
トラップ -l
番号付きリストは 64 で終わりますが、実際には 62 の信号があります。 信号 32 と 33 がありません。 Linux には実装されていません。 これらは、リアルタイム スレッドを処理するためのgcc
コンパイラの機能に置き換えられました。 シグナル 34 のSIGRTMIN
からシグナル 64 のSIGRTMAX
までのすべてがリアルタイムシグナルです。
Unix ライクなオペレーティング システムごとに異なるリストが表示されます。 たとえば、OpenIndiana では、シグナル 32 と 33 が存在し、余分なシグナルが多数あるため、合計カウントは 73 になります。
信号は、名前、番号、または短縮名で参照できます。 短縮名は、先頭の「SIG」を削除した名前です。
シグナルはさまざまな理由で発生します。 それらを解読できれば、その目的は名前に含まれています。 シグナルの影響は、いくつかのカテゴリのいずれかに分類されます。
- 終了:プロセスは終了します。
- 無視:シグナルはプロセスに影響しません。 これは情報のみの信号です。
- Core: dump-core ファイルが作成されます。 これは通常、メモリ違反など、プロセスが何らかの方法で違反したために行われます。
- 停止:プロセスが停止されます。 つまり、終了ではなく一時停止です。
- Continue:停止したプロセスに実行を継続するように指示します。
これらは、最も頻繁に遭遇するシグナルです。
- SIGHUP : シグナル 1. リモート ホスト (SSH サーバーなど) への接続が予期せず切断されたか、ユーザーがログアウトしました。 このシグナルを受信したスクリプトは正常に終了するか、リモート ホストへの再接続を試みる可能性があります。
- SIGINT : シグナル 2。ユーザーが Ctrl+C の組み合わせを押してプロセスを強制終了したか、シグナル 2 で
kill
コマンドが使用されました。技術的には、これは中断シグナルであり、終了シグナルではなく、中断されたスクリプトです。通常、シグナルハンドラは終了します。 - SIGQUIT : シグナル 3。ユーザーが Ctrl+D の組み合わせを押してプロセスを強制終了したか、シグナル 3 で
kill
コマンドが使用されました。 - SIGFPE : シグナル 8。プロセスは、ゼロ除算などの不正な (不可能な) 数学演算を実行しようとしました。
- SIGKILL : 信号 9。これはギロチンに相当する信号です。 それを捕まえたり無視したりすることはできず、即座に起こります。 プロセスはただちに終了します。
- SIGTERM : シグナル 15。これは
SIGKILL
のより配慮されたバージョンです。SIGTERM
もプロセスに終了を指示しますが、トラップされる可能性があり、プロセスは終了する前にクリーンアップ プロセスを実行できます。 これにより、正常なシャットダウンが可能になります。 これは、kill
コマンドによって生成されるデフォルトのシグナルです。
コマンド ラインの信号
シグナルをトラップする 1 つの方法は、シグナルの番号または名前、およびシグナルが受信された場合に実行する応答を指定してtrap
を使用することです。 端末ウィンドウでこれを実演できます。
このコマンドはSIGINT
シグナルをトラップします。 応答は、端末ウィンドウに 1 行のテキストを出力することです。 -e
(エスケープを有効にする) オプションをecho
で使用しているため、「 \n
」形式指定子を使用できます。
trap 'echo -e "\nCtrl+c が検出されました。"' SIGINT
Ctrl+C の組み合わせを押すたびに、テキスト行が出力されます。
シグナルにトラップが設定されているかどうかを確認するには、 -p
(トラップの出力) オプションを使用します。
トラップ -p SIGINT
オプションなしでtrap
を使用すると、同じことが行われます。
信号をトラップされていない通常の状態にリセットするには、ハイフン「 -
」とトラップされた信号の名前を使用します。
トラップ - SIGINT
トラップ -p SIGINT
trap -p
コマンドからの出力がないということは、そのシグナルにトラップが設定されていないことを示しています。
スクリプトでシグナルをトラップする
スクリプト内で同じ一般的な形式のtrap
コマンドを使用できます。 このスクリプトは、 SIGINT
、 SIGQUIT
、およびSIGTERM
の 3 つの異なるシグナルをトラップします。
#!/ビン/バッシュ trap "echo I was SIGINT 終了しました; exit" SIGINT trap "echo I was SIGQUIT 終了しました; exit" SIGQUIT trap "echo I was SIGTERM terminate; exit" SIGTERM エコー$$ カウンター=0 真実でありながら 行う echo "ループ番号:" $((++カウンター)) 睡眠 1 終わり
3 つのtrap
ステートメントは、スクリプトの先頭にあります。 各シグナルへの応答内にexit
コマンドが含まれていることに注意してください。 これは、スクリプトがシグナルに反応して終了することを意味します。
テキストをエディターにコピーして「simple-loop.sh」というファイルに保存し、 chmod
コマンドを使用して実行可能にします。 自分のコンピューターで作業を進めたい場合は、この記事のすべてのスクリプトに対してこれを行う必要があります。 それぞれの場合に適切なスクリプトの名前を使用してください。
chmod +x simple-loop.sh

スクリプトの残りの部分は非常に単純です。 スクリプトのプロセス ID を知る必要があるため、スクリプトにそれをエコーさせます。 $$
変数は、スクリプトのプロセス ID を保持します。
counter
という変数を作成し、ゼロに設定します。
while
ループは、強制的に停止しない限り、永遠に実行されます。 counter
変数をインクリメントし、それを画面にエコーして、1 秒間スリープします。
スクリプトを実行して、さまざまなシグナルを送信してみましょう。
./単純なループ.sh
「Ctrl+C」を押すと、メッセージがターミナル ウィンドウに出力され、スクリプトが終了します。
もう一度実行して、 kill
コマンドを使用してSIGQUIT
シグナルを送信してみましょう。 別の端末ウィンドウからそれを行う必要があります。 独自のスクリプトによって報告されたプロセス ID を使用する必要があります。
./単純なループ.sh
kill -SIGQUIT 4575
予想どおり、スクリプトは信号の到着を報告してから終了します。 最後に、要点を証明するために、 SIGTERM
を使用してもう一度実行します。
./単純なループ.sh
kill -SIGTERM 4584
スクリプトで複数のシグナルをトラップし、それぞれに個別に反応できることを確認しました。 これらすべてを興味深いものから有用なものに昇格させるステップは、シグナル ハンドラーを追加することです。
スクリプトでシグナルを処理する
応答文字列をスクリプト内の関数の名前に置き換えることができます。 シグナルが検出されると、 trap
コマンドはその関数を呼び出します。
このテキストをエディターにコピーして「grace.sh」という名前のファイルとして保存し、 chmod
で実行可能にします。
#!/ビン/バッシュ トラップ graceful_shutdown SIGINT SIGQUIT SIGTERM graceful_shutdown() { echo -e "\n一時ファイルを削除しています:" $temp_file rm -rf "$temp_file" 出口 } temp_file=$(mktemp -p /tmp tmp.XXXXXXXXXX) echo "一時ファイルを作成しました:" $temp_file カウンター=0 真実でありながら 行う echo "ループ番号:" $((++カウンター)) 睡眠 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
コマンドを使用して実行可能にします。
#!/ビン/バッシュ トラップ sigint_handler SIGINT トラップ sigusr1_handler SIGUSR1 トラップ exit_handler EXIT 関数 sigint_handler() { ((++sigint_count)) echo -e "\nSIGINT は $sigint_count 回受信しました。" if [[ "$sigint_count" -eq 3 ]]; それから echo "クローズダウンを開始します。" loop_flag=1 フィ } 関数 sigusr1_handler() { echo "SIGUSR1 は $((++sigusr1_count)) 回送受信しました。" } 関数 exit_handler() { echo "終了ハンドラー: スクリプトを終了しています..." } エコー$$ sigusr1_count=0 sigint_count=0 loop_flag=0 while [[ $loop_flag -eq 0 ]]; 行う kill -SIGUSR1 $$ 睡眠 1 終わり
スクリプトの先頭で 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
を使用して、スクリプトのプロセス ID に送信することにより、 SIGUSR1
シグナルをこのスクリプトに送信します。 スクリプトは自分自身にシグナルを送ることができます!
sigusr1_handler()
関数は、 sigusr1_count
変数をインクリメントし、端末ウィンドウにメッセージを送信します。
SIGINT
シグナルが受信されるたびに、 siguint_handler()
関数はsigint_count
変数をインクリメントし、その値を端末ウィンドウにエコーします。
loop_flag
変数が 3 に等しい場合、 sigint_count
変数は 1 に設定され、シャットダウン プロセスが開始されたことをユーザーに知らせるメッセージがターミナル ウィンドウに送信されます。
loop_flag
が 0 ではなくなるため、 while
ループが終了し、スクリプトが終了します。 しかし、そのアクションは自動的にEXIT
シグナルを発生させ、 exit_handler()
関数が呼び出されます。
./triple.sh
Ctrl+C を 3 回押すと、スクリプトが終了し、自動的にexit_handler()
関数が呼び出されます。
シグナルを読む
シグナルをトラップして単純なハンドラー関数で処理することにより、予期せず終了した場合でも、Bash スクリプトを自分自身で整理することができます。 これにより、よりクリーンなファイルシステムが得られます。 また、次にスクリプトを実行するときの不安定性を防ぎ、スクリプトの目的によっては、セキュリティ ホールを防ぐことさえできます。
関連: Lynis を使用して Linux システムのセキュリティを監査する方法