Wie kann ich mit einer Benutzeraufforderung in einem Shell-Skript mit einem SIGINT-Trap umgehen?

Wie kann ich mit einer Benutzeraufforderung in einem Shell-Skript mit einem SIGINT-Trap umgehen?

Ich versuche SIGINT( CtrlC) so zu handhaben, dass ein Benutzer, der versehentlich Strg-C drückt, die Meldung „Möchten Sie beenden? (j/n)“ erhält. Wenn er „ja“ eingibt, wird das Skript beendet. Wenn „nein“, wird dort fortgefahren, wo die Unterbrechung aufgetreten ist. Im Grunde muss ich CtrlCähnlich wie SIGTSTP( CtrlZ) vorgehen, aber auf eine etwas andere Weise. Ich habe verschiedene Möglichkeiten ausprobiert, um dies zu erreichen, aber nicht die erwarteten Ergebnisse erzielt. Im Folgenden sind einige Szenarien aufgeführt, die ich ausprobiert habe.

Fall 1

Skript: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"

Wenn ich das obige Skript ausführe, erhalte ich die erwarteten Ergebnisse.

Ausgabe:

$ 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 !!! 
$  

Fall: 2

Ich habe die forSchleife in ein neues Skript verschoben loop.sh, so dass play.shsie zum übergeordneten Prozess und loop.shzum untergeordneten Prozess wird.

Skript: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

Skript:loop.sh

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

Die Ausgabe entspricht in diesem Fall nicht den Erwartungen.

Ausgabe:

$ 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
$

Ich verstehe, dass ein Prozess, wenn er ein Signal empfängt SIGINT, dieses an alle untergeordneten Prozesse weiterleitet, sodass mein zweiter Fall fehlschlägt. Gibt es eine Möglichkeit, die SIGINTWeiterleitung an untergeordnete Prozesse zu vermeiden und es so loop.shgenauso funktionieren zu lassen wie im ersten Fall?

Notiz: Dies ist nur ein Beispiel meiner tatsächlichen Anwendung. Die Anwendung, an der ich arbeite, hat mehrere untergeordnete Skripte in play.shund loop.sh. Ich sollte sicherstellen, dass die Anwendung beim Empfang SIGINTvon nicht beendet wird, sondern dem Benutzer eine Meldung anzeigt.

Antwort1

Tolle klassische Frage zum Verwalten von Jobs und Signalen mit guten Beispielen! Ich habe ein abgespecktes Testskript entwickelt, um mich auf die Mechanik der Signalverarbeitung zu konzentrieren.

Um dies zu erreichen, rufen Sie nach dem Starten der untergeordneten Prozesse (loop.sh) im Hintergrund waitund nach Erhalt des INT-Signals killdie Prozessgruppe auf, deren PGID Ihrer PID entspricht.

  1. Für das betreffende Skript play.shkann dies wie folgt erreicht werden:

  2. In der stop()Funktion Ersetzen exit 1durch

    kill -TERM -$$  # note the dash, negative PID, kills the process group
    
  3. loop.shAls Hintergrundprozess starten (mehrere Hintergrundprozesse können hier gestartet und von verwaltet werden play.sh)

    loop.sh &
    
  4. Fügen Sie waitam Ende des Skripts hinzu, dass auf alle untergeordneten Elemente gewartet werden soll.

    wait
    

Wenn Ihr Skript einen Prozess startet, wird dieser untergeordnete Prozess Mitglied einer Prozessgruppe mit der PGID, die der PID des übergeordneten Prozesses entspricht, der sich $$in der übergeordneten Shell befindet.

Wenn das Skript beispielsweise trap.shdrei sleepProzesse im Hintergrund gestartet hat und nun waitauf sie zugreift, beachten Sie, dass die Spalte mit der Prozessgruppen-ID (PGID) mit der PID des übergeordneten Prozesses identisch ist:

  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

Unter Unix und Linux können Sie jedem Prozess in dieser Prozessgruppe ein Signal senden, indem Sie killden negativen Wert von aufrufen PGID. Wenn Sie killeine negative Zahl angeben, wird diese als -PGID verwendet. Da die PID ( $$) des Skripts mit seiner PGID identisch ist, können Sie killIhre Prozessgruppe in der Shell mit

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

Sie müssen eine Signalnummer oder einen Signalnamen angeben, andernfalls erhalten Sie bei einigen Kill-Implementierungen die Meldung „Ungültige Option“ oder „ungültige Signalspezifikation“.

Der einfache Code unten veranschaulicht dies alles. Er setzt einen trapSignalhandler, erzeugt drei untergeordnete Prozesse, geht dann in eine endlose Warteschleife und wartet darauf, sich durch den killProzessgruppenbefehl im Signalhandler selbst zu beenden.

$ 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

Hier ist ein Beispiellauf:

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

OK, es werden keine zusätzlichen Prozesse ausgeführt. Starten Sie das Testskript, unterbrechen Sie es ( ^C), ignorieren Sie die Unterbrechung und setzen Sie es dann aus ( ^Z):

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

Überprüfen Sie die laufenden Prozesse und beachten Sie die Prozessgruppennummern ( 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
$

Bringen Sie unser Testskript in den Vordergrund ( fg) und unterbrechen Sie ( ^C), diesmal wählen Sienichtignorieren:

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

Laufende Prozesse prüfen, keine Ruhevorgänge mehr:

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

Hinweis zu Ihrer Shell:

Ich musste Ihren Code ändern, damit er auf meinem System läuft. Sie haben #!/bin/shals erste Zeile in Ihren Skripten, aber die Skripte verwenden Erweiterungen (von Bash oder Zsh), die in /bin/sh nicht verfügbar sind.

Antwort2

„Ich verstehe, dass ein Prozess, wenn er ein SIGINT-Signal empfängt, das Signal an alle untergeordneten Prozesse weitergibt.“

Woher haben Sie diese falsche Vorstellung?

$ 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
$

Wenn es, wie behauptet, zu einer Ausbreitung gekommen wäre, hätte man vom Kindprozess ein „Autsch“ erwarten können.

In Wirklichkeit:

"Wenn wir die Unterbrechungstaste (häufig ENTF oder Strg-C) oder die Beendigungstaste (häufig Strg-Backslash) unseres Terminals drücken, wird entweder das Unterbrechungssignal oder das Beendigungssignal an alle Prozesse in der Vordergrundprozessgruppe gesendet" – W. Richard Stevens. "Fortgeschrittene Programmierung in der UNIX®-Umgebung". Addison-Wesley. 1993. S. 246.

Dies kann beobachtet werden über:

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

Da alle Prozesse der Vordergrundprozessgruppe ein Signal SIGINTvom verwenden Control+C, müssen Sie alle Prozesse so gestalten, dass sie dieses Signal je nach Bedarf richtig verarbeiten oder ignorieren oder dass untergeordnete Prozesse möglicherweise zur neuen Vordergrundprozessgruppe werden, sodass das übergeordnete Element (z. B. die Shell) das Signal nicht sieht, da es sich nicht mehr in der Vordergrundprozessgruppe befindet.

Antwort3

Im play.shQuelltext sieht die Loop-Datei so aus:

source ./loop.sh

Eine aufgrund einer Falle verlassene Unterschale kann nach Verlassen der Falle nicht wieder betreten werden.

Antwort4

Das Problem ist sleep.

Wenn duCTRL+Cdu tötest einen sleep- nicht deinensh (auch wenn die Syntax in Ihrem #!/bin/shetwas verdächtig ist). Die gesamte Prozessgruppe erhält jedoch das Signal, da Sie Ihren Handler nicht in loop.sheiner Subshell installieren, wird er unmittelbar danach beendet.

Sie benötigen eine Falle in loop.sh. Fallen werden bei jedem Start einer Subshell gelöscht, es sei denn, sie werden explizittrap ''SIGvom übergeordneten Element ignoriert.

Sie benötigen außerdem eine jobgesteuerte -mÜberwachung in Ihrem übergeordneten Element, damit es den Überblick über seine untergeordneten Elemente behält. waitfunktioniert beispielsweise überhaupt nur mit Jobsteuerung. -mIm Überwachungsmodus interagieren Shells mit Terminals.

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]$

verwandte Informationen