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 for
Schleife in ein neues Skript verschoben loop.sh
, so dass play.sh
sie zum übergeordneten Prozess und loop.sh
zum 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 SIGINT
Weiterleitung an untergeordnete Prozesse zu vermeiden und es so loop.sh
genauso 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.sh
und loop.sh
. Ich sollte sicherstellen, dass die Anwendung beim Empfang SIGINT
von 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 wait
und nach Erhalt des INT-Signals kill
die Prozessgruppe auf, deren PGID Ihrer PID entspricht.
Für das betreffende Skript
play.sh
kann dies wie folgt erreicht werden:In der
stop()
Funktion Ersetzenexit 1
durchkill -TERM -$$ # note the dash, negative PID, kills the process group
loop.sh
Als Hintergrundprozess starten (mehrere Hintergrundprozesse können hier gestartet und von verwaltet werdenplay.sh
)loop.sh &
Fügen Sie
wait
am 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.sh
drei sleep
Prozesse im Hintergrund gestartet hat und nun wait
auf 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 kill
den negativen Wert von aufrufen PGID
. Wenn Sie kill
eine negative Zahl angeben, wird diese als -PGID verwendet. Da die PID ( $$
) des Skripts mit seiner PGID identisch ist, können Sie kill
Ihre 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 trap
Signalhandler, erzeugt drei untergeordnete Prozesse, geht dann in eine endlose Warteschleife und wartet darauf, sich durch den kill
Prozessgruppenbefehl 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/sh
als 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 SIGINT
vom 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.sh
Quelltext 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+C
du tötest einen sleep
- nicht deinensh
(auch wenn die Syntax in Ihrem #!/bin/sh
etwas verdächtig ist). Die gesamte Prozessgruppe erhält jedoch das Signal, da Sie Ihren Handler nicht in loop.sh
einer 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 ''
SIG
vom ü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. wait
funktioniert beispielsweise überhaupt nur mit Jobsteuerung. -m
Im Ü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]$