2017-10-01 3 views
1

J'ai un certain nombre de scripts bash qui exécutent de nombreuses tâches similaires et utilisent des programmes binaires externes. Le problème est que les programmes binaires ne se terminent souvent pas comme ils le devraient. Comme mes scripts les exécutent des milliers de fois, il arrive rapidement que beaucoup d'instances inactives/presque mortes de ces processus s'accumulent. Je ne peux pas réparer ces programmes, donc je dois m'assurer que mes scripts bash les terminent.Tuer tous les processus démarrés par un script Bash

Il existe déjà des sujets dans SE qui traitent de cette tâche d'arrêt des processus de scripts bash. J'ai appliqué et testé ce qui était écrit là-bas, et dans une certaine mesure cela fonctionne. Mais ça ne marche pas assez bien pour mon cas, et je ne comprends pas pourquoi, donc j'ouvre une nouvelle question.

Mes scripts ont une hiérarchie, ici représentée de manière simplifiée: Le script A appelle le script B, et le script B appelle plusieurs instances du script C en parallèle pour utiliser toutes les CPU. Par exemple. Le script B exécute 5 instances du script C en parallèle, et lorsqu'une instance du script C est terminée, elle en lance une nouvelle, au total des milliers d'exécutions du script C. Et le script C appelle plusieurs binaires/commandes externes qui ne se terminent pas bien. Ils sont en parallèle en arrière-plan et communiquent entre eux.

Cependant, mon script C est capable de détecter quand les commandes externes sont terminées avec leur travail, même si elles ne sont pas terminées, et ensuite mon script bash se ferme.

Afin de mettre fin à tous les programmes externes au cours de la fin du script bash, j'ai ajouté un piège de sortie:

# Exit cleanup 
cleanup_exit() { 
    # Running the termination in an own process group to prevent it from preliminary termination. Since it will run in the background it will not cause any delays 
    setsid nohup bash -c " 
     touch /tmp/trace_1 # To see if this code was really executed to this point 

     # Trapping signals to prevent that this function is terminated preliminary 
     trap '' SIGINT SIGQUIT SIGTERM SIGHUP ERR 
     touch /tmp/trace_2 # To see if this code was really executed to this point 

     # Terminating the main processes 
     kill ${pids[@]} 1>/dev/null 2>&1 || true 
     touch /tmp/trace_3 
     sleep 5 
     touch /tmp/trace_4 
     kill -9 ${pids[@]} 1>/dev/null 2>&1 || true 
     touch /tmp/trace_5 

     # Terminating the child processes of the main processes 
     echo "Terminating the child processes" 
     pkill -P ${pids[@]} 1>/dev/null 2>&1 || true 
     touch /tmp/trace_6 
     sleep 1 
     pkill -9 -P ${pids[@]} 1>/dev/null 2>&1 || true 
     touch /tmp/trace_7 

     # Terminating everything else which is still running and which was started by this script 
     pkill -P $$ || true 
     touch /tmp/trace_8 
     sleep 1 
     pkill -9 -P $$ || true 
     touch /tmp/trace_9 
    " 
} 
trap "cleanup_exit" SIGINT SIGQUIT SIGTERM EXIT 

Maintenant, cela semble fonctionner si je ne diffusez que très peu de cas de script C en parallèle . Si j'augmente le nombre à plus, par ex. 10 (le poste de travail est puissant et devrait être capable de gérer des douzaines d'instances parallèles du script C et des programmes externes en parallèle), alors cela ne fonctionne plus, et des centaines d'instances des programmes externes s'accumulent rapidement.

Mais je ne comprends pas pourquoi. Par exemple, le PID d'un de ces processus qui accumulaient était 32048. Et dans les journaux, je peux voir l'exécution du piège de sortie:

+ echo ' * Snapshot 190 completed after 3 seconds.' 
* Snapshot 190 completed after 3 seconds. 
+ break 
+ cleanup_exit 
+ echo 

+ echo ' * Cleaning up...' 
* Cleaning up... 
+ setsid nohup bash -c ' 
     touch /tmp/trace_1 # To see if this code was really executed to this point 

     # Trapping signals to prevent that this function is terminated preliminary 
     trap '\'''\'' SIGINT SIGQUIT SIGTERM SIGHUP ERR 
     touch /tmp/trace_2 # To see if this code was really executed to this point 

     # Terminating the main processes 
     kill 31678' '32048 1>/dev/null 2>&1 || true 
     touch /tmp/trace_3 
     sleep 5 
     touch /tmp/trace_4 
     kill -9 31678' '32048 1>/dev/null 2>&1 || true 
     touch /tmp/trace_5 

     # Terminating the child processes of the main processes 
     pkill -P 31678' '32048 1>/dev/null 2>&1 || true 
     touch /tmp/trace_6 
     sleep 1 
     pkill -9 -P 31678' '32048 1>/dev/null 2>&1 || true 
     touch /tmp/trace_7 

     # Terminating everything else which is still running and which was started by this script 
     pkill -P 31623 || true 
     touch /tmp/trace_8 
     sleep 1 
     pkill -9 -P 31623 || true 
     touch /tmp/trace_9 
    ' 

De toute évidence, le PID de ce processus a été utilisé dans le piège de sortie, mais le processus n'a pas quitté. Pour tester, j'exécute manuellement la commande kill sur ce processus, puis elle se ferme.

Et le plus intéressant, seuls les fichiers de trace jusqu'au numéro 5 apparaissent. Rien au-delà de 5, mais pourquoi?

Mise à jour: Je viens de découvrir que même si je n'exécute qu'une seule instance du script C en parallèle, c'est-à-dire séquentiellement, cela ne fonctionne que pendant un certain temps. Soudain, à un certain moment, les processus ponctuels ne se terminent plus, mais commencent à traîner pour toujours et à s'accumuler. La machine ne doit pas être surchargée avec un processus en parallèle. Et dans mes fichiers journaux le piège de sortie est encore appelé correctement comme avant, pas de différence là-bas. La mémoire est également gratuite, les processeurs sont également partiellement gratuits.

+1

S'il vous plaît lire https://stackoverflow.com/help/mcve et essayer de créer un exemple minimal, complet et vérifiable – Vinny

+0

@Vinny: C'est généralement une bonne idée, mais serait très difficile dans ce cas, car les programmes externes qui sont utilisés par mes scripts qui causent le problème sont des paquets scientifiques volumineux et complexes (non disponibles dans les repos), et je n'ai pas d'autres programmes plus simples qui reproduiraient leur comportement. – Jadzia

+0

Eh bien, pense que quelqu'un qui veut essayer et aider les besoins d'environ ~ 10min juste pour lire la question. Juste dire, votre question serait beaucoup plus sensible si elle était plus courte (ou plus précise à quel est le problème) – Vinny

Répondre

4

Un bon contrôle de bon sens pour tout script shell est d'exécuter ShellCheck dessus:

Line 9: 
     kill ${pids[@]} 1>/dev/null 2>&1 || true 
      ^-- SC2145: Argument mixes string and array. Use * or separate argument. 

Et en effet, votre xtrace fait quelque chose d'étrange sur cette ligne:

kill 31678' '32048 1>/dev/null 2>&1 || true 
      ^^^--- What is this? 

Le problème ici est que votre ${pids[@]} se développe en plusieurs mots, et bash -c n'interprète que le premier mot. Voici un exemple simplifié:

pids=(2 3 4) 
bash -c "echo killing ${pids[@]}" 

Cela finit par écrire killing 2 sans mention de 3 ou 4. Il est équivalent à l'exécution

bash -c "echo killing 2" "3" "4" 

où les autres pid deviennent simplement les paramètres de position $0 et $1 au lieu de faisant partie de la commande exécutée.

Au lieu de cela, comme ShellCheck suggested, vous vouliez * concaténer tous les pid avec des espaces et les insérer comme un seul argument:

pids=(2 3 4) 
bash -c "echo killing ${pids[*]}" 

qui imprime killing 2 3 4.

+0

Vous êtes ingénieux, merci beaucoup pour votre aide! Va le tester dès maintenant ... :-) – Jadzia

+0

Les premiers tests semblent bien fonctionner, aucun processus ne s'est accumulé jusqu'à présent. Cependant, pour une conclusion finale, j'ai besoin de faire des tests plus longs. En attendant, j'ai trouvé autre chose. J'ai ajouté des fichiers de trace dans chaque étape du piège de sortie (voir le post mis à jour ci-dessus). Avant d'ajouter la correction que vous aviez suggérée, seul le tout premier fichier de trace est apparu. Maintenant avec votre correction, les fichiers de trace jusqu'au numéro 5 apparaissent. Mais pas plus. Une idée pourquoi? Le script B appelle kill -9 sur le script C au bout de 8 secondes, mais le code de terminaison ne doit pas être affecté car il est exécuté avec setsid à ma connaissance. – Jadzia

+0

En fait, pkill -P $$ peut tuer le code de sortie, même avec setsid, puisque son ppid est le pid du script C depuis qu'il l'a démarré. Mais cela signifierait qu'au moins le fichier de trace 7 devrait être atteint. – Jadzia