Comment propager un signal aux processus enfants à partir d'un script bash

Comment propager un signal aux processus enfants à partir d'un script bash

Supposons que nous écrivions un script qui engendre un ou plusieurs processus en cours d'exécution; Si ledit script reçoit un signal tel que Sigint ou Sigterm, Nous voulons probablement aussi que ses enfants soient résiliés (normalement lorsque le parent décède, les enfants survivent). Nous pouvons également vouloir effectuer des tâches de nettoyage avant que le script lui-même ne sorte. Pour pouvoir atteindre notre objectif, nous devons d'abord en savoir plus sur les groupes de processus et comment exécuter un processus en arrière-plan.

Dans ce tutoriel, vous apprendrez:

  • Qu'est-ce qu'un groupe de processus
  • La différence entre les processus de premier plan et de fond
  • Comment exécuter un programme en arrière-plan
  • Comment utiliser la coquille attendez intégré pour attendre un processus exécuté en arrière-plan
  • Comment résilier les processus enfants lorsque le parent reçoit un signal
Comment propager un signal aux processus enfants à partir d'un script bash

Exigences et conventions logicielles utilisées

Exigences logicielles et conventions de ligne de commande Linux
Catégorie Exigences, conventions ou version logicielle utilisée
Système Distribution indépendante
Logiciel Aucun logiciel spécifique nécessaire
Autre Aucun
Conventions # - Exige que les commandes Linux soient exécutées avec des privilèges racine soit directement en tant qu'utilisateur racine, soit par l'utilisation de Sudo commande
$ - Exige que les commandes Linux soient exécutées en tant qu'utilisateur non privilégié régulier

Un exemple simple

Créons un script très simple et simulons le lancement d'un processus de longue date:

#!/ Bin / Bash Trap "Signal d'écho reçu!"Sigint Echo" Le Script Pid est $ "Sleep 30 
Copie

La première chose que nous avons faite dans le script a été de créer un piège à attraper Sigint et imprimer un message lorsque le signal est reçu. Nous avons fait imprimer notre script piquer: nous pouvons obtenir en élargissant le $$ variable. Ensuite, nous avons exécuté le dormir commande pour simuler un processus de longue date (30 secondes).

Nous enregistrons le code dans un fichier (disons qu'il s'appelle test.shot), le rendre exécutable et le lancer à partir d'un émulateur de terminal. Nous obtenons les résultats suivants:

Le Script Pid est 101248 

Si nous nous concentrons sur l'émulateur de terminal et appuyez sur Ctrl + C pendant l'exécution du script, un Sigint Le signal est envoyé et manipulé par notre piège:

Le script PID est 101248 ^ csignal reçu! 

Bien que le piège ait géré le signal comme prévu, le script a été interrompu de toute façon. Pourquoi c'est arrivé? De plus, si nous envoyons un Sigint signal au script en utilisant le tuer Commande, le résultat que nous obtenons est très différent: le piège n'est pas immédiatement exécuté, et le script continue jusqu'à ce que le processus de l'enfant ne sorte pas (après 30 secondes de «dormir»). Pourquoi cette différence? Voyons…

Groupes de processus, emplois de premier plan et arrière-plan

Avant de répondre aux questions ci-dessus, nous devons mieux saisir le concept de groupe de processus.

Un groupe de processus est un groupe de processus qui partagent le même PGID (ID de groupe de processus). Lorsqu'un membre d'un groupe de processus crée un processus enfant, ce processus devient membre du même groupe de processus. Chaque groupe de processus a un leader; Nous pouvons facilement le reconnaître parce que c'est piquer et le PGID sont identiques.

Nous pouvons visualiser piquer et PGID de processus en cours d'exécution en utilisant le ps commande. La sortie de la commande peut être personnalisée afin que seuls les champs qui nous intéressent sont affichés: dans ce cas CMD, Piquer et PGID. Nous le faisons en utilisant le -o Option, fournir une liste de champs séparée par des virgules comme argument:

$ ps -a -o pid, pgid, cmd 

Si nous exécutons la commande pendant que notre script exécute la partie pertinente de la sortie que nous obtenons est la suivante:

 PID PGID CMD 298349 298349 / BIN / BASH ./test.SH 298350 298349 sommeil 30 

Nous pouvons clairement voir deux processus: le piquer du premier est 298349, Identique que c'est PGID: C'est le chef de groupe de processus. Il a été créé lorsque nous avons lancé le script comme vous pouvez le voir dans le CMD colonne.

Ce processus principal a lancé un processus d'enfant avec la commande sommeil 30: Comme prévu, les deux processus sont dans le même groupe de processus.

Lorsque nous avons appuyé sur Ctrl-C tout en nous concentrant sur le terminal à partir duquel le script a été lancé, le signal n'a pas été envoyé uniquement au processus parent, mais à l'ensemble du groupe de processus. Quel groupe de processus? Le groupe de processus de premier plan du terminal. Tous les processus membre de ce groupe sont appelés processus de premier plan, Tous les autres sont appelés Processus en arrière-plan. Voici ce que le manuel Bash a à dire sur la question:

SAVIEZ-VOUS?Pour faciliter la mise en œuvre de l'interface utilisateur au contrôle du travail, le système d'exploitation maintient la notion d'un ID de groupe de processus terminal actuel. Les membres de ce groupe de processus (processus dont l'ID de groupe de processus est égal à l'ID actuel du groupe de processus terminaux) reçoivent des signaux générés par le clavier tels que Sigint. Ces processus seraient au premier plan. Les processus de fond sont ceux dont l'ID de groupe de processus diffère des terminaux; Ces processus sont à l'abri des signaux générés par le clavier.

Quand nous avons envoyé le Sigint signal avec le tuer Commande, à la place, nous n'avons ciblé que le PID du processus parent; Bash présente un comportement spécifique lorsqu'un signal est reçu pendant qu'il attend qu'un programme se termine: le «code de piège» pour ce signal n'est pas exécuté jusqu'à ce que ce processus ait terminé. C'est pourquoi le message «signal reçu» n'a été affiché qu'après le dormir Commande sortie.

Pour reproduire ce qui se passe lorsque nous appuyons sur Ctrl-C dans le terminal en utilisant le tuer Commande pour envoyer le signal, nous devons cibler le groupe de processus. Nous pouvons envoyer un signal à un groupe de processus en utilisant La négation du PID du chef de processus, Alors, en supposant le piquer du leader du processus est 298349 (Comme dans l'exemple précédent), nous courions:

$ kill -2 -298349 

Gérer la propagation du signal à l'intérieur d'un script

Maintenant, supposons que nous lançons un long script en cours d'exécution à partir d'un shell non interactif, et nous voulons que ledit script gère automatiquement la propagation du signal, de sorte que lorsqu'il reçoit un signal tel que Sigint ou Sigterm Il met fin à son enfant potentiellement long, effectuant finalement certaines tâches de nettoyage avant de sortir. Comment nous pouvons faire ça?

Comme nous l'avons fait précédemment, nous pouvons gérer la situation dans laquelle un signal est reçu dans un piège; Cependant, comme nous l'avons vu, si un signal est reçu tandis que le shell en attente d'un programme se termine, le «code de piège» n'est exécuté qu'après que le processus de l'enfant soit sorti.

Ce n'est pas ce que nous voulons: nous voulons que le code de piège soit traité dès que le processus parent reçoit le signal. Pour atteindre notre objectif, nous devons exécuter le processus enfant dans le arrière-plan: nous pouvons le faire en plaçant le & symbole après la commande. Dans notre cas, nous écrivions:

#!/ Bin / Bash Trap 'Signal Echo reçu!'Sigint écho "Le script pid est $" sleep 30 & 
Copie

Si nous quittons le script de cette façon, le processus parent sortirait juste après l'exécution du sommeil 30 commande, nous laissant sans avoir la chance d'effectuer des tâches de nettoyage après sa fin ou est interrompue. Nous pouvons résoudre ce problème en utilisant le shell attendez intégré. La page d'aide de attendez le définit de cette façon:



Attend chaque processus identifié par un identifiant, qui peut être un identifiant de processus ou une spécification de travail, et rapporte son statut de résiliation. Si ID n'est pas donné, attend tous les processus enfants actuellement actifs, et l'état de retour est nul.

Après avoir défini un processus à exécuter en arrière-plan, nous pouvons récupérer son piquer dans le $! variable. Nous pouvons le transmettre comme argument à attendez Pour faire en sorte que le processus parent attende son enfant:

#!/ Bin / Bash Trap 'Signal Echo reçu!'SIGINT ECHO "Le Script Pid est $" Sleep 30 & Wait $! 
Copie

Allons-nous fini? Non, il y a toujours un problème: la réception d'un signal manipulé dans un piège à l'intérieur du script, provoque le attendez intégré pour revenir immédiatement, sans attendre réellement la résiliation de la commande en arrière-plan. Ce comportement est documenté dans le manuel de bash:

Lorsque Bash attend une commande asynchrone via l'attente intégrée, la réception d'un signal pour lequel un piège a été réglé fera revenir l'attente intégrée avec un statut de sortie supérieur à 128, immédiatement après quoi le piège est exécuté. C'est bien, car le signal est géré immédiatement et le piège est exécuté sans avoir à attendre que l'enfant se termine, mais soulève un problème, car dans notre piège, nous voulons exécuter nos tâches de nettoyage qu'une fois que nous sommes sûrs que l'enfant processus quitté.

Pour résoudre ce problème, nous devons utiliser attendez Encore une fois, peut-être dans le cadre du piège lui-même. Voici à quoi pourrait ressembler notre script à la fin:

#!/ bin / bash cleanup () echo "nettoyer…" # Notre code de nettoyage va ici Trap 'Signal Echo reçu!; tuer "$ child_pid"; attendez "$ child_pid"; Cleanup 'SIGINT SIGTERM Echo "Le Script Pid est $" Sleep 30 & child_pid = "$!"attendre" $ child_pid " 
Copie

Dans le script, nous avons créé un nettoyer fonction où nous pourrions insérer notre code de nettoyage et faire notre piège attraper aussi le Sigterm signal. Voici ce qui se passe lorsque nous exécutons ce script et envoyons l'un de ces deux signaux:

  1. Le script est lancé et le sommeil 30 La commande est exécutée en arrière-plan;
  2. Le piquer du processus enfant est «stocké» dans le child_pid variable;
  3. Le script attend la fin du processus de l'enfant;
  4. Le script reçoit un Sigint ou Sigterm signal
  5. Le attendez La commande revient immédiatement, sans attendre la résiliation des enfants;

À ce stade, le piège est exécuté. En elle:

  1. UN Sigterm signal (le tuer par défaut) est envoyé au child_pid;
  2. Nous attendez Pour s'assurer que l'enfant est interrompu après avoir reçu ce signal.
  3. Après attendez retourne, nous exécutons le nettoyer fonction.

Propager le signal à plusieurs enfants

Dans l'exemple ci-dessus, nous avons travaillé avec un script qui n'avait qu'un seul processus d'enfant. Et si un script a beaucoup d'enfants, et si certains d'entre eux ont leurs propres enfants?

Dans le premier cas, un moyen rapide d'obtenir le pides de tous les enfants, c'est utiliser le emplois -p Commande: Cette commande affiche les pides de tous les travaux actifs dans le shell actuel. Nous pouvons que utiliser tuer pour les terminer. Voici un exemple:

#!/ bin / bash cleanup () echo "nettoyer…" # Notre code de nettoyage va ici Trap 'Signal Echo reçu!; tuer $ (jobs -p); attendez; Cleanup 'Sigint Sigterm Echo "Le script pid est $" Sleep 30 & Sleep 40 & Wait 
Copie

Le script lance deux processus en arrière-plan: en utilisant le attendez Construit sans arguments, nous les attendons tous et maintenons le processus parent en vie. Quand le Sigint ou Sigterm Les signaux sont reçus par le script, nous envoyons un Sigterm à les deux, faisant revenir leurs pides par le emplois -p commande (emploi est lui-même une coque intégrée, donc lorsque nous l'utilisons, un nouveau processus n'est pas créé).

Si les enfants ont des enfants qui leur sont propres et que nous voulons tous les mettre fin lorsque l'ancêtre reçoit un signal, nous pouvons envoyer un signal à l'ensemble du groupe de processus, comme nous l'avons vu auparavant.

Ceci, cependant, présente un problème, car en envoyant un signal de terminaison au groupe de processus, nous entrions une boucle «Signal-Sent / Signal Tapked». Pensez-y: dans le piège pour Sigterm Nous envoyons un Sigterm signal à tous les membres du groupe de processus; Cela inclut le script parent lui-même!

Pour résoudre ce problème et être en mesure d'exécuter une fonction de nettoyage après la fin des processus d'enfants, nous devons modifier le piège pour Sigterm Juste avant d'envoyer le signal au groupe de processus, par exemple:

#!/ bin / bash Cleanup () echo "Cleaning Up…" # Notre code de nettoyage va ici trap 'trap "" Sigterm; tuer 0; attendez; Cleanup 'Sigint Sigterm Echo "Le script pid est $" Sleep 30 & Sleep 40 & Wait 
Copie

Dans le piège, avant d'envoyer Sigterm Au groupe de processus, nous avons changé le Sigterm Piège, de sorte que le processus parent ignore le signal et seuls ses descendants sont affectés par celui-ci. Notez également que dans le piège, pour signaler le groupe de processus, nous avons utilisé tuer avec 0 comme pid. C'est une sorte de raccourci: quand le piquer transmis à tuer est 0, Tous les processus dans le actuel Le groupe de processus est signalé.

Conclusions

Dans ce tutoriel, nous avons appris les groupes de processus et quelle est la différence entre les processus de premier plan et de fond. Nous avons appris que Ctrl-C envoie un Sigint signal à l'ensemble du groupe de processus de premier plan du terminal de contrôle, et nous avons appris à envoyer un signal à un groupe de processus en utilisant tuer. Nous avons également appris à exécuter un programme en arrière-plan et à utiliser le attendez Shell intégré pour attendre qu'il quitte sans perdre la coque parentale. Enfin, nous avons vu comment configurer un script afin que lorsqu'il reçoit un signal, il met fin à ses enfants avant de quitter. Ai-je manqué quelque chose? Avez-vous vos recettes personnelles pour accomplir la tâche? N'hésitez pas à me le faire savoir!

Tutoriels Linux connexes:

  • Gestion des processus de fond de bash
  • Gestion de scripts et de processus de bash multithread au…
  • Une introduction à l'automatisation Linux, des outils et des techniques
  • Comment exécuter la commande en arrière-plan sur Linux
  • Masterring Bash Script Loops
  • Comment installer le signal sur Linux
  • Comment tracer les appels du système effectués par un processus avec Strace sur…
  • Mint 20: Mieux que Ubuntu et Microsoft Windows?
  • Comparaison des MPM de Linux Apache PreFork vs Worker
  • Comment travailler avec les groupes de packages DNF