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

公開: 2022-08-09
bash プロンプトを表示している Linux ラップトップ
Fatmawati achmad zaenuri/Shutterstock.com

Linux カーネルは、プロセスが対応する必要があるイベントに関するシグナルをプロセスに送信します。 適切に動作するスクリプトは、シグナルをエレガントかつ堅牢に処理し、Ctrl+C を押しても背後でクリーンアップできます。 方法は次のとおりです。

シグナルとプロセス

シグナルは、スクリプト、プログラム、デーモンなどのプロセスに送信される短くて高速な一方向メッセージです。 それらは、起こったことをプロセスに知らせます。 ユーザーが Ctrl+C を押したか、アプリケーションがアクセス権のないメモリに書き込もうとした可能性があります。

プロセスの作成者が、特定のシグナルがプロセスに送信される可能性があることを予測している場合、そのシグナルを処理するルーチンをプログラムまたはスクリプトに書き込むことができます。 このようなルーチンは、シグナル ハンドラと呼ばれます。 シグナルをキャッチまたはトラップし、それに応じて何らかのアクションを実行します。

Linux ターミナルからプロセスを管理する方法: 知っておくべき 10 のコマンド
関連Linux ターミナルからプロセスを管理する方法: 知っておくべき 10 のコマンド

後述するように、Linux は多くのシグナルを使用しますが、スクリプトの観点からは、関心のあるシグナルの小さなサブセットしかありません。特に、重要なスクリプトでは、シャットダウンするスクリプトは (可能な場合) トラップされ、適切なシャットダウンが実行されます。

たとえば、一時ファイルを作成したり、ファイアウォール ポートを開いたりするスクリプトには、一時ファイルを削除したり、シャットダウンする前にポートを閉じたりする機会を与えることができます。 スクリプトが信号を受信した瞬間に停止した場合、コンピューターは予測不能な状態になる可能性があります。

独自のスクリプトでシグナルを処理する方法を次に示します。

信号に会う

一部の Linux コマンドにはわかりにくい名前が付いています。 シグナルをトラップするコマンドはそうではありません。 それはtrapと呼ばれます。 -l (リスト) オプションを指定してtrapを使用して、Linux が使用するシグナルの完全なリストを表示することもできます。

 トラップ -l 

trap -l を使用して Ubuntu でシグナルを一覧表示する

番号付きリストは 64 で終わりますが、実際には 62 の信号があります。 信号 32 と 33 がありません。 Linux には実装されていません。 これらは、リアルタイム スレッドを処理するためのgccコンパイラの機能に置き換えられました。 シグナル 34 のSIGRTMINからシグナル 64 のSIGRTMAXまでのすべてがリアルタイムシグナルです。

Unix ライクなオペレーティング システムごとに異なるリストが表示されます。 たとえば、OpenIndiana では、シグナル 32 と 33 が存在し、余分なシグナルが多数あるため、合計カウントは 73 になります。

trap -l を使用して OpenIndiana のシグナルを一覧表示する

信号は、名前、番号、または短縮名で参照できます。 短縮名は、先頭の「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 のトラップ

Ctrl+C の組み合わせを押すたびに、テキスト行が出力されます。

シグナルにトラップが設定されているかどうかを確認するには、 -p (トラップの出力) オプションを使用します。

 トラップ -p SIGINT 

シグナルにトラップが設定されているかどうかの確認

オプションなしでtrapを使用すると、同じことが行われます。

信号をトラップされていない通常の状態にリセットするには、ハイフン「 - 」とトラップされた信号の名前を使用します。

 トラップ - SIGINT
 トラップ -p SIGINT 

シグナルからのトラップの削除

trap -pコマンドからの出力がないということは、そのシグナルにトラップが設定されていないことを示しています。

スクリプトでシグナルをトラップする

スクリプト内で同じ一般的な形式のtrapコマンドを使用できます。 このスクリプトは、 SIGINTSIGQUIT 、および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 

chmod でスクリプトを実行可能にする

スクリプトの残りの部分は非常に単純です。 スクリプトのプロセス ID を知る必要があるため、スクリプトにそれをエコーさせます。 $$変数は、スクリプトのプロセス ID を保持します。

counterという変数を作成し、ゼロに設定します。

whileループは、強制的に停止しない限り、永遠に実行されます。 counter変数をインクリメントし、それを画面にエコーして、1 秒間スリープします。

スクリプトを実行して、さまざまなシグナルを送信してみましょう。

 ./単純なループ.sh 

それを識別するスクリプトは Ctrl+C で終了しました

「Ctrl+C」を押すと、メッセージがターミナル ウィンドウに出力され、スクリプトが終了します。

もう一度実行して、 killコマンドを使用してSIGQUITシグナルを送信してみましょう。 別の端末ウィンドウからそれを行う必要があります。 独自のスクリプトによって報告されたプロセス ID を使用する必要があります。

 ./単純なループ.sh
 kill -SIGQUIT 4575 

それを識別するスクリプトは SIGQUIT で終了しました

予想どおり、スクリプトは信号の到着を報告してから終了します。 最後に、要点を証明するために、 SIGTERMを使用してもう一度実行します。

 ./単純なループ.sh
 kill -SIGTERM 4584 

それを識別するスクリプトは SIGTERM で終了しました

スクリプトで複数のシグナルをトラップし、それぞれに個別に反応できることを確認しました。 これらすべてを興味深いものから有用なものに昇格させるステップは、シグナル ハンドラーを追加することです。

スクリプトでシグナルを処理する

応答文字列をスクリプト内の関数の名前に置き換えることができます。 シグナルが検出されると、 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 つの異なるシグナル ( SIGHUPSIGINT 、および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()と呼ばれます。

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

シグナル ハンドラーはさておき、スクリプトの本体はおなじみのはずです。 プロセス ID を端末ウィンドウにエコーし、いくつかの変数を作成します。 変数sigusr1_countSIGUSR1が処理された回数を記録し、 sigint_countSIGINTが処理された回数を記録します。 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 

SIGUSR1 を使用するスクリプト。閉じるには Ctrl+C の 3 つの組み合わせが必要で、シャットダウン時に EXIT シグナルをキャッチします。

Ctrl+C を 3 回押すと、スクリプトが終了し、自動的にexit_handler()関数が呼び出されます。

シグナルを読む

シグナルをトラップして単純なハンドラー関数で処理することにより、予期せず終了した場合でも、Bash スクリプトを自分自身で整理することができます。 これにより、よりクリーンなファイルシステムが得られます。 また、次にスクリプトを実行するときの不安定性を防ぎ、スクリプトの目的によっては、セキュリティ ホールを防ぐことさえできます。

関連: Lynis を使用して Linux システムのセキュリティを監査する方法