Ein Bash-Skript kann nicht mit Strg + C gestoppt werden

42

Ich habe ein einfaches Bash-Skript mit einer Schleife zum Drucken des Datums und zum Senden eines Pings an einen Remotecomputer geschrieben:

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

Wenn ich es von einem Terminal aus starte, kann ich es nicht stoppen Ctrl+C. Es scheint, dass es das ^Can das Terminal sendet , aber das Skript stoppt nicht.

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

Egal wie oft ich drücke oder wie schnell ich es mache. Ich kann es nicht aufhalten.
Machen Sie den Test und realisieren Sie selbst.

Als Nebenlösung höre ich damit auf Ctrl+Z, dass es aufhört und dann kill %1.

Womit ist hier genau los ^C?

Neffe
quelle

Antworten:

26

Was passiert ist, dass beide bashund pingdas SIGINT empfangen ( bashda beide nicht interaktiv sind pingund bashin derselben Prozessgruppe ausgeführt werden, die von der interaktiven Shell, von der aus Sie das Skript ausgeführt haben, erstellt und als Vordergrundprozessgruppe des Terminals festgelegt wurde).

Allerdings bashGriffe dass SIGINT asynchron, erst nach dem aktuell laufenden Befehl verlassen. bashBeendet sich erst nach Erhalt dieses SIGINT, wenn der aktuell ausgeführte Befehl von einem SIGINT abbricht (dh sein Beendigungsstatus zeigt an, dass er von SIGINT getötet wurde).

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

Oben bash, shund sleeperhalten SIGINT , wenn ich Strg-C drücken, aber shAusgänge normalerweise mit einem 0 Exit - Code, so bashignoriert die SIGINT, weshalb wir „hier“ zu sehen.

pingzumindest der von iputils verhält sich so. Bei einer Unterbrechung werden Statistiken gedruckt und mit einem Ausgangsstatus von 0 oder 1 beendet, je nachdem, ob die Pings beantwortet wurden oder nicht. Wenn Sie also während der pingAusführung die Tastenkombination Strg-C bashdrücken, werden Ctrl-Cdie SIGINT-Handler zwar gedrückt , da sie jedoch pingnormal beendet werden, bashjedoch nicht beendet.

Wenn Sie ein Add sleep 1in dieser Schleife und drücken Sie Ctrl-Cwährend sleepausgeführt wird , weil sleepkeine spezielle Handler auf SIGINT hat, wird er sterben und Bericht an , bashdass es eines SIGINT gestorben, und in diesem Fall bashbeendet (es wird sich mit SIGINT tatsächlich töten , um die Unterbrechung dem Elternteil zu melden).

bashIch bin mir nicht sicher , warum sich das so verhält, und ich stelle fest, dass das Verhalten nicht immer deterministisch ist. Ich habe gerade die Frage auf der bashEntwicklungs-Mailingliste gestellt ( Update : @Jilles hat jetzt den Grund in seiner Antwort festgehalten ).

Die einzige andere Shell, die sich ähnlich verhält, ist ksh93 (Update, wie von @Jilles erwähnt, auch FreeBSDsh ). Dort scheint SIGINT einfach ignoriert zu werden. Und wird beendet, ksh93wenn ein Befehl von SIGINT beendet wird.

Sie erhalten das gleiche Verhalten wie bashoben, aber auch:

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

Gibt "test" nicht aus. Das heißt, es beendet sich (indem es sich dort mit SIGINT umbringt), wenn der Befehl, auf den es gewartet hat, von SIGINT stirbt, auch wenn es selbst diesen SIGINT nicht erhalten hat.

Ein Workaround wäre, folgendes hinzuzufügen:

trap 'exit 130' INT

Oben im Skript, um bashdas Beenden beim Empfang eines SIGINT zu erzwingen (beachten Sie, dass SIGINT in jedem Fall nicht synchron verarbeitet wird, nur nachdem der aktuell ausgeführte Befehl beendet wurde).

Idealerweise möchten wir unseren Eltern mitteilen, dass wir an einem SIGINT gestorben sind (wenn es sich also beispielsweise um ein anderes bashSkript handelt, wird dieses bashSkript ebenfalls unterbrochen). Dies exit 130ist nicht dasselbe wie das Sterben von SIGINT (obwohl einige Shells in $?beiden Fällen den gleichen Wert annehmen), es wird jedoch häufig verwendet, um einen Tod von SIGINT zu melden (auf Systemen, bei denen SIGINT 2 ist, was am meisten ist).

Doch für bash, ksh93oder FreeBSD sh, das nicht funktioniert. Dieser 130-Exit-Status wird von SIGINT nicht als Tod betrachtet, und ein übergeordnetes Skript würde dort nicht abgebrochen.

Eine möglicherweise bessere Alternative wäre es, sich mit SIGINT zu töten, wenn Sie SIGINT erhalten:

trap '
  trap - INT # restore default INT handler
  kill -s INT "$$"
' INT
Stéphane Chazelas
quelle
1
jilles antwort erklärt das "warum". Betrachten Sie als veranschaulichendes Beispiel  for f in *.txt; do vi "$f"; cp "$f" newdir; done. Wenn der Benutzer beim Bearbeiten einer der Dateien Strg + C eingibt, wird vinur eine Meldung angezeigt. Es erscheint vernünftig, dass die Schleife fortgesetzt wird, nachdem der Benutzer die Bearbeitung der Datei abgeschlossen hat. (Und ja, ich weiß, dass Sie sagen könnten vi *.txt; cp *.txt newdir; ich reiche nur die forSchleife als Beispiel ein.)
Scott
@ Scott, guter Punkt. Obwohl vi(gut vimzumindest) nicht deaktiviert tty isigbeim Bearbeiten (es nicht abviously , wenn Sie laufen :!cmdaber, und das wäre sehr viel gilt in diesem Fall).
Stéphane Chazelas
@ Tim, siehe meine Bearbeitung zur Korrektur Ihrer Bearbeitung.
Stéphane Chazelas
@ StéphaneChazelas Danke. So ist es, weil pingnach dem Empfang von SIGINT mit 0 beendet wird. Ich fand ein ähnliches Verhalten , wenn ein Bash - Skript enthält sudostatt ping, aber sudoAusgänge mit 1 nach SIGINT erhalten. unix.stackexchange.com/questions/479023/…
Tim
13

Die Erklärung ist, dass bash WCE (Wait and Cooperative Exit) für SIGINT und SIGQUIT implementiert ( http://www.cons.org/cracauer/sigint.html) . Das heißt, wenn die Bash SIGINT oder SIGQUIT empfängt, während sie auf das Beenden eines Prozesses wartet, wartet sie, bis der Prozess beendet ist, und beendet sich selbst, wenn der Prozess bei diesem Signal beendet wurde. Dies stellt sicher, dass Programme, die SIGINT oder SIGQUIT in ihrer Benutzeroberfläche verwenden, wie erwartet funktionieren (wenn das Signal nicht zum Beenden des Programms geführt hat, wird das Skript normal fortgesetzt).

Ein Nachteil tritt bei Programmen auf, die SIGINT oder SIGQUIT abfangen, dann jedoch beenden, aber einen normalen exit () verwenden, anstatt das Signal erneut an sich selbst zu senden. Skripte, die solche Programme aufrufen, können möglicherweise nicht unterbrochen werden. Ich denke, die eigentliche Lösung gibt es in solchen Programmen wie Ping und Ping6.

Ein ähnliches Verhalten wird von ksh93 und FreeBSD's / bin / sh implementiert, aber nicht von den meisten anderen Shells.

Jilles
quelle
Danke, das macht sehr viel Sinn. Ich stelle fest, dass FreeBSD sh auch nicht abbricht, wenn der Befehl mit exit (130) beendet wird. Dies ist eine übliche Methode, um den Tod eines Kindes durch SIGINT zu melden (mksh macht dies exit(130)beispielsweise, wenn Sie unterbrechen mksh -c 'sleep 10;:').
Stéphane Chazelas
5

Wie Sie vermuten, liegt dies daran, dass SIGINT an den untergeordneten Prozess gesendet wird und die Shell nach dem Beenden dieses Prozesses fortgesetzt wird.

Um dies besser zu handhaben, können Sie den Beendigungsstatus der ausgeführten Befehle überprüfen. Der Unix-Rückkehrcode codiert sowohl die Methode, mit der ein Prozess beendet wurde (Systemaufruf oder Signal), als auch den Wert, an den exit()der Prozess übergeben wurde, oder das Signal, mit dem der Prozess beendet wurde. Dies ist alles ziemlich kompliziert, aber die schnellste Möglichkeit, es zu verwenden, besteht darin, zu wissen, dass ein Prozess, der durch ein Signal beendet wurde, einen Rückkehrcode ungleich Null aufweist. Wenn Sie also den Rückkehrcode in Ihrem Skript überprüfen, können Sie sich selbst beenden, wenn der untergeordnete Prozess beendet wurde, und so die Notwendigkeit von Ineleganzen wie unnötigen sleepAufrufen beseitigen . Eine schnelle Möglichkeit, dies in Ihrem gesamten Skript zu tun, ist die Verwendung von. set -eFür Befehle, deren Beendigungsstatus voraussichtlich ungleich Null ist, sind jedoch möglicherweise einige Änderungen erforderlich.

Tom Hunt
quelle
1
Set -e funktioniert in bash nur dann richtig, wenn Sie bash-4 verwenden
schily
Was bedeutet "funktioniert nicht richtig"? Ich habe es in Bash 3 erfolgreich verwendet, aber es gibt wahrscheinlich einige Randfälle.
Tom Hunt
In einigen einfachen Fällen wurde bash3 aufgrund eines Fehlers beendet. Dies ist jedoch im allgemeinen Fall nicht geschehen. Normalerweise wurde make beim Erstellen eines fehlgeschlagenen Ziels nicht gestoppt. Dies war ein Makefile, das eine Liste von Zielen in Unterverzeichnissen bearbeitet hat. David Korn und ich mussten viele Wochen mit dem Bash-Betreuer schreiben, um ihn davon zu überzeugen, den Fehler für Bash4 zu beheben.
Schily
4
Beachten Sie, dass das Problem hier darin besteht, dass pingbeim Empfang von SIGINT ein Exit-Status von 0 zurückgegeben wird und bashder SIGINT, den er selbst erhalten hat, dann ignoriert wird, wenn dies der Fall ist. Das Hinzufügen eines "set -e" oder das Überprüfen des Exit-Status hilft hier nicht weiter. Das Hinzufügen einer expliziten Falle für SIGINT würde helfen.
Stéphane Chazelas
4

Das Terminal bemerkt die Steuerung-c und sendet ein INTSignal an die Vordergrundprozessgruppe, die hier die Shell enthält, da pingkeine neue Vordergrundprozessgruppe erstellt wurde. Dies kann leicht durch Überfüllen überprüft werden INT.

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

Wenn der ausgeführte Befehl eine neue Prozessgruppe im Vordergrund erstellt hat, wechselt das Control-C zu dieser Prozessgruppe und nicht zur Shell. In diesem Fall muss die Shell die Exit-Codes überprüfen, da diese vom Terminal nicht signalisiert werden.

( INTHandhabung in Schalen kann fabulously kompliziert sein, nebenbei bemerkt , wie die Shell manchmal das Signal ignorieren muss, und manchmal nicht Quelle taucht wenn neugierig oder ponder:. tail -f /etc/passwd; echo foo)

Thrig
quelle
In diesem Fall liegt das Problem nicht in der Signalverarbeitung, sondern in der Tatsache, dass bash die Jobsteuerung im Skript ausführt, obwohl dies nicht der Fall sein sollte. Weitere Informationen finden Sie in meiner Antwort
schily,
Damit der SIGINT zur neuen Prozessgruppe wechseln kann, muss der Befehl auch ein ioctl () für das Terminal ausführen, um es zur Vordergrundprozessgruppe des Terminals zu machen. pinghat keinen Grund, hier eine neue Prozessgruppe zu starten, und die Version von ping (iputils unter Debian), mit der ich das OP-Problem reproduzieren kann, erstellt keine Prozessgruppe.
Stéphane Chazelas
1
Beachten Sie, dass es nicht das Terminal ist, das den SIGINT sendet, sondern die Zeilendisziplin des tty-Geräts (des Treibers (Code im Kernel) des / dev / ttysomething-Geräts) beim Empfang eines nicht entkappten (durch lnext normalerweise ^ V) ^ C-Zeichens vom terminal.
Stéphane Chazelas
2

Nun, ich habe versucht, ein sleep 1zu dem Bash-Skript hinzuzufügen , und los geht's!
Jetzt kann ich mit zwei aufhören Ctrl+C.

Beim Drücken von Ctrl+Cwird ein SIGINTSignal an den aktuell ausgeführten Prozess gesendet, welcher Befehl innerhalb der Schleife ausgeführt wurde. Anschließend setzt der Subshell- Prozess die Ausführung des nächsten Befehls in der Schleife fort, der einen anderen Prozess startet. Um das Skript anhalten zu können, müssen zwei SIGINTSignale gesendet werden , eines, um den aktuellen Befehl in der Ausführung zu unterbrechen, und eines, um den Subshell- Prozess zu unterbrechen .

Im Skript ohne den sleepAufruf Ctrl+Cscheint es nicht zu funktionieren, sehr schnell und oft zu drücken , und es ist nicht möglich, die Schleife zu verlassen. Ich vermute, dass zweimaliges Drücken nicht schnell genug ist, um es genau im richtigen Moment zwischen der Unterbrechung des aktuell ausgeführten Prozesses und dem Beginn des nächsten zu machen. Bei jedem Ctrl+CTastendruck wird ein SIGINTan einen Prozess gesendet , der in der Schleife ausgeführt wird, jedoch nicht an die Subshell .

In dem Skript mit sleep 1wird durch diesen Aufruf die Ausführung für eine Sekunde unterbrochen, und wenn die Ausführung durch die erste Ctrl+C(erste SIGINT) unterbrochen wird , benötigt die Subshell mehr Zeit, um den nächsten Befehl auszuführen. Nun wird die zweite Ctrl+C(zweite SIGINT) in die Subshell verschoben , und die Skriptausführung wird beendet.

Neffe
quelle
Sie irren sich, auf einer korrekt funktionierenden Shell ist ein einzelnes ^ C ausreichend, siehe meine Antwort für den Hintergrund.
Schily
Nun, wenn man bedenkt, dass Sie abgelehnt wurden und Ihre Antwort derzeit den Wert -1 hat, bin ich nicht sehr überzeugt, dass ich Ihre Antwort ernst nehmen sollte.
Neffe
Die Tatsache, dass manche Leute abstimmen, hängt nicht immer mit der Qualität einer Antwort zusammen. Wenn Sie zweimal ^ c eingeben müssen, sind Sie definitiv ein Opfer eines Bash-Bugs. Hast du eine andere Shell ausprobiert? Hast du die echte Bourne Shell ausprobiert?
Schily
Wenn die Shell korrekt funktioniert, wird alles von einem Skript in derselben Prozessgruppe ausgeführt, und dann ist ein einzelnes ^ c ausreichend.
Schily
1
Das in dieser Antwort beschriebene Verhalten von @nephewtom kann durch unterschiedliche Befehle im Skript erklärt werden, die sich beim Empfang von Strg-C unterschiedlich verhalten. Wenn ein Ruhezustand vorliegt, ist es überwältigend wahrscheinlich, dass Strg-C empfangen wird, während der Ruhezustand ausgeführt wird (vorausgesetzt, alles andere in der Schleife ist schnell). Der Schlaf wird mit dem Ausgangswert 130 getötet. Der Elternteil des Schlafes, eine Muschel, bemerkt, dass der Schlaf durch Sigint getötet wurde, und tritt aus. Wenn das Skript jedoch keinen Ruhezustand enthält, wechselt die Tastenkombination Strg-C stattdessen zu Ping, das durch Beenden mit 0 reagiert, sodass die übergeordnete Shell den nächsten Befehl ausführt.
Jonathan Hartley
0

Versuche dies:

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

Ändern Sie nun die erste Zeile in:

#!/bin/sh

und versuchen Sie es erneut - prüfen Sie, ob der Ping jetzt unterbrochen werden kann.

Spatz
quelle
0
pgrep -f process_name > any_file_name
sed -i 's/^/kill /' any_file_name
chmod 777 any_file_name
./any_file_name

Zum Beispiel pgrep -f firefoxwird die PID der Ausführung überprüft firefoxund diese PID in einer Datei mit dem Namen gespeichert any_file_name. Der Befehl 'sed' fügt die killPID-Nummer am Anfang der Datei 'any_file_name' hinzu. Die dritte Zeile any_file_nameenthält die ausführbare Datei. Die vierte Zeile beendet die in der Datei vorhandene PID any_file_name. Das Schreiben der obigen vier Zeilen in eine Datei und das Ausführen dieser Datei kann das Control- C. Für mich absolut in Ordnung.

user2176228
quelle
0

Wenn jemand an einer Lösung für diese bashFunktion interessiert ist und nicht so sehr an der Philosophie dahinter , ist hier ein Vorschlag:

Führen Sie den problematischen Befehl nicht direkt aus, sondern führen Sie ihn von einem Wrapper aus, der a) darauf wartet, dass er beendet wird, b) keine Signale verarbeitet und c) den WCE-Mechanismus selbst nicht implementiert, sondern einfach beim Empfang von a abstirbt SIGINT.

Eine solche Hülle könnte mit awkihrer system()Funktion hergestellt werden.

$ 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

Fügen Sie ein Skript wie OPs ein:

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

Sie sind Opfer eines bekannten Bash-Bugs. Bash führt eine Jobkontrolle für Skripte durch, was ein Fehler ist.

Was passiert ist, dass Bash die externen Programme in einer anderen Prozessgruppe ausführt, als sie für das Skript selbst verwendet. Da die TTY-Prozessgruppe auf die Prozessgruppe des aktuellen Vordergrundprozesses festgelegt ist, wird nur dieser Vordergrundprozess beendet und die Schleife im Shell-Skript fortgesetzt.

So überprüfen Sie Folgendes: Rufen Sie eine aktuelle Bourne-Shell ab und kompilieren Sie sie, die pgrp (1) als integriertes Programm implementiert. Fügen Sie dann der Skriptschleife / bin / sleep 100 (oder / usr / bin / sleep, abhängig von Ihrer Plattform) hinzu, und starten Sie dann die Borowski-Shell. Nachdem Sie mit ps (1) die Prozess-IDs für den Befehl sleep und die Bash abgerufen haben, die das Skript pgrp <pid>ausführt , rufen Sie "<pid>" auf und ersetzen Sie "<pid>" durch die Prozess-ID des Sleep und der Bash, die das Skript ausführt. Sie sehen unterschiedliche Prozessgruppen-IDs. Rufen Sie nun so etwas wie auf pgrp < /dev/pts/7(ersetzen Sie den tty-Namen durch das vom Skript verwendete tty), um die aktuelle tty-Prozessgruppe zu erhalten. Die TTY-Prozessgruppe entspricht der Prozessgruppe des Sleep-Befehls.

Behebung: Verwenden Sie eine andere Shell.

Die neuesten Bourne Shell-Quellen befinden sich in meinem Schily-Tool-Paket, das Sie hier finden:

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

schily
quelle
Von welcher Version bashist das? AFAIK bashtut dies nur, wenn Sie die Option -m oder -i übergeben.
Stéphane Chazelas
Es scheint, dass dies nicht mehr auf bash4 zutrifft, aber wenn das OP solche Probleme hat, scheint er bash3 zu verwenden
schily
Kann nicht mit bash3.2.48 oder bash 3.0.16 oder bash-2.05b (ausprobiert mit bash -c 'ps -j; ps -j; ps -j') reproduzieren .
Stéphane Chazelas
Dies passiert definitiv, wenn Sie bash as aufrufen /bin/sh -ce. Ich musste eine hässliche Problemumgehung hinzufügen smake, die explizit die Prozessgruppe für einen aktuell ausgeführten Befehl beendet ^C, um einen geschichteten Make-Aufruf abbrechen zu können. Haben Sie überprüft, ob bash die Prozessgruppe von der Prozessgruppen-ID geändert hat, mit der sie initiiert wurde?
Schily
ARGV0=sh bash -ce 'ps -j; ps -j; ps -j'gibt in allen 3 ps-Aufrufen dieselbe pgid für ps und bash an. (ARGV0 = sh ist ein zshWeg, argv [0] zu übergeben).
Stéphane Chazelas