Comment puis-je supprimer les processus / travaux en arrière-plan à la fin de mon script shell?

193

Je cherche un moyen de nettoyer le gâchis lorsque mon script de niveau supérieur se termine.

Surtout si je veux l'utiliser set -e, je souhaite que le processus d'arrière-plan meure à la fin du script.

elmarco
la source

Réponses:

186

Pour nettoyer certains dégâts, trappeut être utilisé. Il peut fournir une liste de choses exécutées lorsqu'un signal spécifique arrive:

trap "echo hello" SIGINT

mais peut également être utilisé pour exécuter quelque chose si le shell se termine:

trap "killall background" EXIT

C'est une fonction intégrée, help trapvous donnera donc des informations (fonctionne avec bash). Si vous ne souhaitez supprimer que les jobs d'arrière-plan, vous pouvez le faire

trap 'kill $(jobs -p)' EXIT

Attention à utiliser le single ', pour éviter que la coque ne se substitue $()immédiatement.

Johannes Schaub - litb
la source
alors comment tu tues tout enfant seulement? (ou est-ce que je manque quelque chose d'évident)
elmarco
18
killall tue vos enfants, mais pas vous
orip
4
kill $(jobs -p)ne fonctionne pas dans le tiret, car il exécute la substitution de commandes dans un sous-shell (voir Substitution de commandes dans le tiret man)
user1431317
8
est killall backgroundcensé être un espace réservé? backgroundn'est pas dans la page de manuel ...
Evan Benn
170

Cela fonctionne pour moi (amélioré grâce aux commentateurs):

trap "trap - SIGTERM && kill -- -$$" SIGINT SIGTERM EXIT
  • kill -- -$$envoie un SIGTERM à l'ensemble du groupe de processus, tuant ainsi également les descendants.

  • La spécification du signal EXITest utile lors de l'utilisation set -e(plus de détails ici ).

Tokland
la source
1
Devrait bien fonctionner dans l'ensemble, mais les processus enfants peuvent changer les groupes de processus. D'un autre côté, cela ne nécessite pas de contrôle des tâches et certains processus de petits-enfants peuvent également manquer à d'autres solutions.
michaeljt
5
Remarque: "kill 0" supprimera également un script bash parent. Vous voudrez peut-être utiliser "kill - - $ BASHPID" pour tuer uniquement les enfants du script actuel. Si vous n'avez pas $ BASHPID dans votre version bash, vous pouvez exporter BASHPID = $ (sh -c 'echo $ PPID')
ACyclic
2
Merci pour la solution claire et agréable! Malheureusement, il sépare Bash 4.3, ce qui permet la récursivité des pièges. J'ai rencontré cela sur 4.3.30(1)-releaseOSX, et cela est également confirmé sur Ubuntu . Il y a un wokaround obvoius , cependant :)
skozin
4
Je ne comprends pas très bien -$$. Il évalue à '- <PID> `par exemple -1234. Dans la page de manuel kill // page de manuel intégrée, un tiret de tête spécifie le signal à envoyer. Cependant - bloque probablement cela, mais le tiret de tête n'est pas documenté autrement. De l'aide?
Evan Benn
4
@EvanBenn: Vérifiez man 2 kill, ce qui explique que lorsqu'un PID est négatif, le signal est envoyé à tous les processus du groupe de processus avec l'ID fourni ( en.wikipedia.org/wiki/Process_group ). Il est déroutant que cela ne soit pas mentionné dans man 1 killou man bash, et pourrait être considéré comme un bogue dans la documentation.
user001
111

Mise à jour: https://stackoverflow.com/a/53714583/302079 améliore cela en ajoutant l'état de sortie et une fonction de nettoyage.

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

Pourquoi convertir INTet TERMquitter? Parce que les deux devraient déclencher le kill 0sans entrer dans une boucle infinie.

Pourquoi déclenchement kill 0sur EXIT? Parce que les sorties de script normales devraient se déclencherkill 0 .

Pourquoi kill 0? Parce que les sous-coquilles imbriquées doivent également être tuées. Cela supprimera tout l'arbre de processus .

korkman
la source
3
La seule solution pour mon cas sur Debian.
MindlessRanger
3
Ni la réponse de Johannes Schaub ni celle fournie par tokland n'ont réussi à tuer les processus d'arrière-plan que mon script shell a démarrés (sur Debian). Cette solution a fonctionné. Je ne sais pas pourquoi cette réponse n'est pas plus votée. Pourriez-vous développer davantage ce que kill 0signifie / fait exactement ?
josch
7
C'est génial, mais tue également mon shell parent :-(
vidstige
5
Cette solution est littéralement exagérée. kill 0 (dans mon script) a ruiné toute ma session X! Dans certains cas, tuer 0 peut être utile, mais cela ne change pas le fait que ce n'est pas une solution générale et doit être évité si possible à moins qu'il n'y ait de très bonnes raisons de l'utiliser. Ce serait bien d'ajouter un avertissement qu'il peut tuer le shell parent ou même toute la session X, pas seulement les jobs d'arrière-plan d'un script!
Lissanro Rayen
3
Bien que cela puisse être une solution intéressante dans certaines circonstances, comme l'a souligné @vidstige, cela tuera tout le groupe de processus qui comprend le processus de lancement (c'est-à-dire le shell parent dans la plupart des cas). Certainement pas quelque chose que vous voulez lorsque vous exécutez un script via un IDE.
matpen
21

piège 'kill $ (jobs -p)' EXIT

Je n'apporterais que des modifications mineures à la réponse de Johannes et j'utiliserais jobs -pr pour limiter le kill aux processus en cours et ajouter quelques signaux supplémentaires à la liste:

trap 'kill $(jobs -pr)' SIGINT SIGTERM EXIT
raytraced
la source
Pourquoi ne pas aussi tuer les emplois arrêtés? Dans Bash EXIT, le trap sera également exécuté en cas de SIGINT et SIGTERM, de sorte que le trap sera appelé deux fois en cas d'un tel signal.
jarno
14

La trap 'kill 0' SIGINT SIGTERM EXITsolution décrite dans la réponse de @ tokland est vraiment sympa, mais le dernier Bash plante avec une faute de segmentation lors de son utilisation. C'est parce que Bash, à partir de la version 4.3, permet la récursivité des pièges, qui devient infinie dans ce cas:

  1. processus shell reçoit SIGINTou SIGTERMou EXIT;
  2. le signal est piégé, en cours d'exécution kill 0, qui est envoyé SIGTERMà tous les processus du groupe, y compris le shell lui-même;
  3. passez à 1 :)

Cela peut être contourné en désenregistrant manuellement le piège:

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

La manière la plus sophistiquée, qui permet d'imprimer le signal reçu et évite les messages "Terminé:":

#!/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 : ajout d'un exemple minimal; stopfonction améliorée pour éviter le piégeage des signaux inutiles et pour masquer les messages "Terminé:" de la sortie. Merci Trevor Boyd Smith pour les suggestions!

skozin
la source
en stop()vous fournir le premier argument que le numéro de signal , mais alors vous coder en dur les signaux qui sont désinscrits. plutôt que de coder en dur les signaux en cours de désenregistrement, vous pouvez utiliser le premier argument pour vous désenregistrer dans la stop()fonction (cela pourrait potentiellement arrêter d'autres signaux récursifs (autres que les 3 codés en dur)).
Trevor Boyd Smith,
@TrevorBoydSmith, cela ne fonctionnerait pas comme prévu, je suppose. Par exemple, le shell peut être tué avec SIGINT, mais kill 0envoie SIGTERM, qui sera à nouveau piégé. Cela ne produira cependant pas de récursion infinie, car SIGTERMil sera piégé lors du deuxième stopappel.
skozin
Cela trap - $1 && kill -s $1 0devrait probablement fonctionner mieux. Je vais tester et mettre à jour cette réponse. Merci pour la belle idée! :)
skozin
Non, trap - $1 && kill -s $1 0ça ne marchera pas aussi, car nous ne pouvons pas tuer avec EXIT. Mais il suffit vraiment de dé-piéger TERM, car killenvoie ce signal par défaut.
skozin
J'ai testé la récursivité avec EXIT, le trapgestionnaire de signaux n'est toujours exécuté qu'une seule fois.
Trevor Boyd Smith,
9

Pour être sûr, je trouve préférable de définir une fonction de nettoyage et de l'appeler depuis trap:

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

ou en évitant complètement la fonction:

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

Pourquoi? Parce qu'en utilisant simplement trap 'kill $(jobs -pr)' [...]un, on suppose qu'il y aura des travaux en arrière-plan en cours d'exécution lorsque la condition d'interruption est signalée. Lorsqu'il n'y a pas de tâches, le message suivant (ou similaire) s'affiche:

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

parce que jobs -prc'est vide - je me suis retrouvé dans ce «piège» (jeu de mots voulu).

tdaitx
la source
Ce cas de test [ -n "$(jobs -pr)" ]ne fonctionne pas sur mon bash. J'utilise GNU bash, version 4.2.46 (2) -release (x86_64-redhat-linux-gnu). Le message "kill: usage" apparaît toujours.
Douwe van der Leest
Je soupçonne que cela a à voir avec le fait que jobs -prne renvoie pas les PID des enfants des processus d'arrière-plan. Il ne détruit pas tout l'arbre du processus, il ne fait que couper les racines.
Douwe van der Leest
2

Une belle version qui fonctionne sous Linux, BSD et MacOS X. Essaie d'abord d'envoyer SIGTERM, et si cela ne réussit pas, tue le processus après 10 secondes.

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

Veuillez noter que les emplois n'incluent pas les processus des petits enfants.

Orsiris de Jong
la source
2
function cleanup_func {
    sleep 0.5
    echo cleanup
}

trap "exit \$exit_code" INT TERM
trap "exit_code=\$?; cleanup_func; kill 0" EXIT

# exit 1
# exit 0

Comme https://stackoverflow.com/a/22644006/10082476 , mais avec un code de sortie ajouté

Delaware
la source
D'où exit_codevient le INT TERMpiège?
jarno
1

Une autre option consiste à définir le script comme chef de groupe de processus et à intercepter un killpg sur votre groupe de processus à la sortie.

orip
la source
Comment définissez-vous le processus en tant que chef de groupe de processus? Qu'est-ce que "killpg"?
jarno
0

Alors scriptez le chargement du script. Exécutez une commande killall(ou tout ce qui est disponible sur votre système d'exploitation) qui s'exécute dès que le script est terminé.

Oli
la source
0

jobs -p ne fonctionne pas dans tous les shells s'il est appelé dans un sous-shell, sauf si sa sortie est redirigée dans un fichier mais pas dans un pipe. (Je suppose qu'il était initialement destiné à un usage interactif uniquement.)

Qu'en est-il des éléments suivants:

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

L'appel à "jobs" est nécessaire avec le shell de tableau de bord de Debian, qui ne met pas à jour le job actuel ("%%") s'il est manquant.

michaeljt
la source
Hmm approche intéressante, mais ça ne semble pas marcher. Envisagez scipt trap 'echo in trap; set -x; trap - TERM EXIT; while kill %% 2>/dev/null; do jobs > /dev/null; done; set +x' INT TERM EXIT; sleep 100 & while true; do printf .; sleep 1; doneSi vous l'exécutez dans Bash (5.0.3) et essayez de terminer, il semble y avoir une boucle infinie. Cependant, si vous le résiliez à nouveau, cela fonctionne. Même par Dash (0.5.10.2-6), vous devez y mettre fin deux fois.
jarno
0

J'ai fait une adaptation de la réponse de @ tokland combinée avec les connaissances de http://veithen.github.io/2014/11/16/sigterm-propagation.html quand j'ai remarqué que trapcela ne se déclenche pas si j'exécute un processus de premier plan (sans arrière-plan avec &):

#!/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

Exemple de fonctionnement:

$ 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
nh2
la source
0

Juste pour la diversité, je publierai une variation de https://stackoverflow.com/a/2173421/102484 , car cette solution conduit au message "Terminé" dans mon environnement:

trap 'test -z "$intrap" && export intrap=1 && kill -- -$$' SIGINT SIGTERM EXIT
noonex
la source