Impossible d'arrêter un script bash avec Ctrl + C

42

J'ai écrit un script bash simple avec une boucle pour imprimer la date et un ping sur une machine distante:

#!/bin/bash
while true; do
    #     *** DATE: Thu Sep 17 10:17:50 CEST 2015  ***
    echo -e "\n*** DATE:" `date` " ***";
    echo "********************************************"
    ping -c5 $1;
done

Quand je le lance depuis un terminal, je ne peux pas l'arrêter avec Ctrl+C. Il semble que cela envoie le ^Cau terminal, mais le script ne s’arrête pas.

MacAir:~ tomas$ ping-tester.bash www.google.com

*** DATE: Thu Sep 17 23:58:42 CEST 2015  ***
********************************************
PING www.google.com (216.58.211.228): 56 data bytes
64 bytes from 216.58.211.228: icmp_seq=0 ttl=55 time=39.195 ms
64 bytes from 216.58.211.228: icmp_seq=1 ttl=55 time=37.759 ms
^C                                                          <= That is Ctrl+C press
--- www.google.com ping statistics ---
2 packets transmitted, 2 packets received, 0.0% packet loss
round-trip min/avg/max/stddev = 40.887/59.699/78.510/18.812 ms

*** DATE: Thu Sep 17 23:58:48 CEST 2015  ***
********************************************
PING www.google.com (216.58.211.196): 56 data bytes
64 bytes from 216.58.211.196: icmp_seq=0 ttl=55 time=37.460 ms
64 bytes from 216.58.211.196: icmp_seq=1 ttl=55 time=37.371 ms

Peu importe combien de fois j'appuie dessus ou à quelle vitesse je le fais. Je ne suis pas capable de l'arrêter.
Faites le test et réalisez vous-même.

En tant que solution secondaire, je l’arrête avec Ctrl+Z, ça l’arrête et puis kill %1.

Qu'est-ce qui se passe exactement ici ^C?

neveu
la source

Réponses:

26

Ce qui se passe est que les deux bashet pingreçoivent le SIGINT ( bashétant pas interactif, à la fois pinget bashexécuter dans le même groupe de processus qui a été créé et ensemble comme groupe de processus de premier plan du terminal par le shell interactif vous avez exécuté ce script à partir).

Cependant, bashgère ce SIGINT de manière asynchrone, uniquement après la fin de la commande en cours d'exécution. bashsort uniquement à la réception de ce SIGINT si la commande en cours d'exécution meurt d'un SIGINT (c'est-à-dire que son état de sortie indique qu'il a été tué par SIGINT).

$ bash -c 'sh -c "trap exit\ 0 INT; sleep 10; :"; echo here'
^Chere

Au- dessus, bash, shet sleeprecevoir SIGINT quand j'appuyez sur Ctrl-C, mais les shsorties normalement avec un code de sortie 0, donc bashne tient pas compte du SIGINT, ce qui est la raison pour laquelle nous voyons « ici ».

ping, du moins celui d’iputils, se comporte comme ça. Lorsqu'il est interrompu, il imprime des statistiques et quitte avec un état de sortie égal à 0 ou 1, selon que ses pings ont été répondus ou non. Ainsi, lorsque vous appuyez sur Ctrl-C en pingcours d’exécution, bashvous notez que vous avez appuyé Ctrl-Cdans ses gestionnaires SIGINT, mais que ping, normalement, il bashne se ferme pas.

Si vous ajoutez une sleep 1boucle dans cette boucle et appuyez sur tant Ctrl-Cque vous êtes en sleepcours d'exécution, car sleepil n'y a pas de gestionnaire spécial sur SIGINT, il mourra et signalera bashqu'il est mort d'un SIGINT et, dans ce cas, il se bashfermera (il se tuera lui-même avec SIGINT afin signaler l'interruption à son parent).

En ce qui concerne pourquoi bashse comporte comme ça, je ne suis pas sûr et je remarque que le comportement n'est pas toujours déterministe. Je viens de poser la question sur la bashliste de diffusion de développement ( Mise à jour : @Jilles a maintenant précisé la raison dans sa réponse ).

Le seul autre shell que j'ai trouvé qui se comporte de manière similaire est ksh93 (Update, comme mentionné par @Jilles, FreeBSD égalementsh ). Là, SIGINT semble être clairement ignoré. Et ksh93quitte chaque fois qu'une commande est tuée par SIGINT.

Vous obtenez le même comportement que bashci-dessus mais aussi:

ksh -c 'sh -c "kill -INT \$\$"; echo test'

Ne pas sortir "test". C'est-à-dire qu'il se ferme (en se tuant avec SIGINT là) si la commande qu'elle attendait meurt de SIGINT, même si elle-même n'a pas reçu ce SIGINT.

Une solution consiste à ajouter un:

trap 'exit 130' INT

En haut du script pour forcer la bashsortie à la réception d'un SIGINT (notez que dans tous les cas, le SIGINT ne sera pas traité de manière synchrone, uniquement après la fin de la commande en cours d'exécution).

Idéalement, nous aimerions signaler à notre parent que nous sommes morts d'un SIGINT (afin que, s'il s'agit d'un autre bashscript par exemple, ce bashscript soit également interrompu). Faire un exit 130n'est pas la même chose que mourir de SIGINT (bien que certains obus aient la $?même valeur dans les deux cas), mais il est souvent utilisé pour signaler un décès par SIGINT (sur les systèmes où SIGINT est 2, ce qui est le plus).

Toutefois , pour bash, ksh93ou FreeBSD sh, cela ne fonctionne pas. Le statut de sortie 130 n'est pas considéré comme une mort par SIGINT et un script parent ne serait pas abandonné là.

Donc, une meilleure alternative serait de se tuer avec SIGINT après avoir reçu SIGINT:

trap '
  trap - INT # restore default INT handler
  kill -s INT "$$"
' INT
Stéphane Chazelas
la source
1
La réponse de jilles explique le «pourquoi». À titre d' exemple illustratif , considérons  for f in *.txt; do vi "$f"; cp "$f" newdir; done. Si l'utilisateur tape Ctrl + C lors de la modification de l'un des fichiers, viaffiche simplement un message. Il semble raisonnable que la boucle se poursuive une fois que l'utilisateur a fini de modifier le fichier. (Et oui, je sais que vous pourriez dire vi *.txt; cp *.txt newdir; je soumets simplement la forboucle à titre d'exemple.)
Scott
@ Scott, bon point. Bien que vi( vimdu moins, au moins) désactive tty isiglors de l'édition (cela ne se fait pas de manière évidente lorsque vous exécutez :!cmd, et cela s'appliquerait beaucoup dans ce cas).
Stéphane Chazelas le
@ Tim, voir mon édition pour la correction sur votre édition.
Stéphane Chazelas
@ StéphaneChazelas Merci. C'est donc parce que pingquitte avec 0 après avoir reçu SIGINT. J'ai trouvé un comportement similaire quand un script bash contient sudoau lieu de ping, mais se sudoferme avec 1 après avoir reçu SIGINT. unix.stackexchange.com/questions/479023/…
Tim
13

L'explication est que bash implémente WCE (attente et sortie coopérative) pour SIGINT et SIGQUIT selon http://www.cons.org/cracauer/sigint.html . Cela signifie que si bash reçoit SIGINT ou SIGQUIT en attendant la fin d'un processus, il attendra que le processus se termine et se termine lui-même si le processus s'est arrêté sur ce signal. Cela garantit que les programmes qui utilisent SIGINT ou SIGQUIT dans leur interface utilisateur fonctionneront comme prévu (si le signal ne provoque pas la fin du programme, le script continue normalement).

Un inconvénient apparaît avec les programmes qui interceptent SIGINT ou SIGQUIT mais s’arrêtent à cause de cela, mais en utilisant une sortie normale () au lieu de renvoyer le signal à eux-mêmes. Il peut ne pas être possible d'interrompre les scripts qui appellent de tels programmes. Je pense que la solution réelle existe dans de tels programmes tels que ping et ping6.

Un comportement similaire est mis en œuvre par ksh93 et ​​par / bin / sh de FreeBSD, mais pas par la plupart des autres shells.

jilles
la source
Merci, cela fait beaucoup de sens. Je remarque que FreeBSD sh n'abandonne pas non plus lorsque la commande cmd se termine avec exit (130), ce qui est un moyen courant de signaler le décès de SIGINT d'un enfant (mksh effectue exit(130)par exemple une interruption si vous l'interrompez mksh -c 'sleep 10;:').
Stéphane Chazelas le
5

Comme vous le supposez, cela est dû au fait que SIGINT a été envoyé au processus subordonné et que le shell continue après la fin de ce processus.

Pour mieux gérer cela, vous pouvez vérifier le statut de sortie des commandes en cours d'exécution. Le code de retour Unix code à la fois la méthode par laquelle un processus s'est exit()terminé (appel système ou signal) et la valeur à laquelle le processus a été passé ou quel signal a mis fin au processus. Tout cela est assez compliqué, mais le moyen le plus rapide de l’utiliser est de savoir qu’un processus terminé par signal aura un code retour non nul. Ainsi, si vous vérifiez le code de retour dans votre script, vous pouvez quitter vous-même si le processus enfant a été arrêté, éliminant ainsi le besoin d'inélégations comme les sleepappels inutiles . Un moyen rapide de le faire tout au long de votre script consiste à utiliser set -e, bien que cela puisse nécessiter quelques ajustements pour les commandes dont le statut de sortie est différent de zéro.

Tom Hunt
la source
1
Set -e ne fonctionne pas correctement sous bash sauf si vous utilisez bash-4
schily
Que signifie "ne fonctionne pas correctement"? Je l'ai utilisé avec succès sur bash 3, mais il y a probablement des cas extrêmes.
Tom Hunt
Dans quelques cas simples, bash3 s'est arrêté en cas d'erreur. Cela ne s'est toutefois pas produit dans le cas général. En règle générale, make ne s’arrête pas lorsque la création d’une cible échoue et provient d’un fichier Make qui fonctionne sur une liste de cibles dans des sous-répertoires. David Korn et moi avons dû envoyer un courrier de plusieurs semaines au mainteneur de bash pour le convaincre de corriger le bogue pour bash4.
Schily
4
Notez que le problème ici est que pingrenvoie avec un état de sortie 0 à la réception de SIGINT et qu’il bashignore ensuite le SIGINT qu’il a reçu lui-même si tel est le cas. Ajouter un "set -e" ou vérifier l'état de sortie ne vous aidera pas ici. Ajouter un piège explicite sur SIGINT serait utile.
Stéphane Chazelas
4

Le terminal remarque le control-c et envoie un INTsignal au groupe de processus de premier plan, qui inclut ici le shell, car il pingn'a pas créé de nouveau groupe de processus de premier plan. Ceci est facile à vérifier en piégeant INT.

#! /bin/bash
trap 'echo oh, I am slain; exit' INT
while true; do
  ping -c5 127.0.0.1
done

Si la commande en cours d'exécution a créé un nouveau groupe de processus de premier plan, alors le control-c ira à ce groupe de processus et non au shell. Dans ce cas, le shell devra inspecter les codes de sortie, car le terminal ne le signalera pas.

( INTManutention dans des coquilles peut être compliqué fabuleusement, par ailleurs, que la coquille a parfois besoin d'ignorer le signal, et parfois pas plongée Source si curieux, ou Ponder. tail -f /etc/passwd; echo foo)

thrig
la source
Dans ce cas, le problème n'est pas la gestion du signal, mais le fait que bash effectue jobcontrol dans le script bien que ce ne soit pas le cas. Voir ma réponse pour plus d'informations
schily
Pour que SIGINT accède au nouveau groupe de processus, la commande doit également effectuer un ioctl () sur le terminal pour en faire le groupe de processus d’avant-plan du terminal. pingn'a pas de raison de démarrer un nouveau groupe de processus ici et la version de ping (iputils sur Debian) avec laquelle je peux reproduire le problème de l'OP ne crée pas de groupe de processus.
Stéphane Chazelas
1
Notez que ce n’est pas le terminal qui envoie le SIGINT, c’est la discipline de ligne du périphérique tty (le pilote (code dans le noyau) du périphérique / dev / ttysomething) à la réception d’un caractère non échappé (par lnext généralement ^ V) ^ C du terminal.
Stéphane Chazelas
2

Eh bien, j'ai essayé d'ajouter un sleep 1au script bash, et bang!
Maintenant, je suis capable de l'arrêter avec deux Ctrl+C.

En appuyant sur Ctrl+C, un SIGINTsignal est envoyé au processus en cours d'exécution, quelle commande a été exécutée à l'intérieur de la boucle. Ensuite, le processus de sous - shell continue à exécuter la commande suivante dans la boucle, ce qui démarre un autre processus. Pour pouvoir arrêter le script, il est nécessaire d’envoyer deux SIGINTsignaux, un pour interrompre la commande en cours d’exécution et un pour interrompre le processus de sous - shell .

Dans le script sans sleepappel, une pression Ctrl+Ctrès rapide et répétée ne semble pas fonctionner et il est impossible de sortir de la boucle. J'imagine qu'appuyer deux fois n'est pas assez rapide pour arriver juste au bon moment entre l'interruption du processus en cours d'exécution et le début du processus suivant. Chaque Ctrl+Cpression sur envoie un message SIGINTà un processus exécuté à l'intérieur de la boucle, mais non plus au sous - shell .

Dans le script avec sleep 1, cet appel suspend l'exécution pendant une seconde et, lorsqu'il est interrompu par le premier Ctrl+C(premier SIGINT), l' exécution de la commande suivante par le sous - shell prendra plus de temps. Alors maintenant, la seconde Ctrl+C(seconde SIGINT) ira au sous - shell et l'exécution du script se terminera.

neveu
la source
Vous vous trompez, sur un shell fonctionnant correctement, un seul ^ C suffit de voir ma réponse pour le fond.
Schily
Eh bien, étant donné que votre vote a été négatif et que votre réponse a actuellement un score de -1, je ne suis pas très convaincue de devoir prendre votre réponse au sérieux.
nevhewtom
Le fait que certaines personnes votent vers le bas n’est pas toujours lié à la qualité d’une réponse. Si vous devez taper deux fois ^ c, vous êtes certainement victime d'un bogue bash. Avez-vous essayé un shell différent? Avez-vous essayé le vrai Bourne Shell?
Schily
Si le shell fonctionne correctement, il exécute tout depuis un script du même groupe de processus, puis un seul ^ c suffit.
Schily
1
Le comportement décrit par @nephewtom dans cette réponse peut être expliqué par différentes commandes du script qui se comportent différemment lorsqu'elles reçoivent Ctrl-C. Si un sommeil est présent, il est très probable que Ctrl-C sera reçu pendant l'exécution du sommeil (en supposant que tout le reste de la boucle est rapide). Le sommeil est tué, avec la valeur de sortie 130. Le parent du sommeil, un shell, remarque que le sommeil a été tué par sigint et quitte. Mais si le script ne contient pas de veille, le Ctrl-C se met à la place de la commande ping, qui réagit en quittant avec 0, ainsi le shell parent continue à exécuter la commande suivante.
Jonathan Hartley
0

Essaye ça:

#!/bin/bash
while true; do
   echo "Ctrl-c works during sleep 5"
   sleep 5
   echo "But not during ping -c 5"
   ping -c 5 127.0.0.1
done

Maintenant changez la première ligne en:

#!/bin/sh

et essayez à nouveau - voyez si le ping est maintenant interruptible.

Moineau
la source
0
pgrep -f process_name > any_file_name
sed -i 's/^/kill /' any_file_name
chmod 777 any_file_name
./any_file_name

par exemple pgrep -f firefox, grep le PID de l'exécution firefoxet sauvegardera ce PID dans un fichier appelé any_file_name. La commande 'sed' ajoutera le killdébut du numéro PID dans le fichier 'any_file_name'. La troisième ligne any_file_nameclassera l'exécutable. La quatrième ligne supprimera le PID disponible dans le fichier any_file_name. L'écriture des quatre lignes ci-dessus dans un fichier et l'exécution de ce fichier peuvent effectuer l'opération Control- C. Travailler absolument bien pour moi.

utilisateur2176228
la source
0

Si quelqu'un cherche un correctif pour cette bashfonctionnalité et pas tant la philosophie qui la sous-tend , voici une proposition:

N'exécutez pas la commande problématique directement, mais à partir d'un wrapper qui a) attend qu'il se termine b) ne gêne pas les signaux et c) n'implémente pas le mécanisme WCE, mais meurt simplement à la réception d'un SIGINT.

Un tel emballage pourrait être fait avec awk+ sa system()fonction.

$ while true; do awk 'BEGIN{system("ping -c5 localhost")}'; done
PING localhost(localhost (::1)) 56 data bytes
64 bytes from localhost (::1): icmp_seq=1 ttl=64 time=0.082 ms
64 bytes from localhost (::1): icmp_seq=2 ttl=64 time=0.087 ms
^C
--- localhost ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1022ms
rtt min/avg/max/mdev = 0.082/0.084/0.087/0.009 ms
[3]-  Terminated              ping -c5 localhost

Mettez dans un script comme OP:

#!/bin/bash
while true; do
        echo -e "\n*** DATE:" `date` " ***";
        echo "********************************************"
        awk 'BEGIN{system(ARGV[1])}' "ping -c5 ${1-localhost}"
done
mosvy
la source
-3

Vous êtes victime d'un bogue bien connu. Bash fait jobcontrol pour les scripts, ce qui est une erreur.

En réalité, bash exécute les programmes externes dans un groupe de processus différent de celui utilisé pour le script lui-même. Comme le groupe de processus TTY est défini sur le groupe de processus du processus de premier plan actuel, seul ce processus de premier plan est supprimé et la boucle dans le script shell continue.

Pour vérifier: Extrayez et compilez un Bourne Shell récent qui implémente pgrp (1) en tant que programme intégré, puis ajoutez un / bin / sleep 100 (ou / usr / bin / sleep selon votre plate-forme) à la boucle de script, puis démarrez le processus. Bourne Shell. Après avoir utilisé ps (1) pour obtenir les ID de processus pour la commande de veille et le bash qui exécute le script, appelez pgrp <pid>et remplacez "<pid>" par l'ID de processus des veille et bash qui exécutent le script. Vous verrez différents ID de groupe de processus. Appelez maintenant quelque chose comme pgrp < /dev/pts/7(remplacez le nom de tty par le tty utilisé par le script) pour obtenir le groupe de processus tty actuel. Le groupe de processus TTY est égal au groupe de processus de la commande de veille.

Pour réparer: utilisez un shell différent.

Les sources récentes de Bourne Shell se trouvent dans mon paquet d’outils schily que vous pouvez trouver ici:

http://sourceforge.net/projects/schilytools/files/

schily
la source
De quelle version bashs'agit-il? Autant bashque je sache, cela n’est possible que si vous passez l’option -m ou -i.
Stéphane Chazelas le
Il semble que cela ne s'applique plus à bash4, mais lorsque le PO a de tels problèmes, il semble utiliser bash3
schily
Impossible de reproduire avec bash3.2.48, ni bash 3.0.16, ni bash-2.05b (essayé avec bash -c 'ps -j; ps -j; ps -j').
Stéphane Chazelas le
Cela se produit vraiment lorsque vous appelez bash en tant que /bin/sh -ce. J'ai dû ajouter une solution de contournement moche smakequi tue explicitement le groupe de processus d'une commande en cours d'exécution afin d'autoriser l' ^Cabandon d'un appel de création en couches. Avez-vous vérifié si bash a modifié le groupe de processus à partir de l'ID de groupe de processus avec lequel il a été lancé?
Schily
ARGV0=sh bash -ce 'ps -j; ps -j; ps -j'rapporte le même pgid pour ps et bash dans toutes les invocations de 3 ps. (ARGV0 = sh est le zshmoyen de passer argv [0]).
Stéphane Chazelas