Wie töte ich Hintergrundprozesse / Jobs, wenn mein Shell-Skript beendet wird?


193

Ich suche nach einer Möglichkeit, das Chaos zu beseitigen, wenn mein Skript der obersten Ebene beendet wird.

Insbesondere wenn ich verwenden möchte, möchte set -eich, dass der Hintergrundprozess beim Beenden des Skripts abbricht.

Antworten:


186

trapKann verwendet werden, um etwas Chaos zu beseitigen. Es kann eine Liste von Dingen bereitstellen, die ausgeführt werden, wenn ein bestimmtes Signal eintrifft:

trap "echo hello" SIGINT

kann aber auch verwendet werden, um etwas auszuführen, wenn die Shell beendet wird:

trap "killall background" EXIT

Es ist ein eingebautes Gerät, help trapdas Ihnen Informationen gibt (funktioniert mit Bash). Wenn Sie nur Hintergrundjobs beenden möchten, können Sie dies tun

trap 'kill $(jobs -p)' EXIT

Achten Sie darauf, Single zu verwenden ', um zu verhindern, dass die Shell die $()sofort ersetzt.


Wie tötest du dann nur alle Kinder ? (oder vermisse ich etwas Offensichtliches)
Elmarco

18
killall tötet deine Kinder, aber nicht dich
orip

3
kill $(jobs -p)funktioniert nicht in Bindestrich, da es die Befehlssubstitution in einer Subshell ausführt (siehe Befehlssubstitution im Man-Bindestrich)
user1431317

7
ist killall backgroundangeblich ein Platzhalter sein? backgroundist nicht in der Manpage ...
Evan Benn

171

Das funktioniert bei mir (verbessert dank der Kommentatoren):

trap "trap - SIGTERM && kill -- -$$" SIGINT SIGTERM EXIT
  • kill -- -$$sendet ein SIGTERM an die gesamte Prozessgruppe und tötet so auch Nachkommen.

  • Die Angabe des Signals EXITist bei der Verwendung hilfreich set -e(weitere Details hier ).


1
Sollte im Großen und Ganzen gut funktionieren, aber die untergeordneten Prozesse können Prozessgruppen ändern. Auf der anderen Seite erfordert es keine Jobkontrolle und kann auch dazu führen, dass einige Enkelkinderprozesse von anderen Lösungen übersehen werden.
Michaeljt

5
Beachten Sie, dass "kill 0" auch ein übergeordnetes Bash-Skript beendet. Möglicherweise möchten Sie "kill - - $ BASHPID" verwenden, um nur die untergeordneten Elemente des aktuellen Skripts zu töten. Wenn Sie nicht $ BASHPID in Ihrer Bash-Version haben, können Sie BASHPID = $ (sh -c 'echo $ PPID')
ACyclic

2
Vielen Dank für die schöne und klare Lösung! Leider ist Bash 4.3 fehlerhaft, was eine Trap-Rekursion ermöglicht. Ich bin 4.3.30(1)-releaseunter OSX darauf gestoßen, und es ist auch unter Ubuntu bestätigt . Es gibt jedoch einen Obvoius Wokaround :)
Skozin

4
Ich verstehe nicht ganz -$$. Es ergibt '- <PID> `zB -1234. In der eingebauten Manpage kill manpage // gibt ein führender Strich das zu sendende Signal an. Allerdings - blockiert das wahrscheinlich, aber dann ist der führende Bindestrich ansonsten nicht dokumentiert. Irgendeine Hilfe?
Evan Benn

4
@EvanBenn: Überprüfen Sie man 2 kill, ob bei einer negativen PID das Signal mit der angegebenen ID ( en.wikipedia.org/wiki/Process_group ) an alle Prozesse in der Prozessgruppe gesendet wird . Es ist verwirrend, dass dies in man 1 killoder nicht erwähnt man bashwird und als Fehler in der Dokumentation angesehen werden kann.
user001

111

Update: https://stackoverflow.com/a/53714583/302079 verbessert dies durch Hinzufügen eines Exit-Status und einer Bereinigungsfunktion.

trap "exit" INT TERM
trap "kill 0" EXIT

Warum konvertieren INTund TERMbeenden? Denn beide sollten das auslösen, kill 0ohne in eine Endlosschleife einzutreten.

Warum Trigger kill 0auf EXIT? Weil auch normale Skript-Exits ausgelöst werden sollten kill 0.

Warum kill 0? Weil verschachtelte Unterschalen ebenfalls getötet werden müssen. Dadurch wird der gesamte Prozessbaum heruntergefahren .


3
Die einzige Lösung für meinen Fall auf Debian.
MindlessRanger

3
Weder die Antwort von Johannes Schaub noch die von tokland haben es geschafft, die Hintergrundprozesse zu beenden, die mein Shell-Skript gestartet hat (auf Debian). Diese Lösung hat funktioniert. Ich weiß nicht, warum diese Antwort nicht mehr positiv bewertet wird. Könnten Sie näher erläutern, was genau kill 0bedeutet / tut?
Josch

7
Das ist großartig, tötet aber auch meine Eltern-Shell :-(
vidstige

5
Diese Lösung ist buchstäblich übertrieben. kill 0 (in meinem Skript) hat meine gesamte X-Sitzung ruiniert! Vielleicht kann in einigen Fällen kill 0 nützlich sein, aber dies ändert nichts an der Tatsache, dass es keine allgemeine Lösung ist und nach Möglichkeit vermieden werden sollte, es sei denn, es gibt einen sehr guten Grund, es zu verwenden. Es wäre schön, eine Warnung hinzuzufügen, die möglicherweise die übergeordnete Shell oder sogar die gesamte X-Sitzung beendet, nicht nur Hintergrundjobs eines Skripts!
Lissanro Rayen

3
Obwohl dies unter bestimmten Umständen eine interessante Lösung sein kann, wie von @vidstige hervorgehoben, wird dadurch die gesamte Prozessgruppe , einschließlich des Startvorgangs (in den meisten Fällen die übergeordnete Shell), beendet. Auf keinen Fall etwas, das Sie wollen, wenn Sie ein Skript über eine IDE ausführen.
Matpen

22

Trap 'kill $ (jobs -p)' EXIT

Ich würde nur geringfügige Änderungen an Johannes 'Antwort vornehmen und Jobs -pr verwenden, um den Kill auf laufende Prozesse zu beschränken und der Liste ein paar weitere Signale hinzuzufügen:

trap 'kill $(jobs -pr)' SIGINT SIGTERM EXIT

14

Die trap 'kill 0' SIGINT SIGTERM EXITin der Antwort von @ tokland beschriebene Lösung ist wirklich nett, aber der neueste Bash stürzt bei der Verwendung mit einem Segmantationsfehler ab . Das liegt daran, dass Bash ab Version 4.3 eine Trap-Rekursion zulässt, die in diesem Fall unendlich wird:

  1. Shell-Prozess erhält SIGINToder SIGTERModer EXIT;
  2. Das Signal wird beim Ausführen kill 0abgefangen und SIGTERMan alle Prozesse in der Gruppe gesendet , einschließlich der Shell selbst.
  3. gehe zu 1 :)

Dies kann umgangen werden, indem die Falle manuell abgemeldet wird:

trap 'trap - SIGTERM && kill 0' SIGINT SIGTERM EXIT

Die ausgefallenere Methode, mit der das empfangene Signal gedruckt werden kann und "Terminated:" - Nachrichten vermieden werden:

#!/usr/bin/env bash

trap_with_arg() { # from https://stackoverflow.com/a/2183063/804678
  local func="$1"; shift
  for sig in "$@"; do
    trap "$func $sig" "$sig"
  done
}

stop() {
  trap - SIGINT EXIT
  printf '\n%s\n' "recieved $1, killing children"
  kill -s SIGINT 0
}

trap_with_arg 'stop' EXIT SIGINT SIGTERM SIGHUP

{ i=0; while (( ++i )); do sleep 0.5 && echo "a: $i"; done } &
{ i=0; while (( ++i )); do sleep 0.6 && echo "b: $i"; done } &

while true; do read; done

UPD : minimales Beispiel hinzugefügt; Verbesserte stopFunktion, um unnötige Signale nicht mehr abzufangen und "Terminated:" - Meldungen vor dem Ausgang zu verbergen. Vielen Dank an Trevor Boyd Smith für die Vorschläge!


in stop()Sie bieten das erste Argument als die Signalnummer , aber dann codieren Sie , welche Signale deregistriert werden. Anstatt die abgemeldeten Signale fest zu codieren, können Sie das erste Argument verwenden, um die Registrierung in der stop()Funktion aufzuheben (dies würde möglicherweise andere rekursive Signale stoppen (außer den 3 fest codierten Signalen)).
Trevor Boyd Smith

@ TrevorBoydSmith, das würde nicht wie erwartet funktionieren, denke ich. Zum Beispiel könnte die Shell mit getötet werden SIGINT, kill 0sendet aber SIGTERM, was erneut gefangen wird. Dies führt jedoch nicht zu einer unendlichen Rekursion, da SIGTERMsie beim zweiten stopAufruf entfernt wird.
Skozin

Sollte wahrscheinlich trap - $1 && kill -s $1 0besser funktionieren. Ich werde diese Antwort testen und aktualisieren. Danke für die schöne Idee! :)
Skozin

Nein, trap - $1 && kill -s $1 0würde auch nicht funktionieren, da wir nicht mit töten können EXIT. Aber es ist wirklich ausreichend, die Falle zu lösen TERM, da killdieses Signal standardmäßig gesendet wird.
Skozin

Ich habe die Rekursion mit getestet EXIT, der trapSignal-Handler wird immer nur einmal ausgeführt.
Trevor Boyd Smith

9

Um auf der sicheren Seite zu sein, finde ich es besser, eine Bereinigungsfunktion zu definieren und sie aus der Falle aufzurufen:

cleanup() {
        local pids=$(jobs -pr)
        [ -n "$pids" ] && kill $pids
}
trap "cleanup" INT QUIT TERM EXIT [...]

oder die Funktion insgesamt vermeiden:

trap '[ -n "$(jobs -pr)" ] && kill $(jobs -pr)' INT QUIT TERM EXIT [...]

Warum? Denn durch einfache Verwendung wird trap 'kill $(jobs -pr)' [...]davon ausgegangen, dass Hintergrundjobs ausgeführt werden, wenn der Trap-Zustand signalisiert wird. Wenn keine Jobs vorhanden sind, wird die folgende (oder eine ähnliche) Meldung angezeigt:

kill: usage: kill [-s sigspec | -n signum | -sigspec] pid | jobspec ... or kill -l [sigspec]

weil jobs -pres leer ist - ich endete in dieser 'Falle' (Wortspiel beabsichtigt).


Dieser Testfall [ -n "$(jobs -pr)" ]funktioniert bei meiner Bash nicht. Ich benutze GNU Bash, Version 4.2.46 (2) -release (x86_64-redhat-linux-gnu). Die Meldung "kill: usage" wird immer wieder angezeigt.
Douwe van der Leest

Ich vermute, es hat damit zu tun, dass jobs -prdie PIDs der Kinder der Hintergrundprozesse nicht zurückgegeben werden. Es reißt nicht den gesamten Prozessbaum ab, sondern schneidet nur die Wurzeln ab.
Douwe van der Leest

2

Eine schöne Version, die unter Linux, BSD und MacOS X funktioniert. Zuerst wird versucht, SIGTERM zu senden. Wenn dies nicht erfolgreich ist, wird der Vorgang nach 10 Sekunden abgebrochen.

KillJobs() {
    for job in $(jobs -p); do
            kill -s SIGTERM $job > /dev/null 2>&1 || (sleep 10 && kill -9 $job > /dev/null 2>&1 &)

    done
}

TrapQuit() {
    # Whatever you need to clean here
    KillJobs
}

trap TrapQuit EXIT

Bitte beachten Sie, dass Jobs keine Prozesse für Enkelkinder beinhalten.



1

Eine andere Möglichkeit besteht darin, das Skript selbst als Prozessgruppenleiter festzulegen und beim Beenden ein Killpg für Ihre Prozessgruppe abzufangen.


0

Schreiben Sie also das Laden des Skripts. Führen Sie einen killallBefehl (oder was auch immer auf Ihrem Betriebssystem verfügbar ist) aus, der ausgeführt wird, sobald das Skript fertig ist.


0

jobs -p funktioniert nicht in allen Shells, wenn es in einer Sub-Shell aufgerufen wird, möglicherweise nur, wenn seine Ausgabe in eine Datei, aber nicht in eine Pipe umgeleitet wird. (Ich nehme an, es war ursprünglich nur für den interaktiven Gebrauch gedacht.)

Was ist mit Folgendem:

trap 'while kill %% 2>/dev/null; do jobs > /dev/null; done' INT TERM EXIT [...]

Der Aufruf von "jobs" wird mit der Dash-Shell von Debian benötigt, die den aktuellen Job ("%%") nicht aktualisiert, wenn er fehlt.


0

Ich habe die Antwort von @ tokland in Kombination mit dem Wissen von http://veithen.github.io/2014/11/16/sigterm-propagation.html angepasst, als ich bemerkte, dass trapdies nicht ausgelöst wird, wenn ich einen Vordergrundprozess ausführe (nicht mit Hintergrund &):

#!/bin/bash

# killable-shell.sh: Kills itself and all children (the whole process group) when killed.
# Adapted from http://stackoverflow.com/a/2173421 and http://veithen.github.io/2014/11/16/sigterm-propagation.html
# Note: Does not work (and cannot work) when the shell itself is killed with SIGKILL, for then the trap is not triggered.
trap "trap - SIGTERM && echo 'Caught SIGTERM, sending SIGTERM to process group' && kill -- -$$" SIGINT SIGTERM EXIT

echo $@
"$@" &
PID=$!
wait $PID
trap - SIGINT SIGTERM EXIT
wait $PID

Beispiel dafür:

$ bash killable-shell.sh sleep 100
sleep 100
^Z
[1]  + 31568 suspended  bash killable-shell.sh sleep 100

$ ps aux | grep "sleep"
niklas   31568  0.0  0.0  19640  1440 pts/18   T    01:30   0:00 bash killable-shell.sh sleep 100
niklas   31569  0.0  0.0  14404   616 pts/18   T    01:30   0:00 sleep 100
niklas   31605  0.0  0.0  18956   936 pts/18   S+   01:30   0:00 grep --color=auto sleep

$ bg
[1]  + 31568 continued  bash killable-shell.sh sleep 100

$ kill 31568
Caught SIGTERM, sending SIGTERM to process group
[1]  + 31568 terminated  bash killable-shell.sh sleep 100

$ ps aux | grep "sleep"
niklas   31717  0.0  0.0  18956   936 pts/18   S+   01:31   0:00 grep --color=auto sleep

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.