シェルスクリプトでユーザープロンプトを使用してSIGINTトラップを処理する方法は?

シェルスクリプトでユーザープロンプトを使用してSIGINTトラップを処理する方法は?

ユーザーが誤ってctrl-cを押したときに「終了しますか?(y / n)」というメッセージが表示されるように処理しようとしていSIGINTます。CtrlCyes と入力するとスクリプトが終了します。そうでない場合は、中断が発生した時点から続行してください。基本的に()とCtrlC同じように作業する必要がありますが、少し異なる方法で作業する必要があります。これを達成するためにさまざまな方法を試しましたが、期待した結果は得られませんでした。以下は私が試したいくつかのシナリオです。SIGTSTPCtrlZ

ケース1

スクリプト:play.sh

#!/bin/sh
function stop()
{
while true; do 
    read -rep $'\nDo you wish to stop playing?(y/n)' yn
    case $yn in
        [Yy]* ) echo "Thanks for playing !!!"; exit 1;;
        [Nn]* ) break;;
        * ) echo "Please answer (y/n)";;
    esac
done
} 
trap 'stop' SIGINT 
echo "going to sleep"
for i in {1..100}
do
  echo "$i"
  sleep 3   
done
echo "end of sleep"

上記のスクリプトを実行すると、期待した結果が得られます。

出力:

$ play.sh 
going to sleep
1
^C
Do you wish to stop playing?(y/n)y
Thanks for playing !!!

$ play.sh 
going to sleep
1
2
^C
Do you wish to stop playing?(y/n)n
3
4
^C
Do you wish to stop playing?(y/n)y
Thanks for playing !!! 
$  

ケース: 2

forループを新しいスクリプトに移動して親プロセスと子プロセスloop.shにしました。play.shloop.sh

スクリプト:play.sh

#!/bin/sh
function stop()
{
while true; do 
    read -rep $'\nDo you wish to stop playing?(y/n)' yn
    case $yn in
        [Yy]* ) echo "Thanks for playing !!!"; exit 1;;
        [Nn]* ) break;;
        * ) echo "Please answer (y/n)";;
    esac
done
}
trap 'stop' SIGINT 
loop.sh

スクリプト:loop.sh

#!/bin/sh
echo "going to sleep"
for i in {1..100}
do
  echo "$i"
  sleep 3   
done
echo "end of sleep"

この場合の出力は予想とは異なります。

出力:

$ play.sh 
going to sleep
1
2
^C
Do you wish to stop playing?(y/n)y
Thanks for playing !!!

$ play.sh 
going to sleep
1
2
3
4
^C
Do you wish to stop playing?(y/n)n
$

プロセスがシグナルを受信すると、シグナルをSIGINTすべてのサブプロセスに伝播するので、2番目のケースは失敗することがわかります。最初のケースと同じように機能するようにSIGINT子プロセスに伝播するのを防ぐ方法はありますか?loop.sh

メモ: これは私の実際の適用例です。私が開発しているアプリケーションplay.shには、およびに複数の添字がありますloop.sh。アプリケーションが受信時に終了せず、SIGINTユーザーにメッセージを表示する必要があることを確認する必要があります。

答え1

素晴らしい例と一緒に仕事と信号管理に関する古典的な質問です!私は信号処理メカニズムに焦点を当てるために合理化されたテストスクリプトを開発しました。

これを行うには、バックグラウンドで子プロセス(loop.sh)を起動して呼び出しwait、INT信号を受信した後、killPIDと同じPGIDを使用してプロセスグループを呼び出します。

  1. 問題のスクリプトの場合は、次のplay.sh方法で実行できます。

  2. stop()関数でexit 1次のように置き換えます。

    kill -TERM -$$  # note the dash, negative PID, kills the process group
    
  3. バックグラウンドプロセスで開始loop.sh(ここで複数のバックグラウンドプロセスを起動および管理できますplay.sh

    loop.sh &
    
  4. waitすべてのサブアイテムを待つには、スクリプトの最後に追加してください。

    wait
    

$$スクリプトがプロセスを開始すると、子プロセスは親シェルの親プロセスのPIDと同じPGIDを持つプロセスグループのメンバーになります。

たとえば、スクリプトはバックグラウンドでtrap.sh3つのプロセスを開始し、現在のプロセスグループID列(PGID)が親プロセスのPIDと同じであることに注意してください。sleepwait

  PID  PGID STAT COMMAND
17121 17121 T    sh trap.sh
17122 17121 T    sleep 600
17123 17121 T    sleep 600
17124 17121 T    sleep 600

killUnixとLinuxでは、負の値で呼び出してプロセスグループ内の各プロセスにシグナルを送信できますPGID。負の数を指定すると、kill-PGID として使用されます。スクリプトのPID($$)はPGIDと同じであるため、killシェルでプロセスグループを使用できます。

kill -TERM -$$    # note the dash before $$

信号番号または名前を入力する必要があります。それ以外の場合、kill の一部の実装では、「無効なオプション」または「無効な信号仕様」と通知されます。

以下の簡単なコードはすべてを説明します。シグナルハンドラを設定し、3つのサブプロセスを作成し、シグナルハンドラのプロセスグループコマンドが自分で終了するのをtrap待つ無限待機ループに入ります。kill

$ cat trap.sh
#!/bin/sh

signal_handler() {
        echo
        read -p 'Interrupt: ignore? (y/n) [Y] >' answer
        case $answer in
                [nN]) 
                        kill -TERM -$$  # negative PID, kill process group
                        ;;
        esac
}

trap signal_handler INT 

for i in 1 2 3
do
    sleep 600 &
done

wait  # don't exit until process group is killed or all children die

実行例は次のとおりです。

$ ps -o pid,pgid,stat,args
  PID  PGID STAT COMMAND
 8073  8073 Ss   /bin/bash
17111 17111 R+   ps -o pid,pgid,stat,args
$ 

いいですね。追加プロセスが実行されていません。テストスクリプトを起動し、中断し(^C)、中断を無視することを選択してから一時停止します(^Z)。

$ sh trap.sh 
^C
Interrupt: ignore? (y/n) [Y] >y
^Z
[1]+  Stopped                 sh trap.sh
$

実行中のプロセスを確認し、プロセスグループ番号(PGID)を書き留めます。

$ ps -o pid,pgid,stat,args
  PID  PGID STAT COMMAND
 8073  8073 Ss   /bin/bash
17121 17121 T    sh trap.sh
17122 17121 T    sleep 600
17123 17121 T    sleep 600
17124 17121 T    sleep 600
17143 17143 R+   ps -o pid,pgid,stat,args
$

テストスクリプトを前景(fg)にインポートして再び中断(^C)します。今回はいいえ無視する:

$ fg
sh trap.sh
^C
Interrupt: ignore? (y/n) [Y] >n
Terminated
$

実行中のプロセスを確認し、スリープモードを解除します。

$ ps -o pid,pgid,stat,args
  PID  PGID STAT COMMAND
 8073  8073 Ss   /bin/bash
17159 17159 R+   ps -o pid,pgid,stat,args
$ 

シェルに注意してください。

私のシステムで動作するようにコードを変更する必要があります。この行は#!/bin/shスクリプトの最初の行にありますが、スクリプトは/ bin / shでは使用できない拡張子(bashまたはzsh)を使用します。

答え2

「プロセスがSIGINT信号を受信すると、その信号をすべての子プロセスに伝播することを知っています」

どこでこのような誤った考えを得ましたか?

$ perl -E '$SIG{INT}=sub { say "ouch $$" }; if (fork()) { say "parent $$"; sleep 3; kill 2, $$ } else { say "child $$"; sleep 99 }'
parent 25831
child 25832
ouch 25831
$

主張されているように伝播が存在する場合は、子プロセスで「申し訳ありません」を期待できます。

実際は:

「ターミナルの割り込みキー(通常はDELETEまたはControl-C)または終了キー(通常はControlバックスラッシュ)を入力するたびに、割り込み信号または終了信号がフォアグラウンドプロセスグループ内のすべてのプロセスに送信されます。リチャード・スティーブンス。 「UNIX®環境の高度なプログラミング」アディソン - ウェスリー。 1993. 246ページ。

これは次のように観察できます。

$ perl -E '$SIG{INT}=sub { say "ouch $$" }; fork(); sleep 99'
^Couch 25971
ouch 25972
$

フォアグラウンドプロセスグループ内のすべてのプロセスはSIGINTシグナルを食べるため、シグナルControl+Cを適切に処理または無視するようにすべてのプロセスを設計したり、サブプロセスを新しいフォアグラウンドプロセスグループにすることで、親プロセス(シェルなど)はもはやフォアグラウンドプロセスグループに属していないため、信号を見ることができません。

答え3

ソースループファイルでは、play.sh次のようになります。

source ./loop.sh

トラップのために一度シャットダウンされたサブシェルは、トラップを終了しても戻りません。

答え4

問題はですsleep

CTRL+Cあなたは一人を殺しましたsleep- あなたではありませんsh (構文が#!/bin/sh少し疑われても)。プロセスグループ全体はシグナルを受け取りますが、ハンドラをインストールしなかったためloop.sh(サブシェル)、すぐにシャットダウンされます。

罠が必要ですloop.sh。明示的に指定しない限り、開始されたすべてのサブシェルはトラップを消去します。trap ''SIG親は無視します。

-mまた、子項目を追跡できるように、親項目に対してジョブ制御監視を実行する必要があります。waitたとえば、ジョブ制御にのみ使用できます。-mモニタモードは、シェルが端末と対話する方法です。

sh  -cm '  . /dev/fd/3' 3<<""                     # `-m`onitor is important
     sh -c ' kill -INT "$$"'&                     # get the sig#
     wait %%;INT=$?                               # keep it in $INT
     trap  " stty $(stty -g;stty -icanon)" EXIT   # lose canonical input
     loop()( trap    exit INT                     # this is a child
             while   [ "$#" -le 100 ]             # same as yours
             do      echo "$#"                    # writing the iterator
                     set "" "$@"                  # iterating
                     sleep 3                      # sleeping
             done
     )
     int(){  case    $1      in                   # handler fn
             ($INT)  printf  "\33[2K\rDo you want to stop playing? "
                     case    $(dd bs=1 count=1; echo >&3)  in
                     ([Nn])  return
             esac;   esac;   exit "$1"
     }       3>&2    >&2     2>/dev/null
     while   loop ||
             int "$?"
     do :;   done

0
1
2
3
4
Do you want to stop playing? n
0
1
2
3
Do you want to stop playing? N
0
1
Do you want to stop playing? y
[mikeserv@desktop tmp]$

関連情報