Impossibile interrompere uno script bash con Ctrl + C


42

Ho scritto un semplice script bash con un ciclo per stampare la data e eseguire il ping su una macchina remota:

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

Quando lo eseguo da un terminale non riesco a fermarlo Ctrl+C. Sembra che invii il messaggio ^Cal terminale, ma lo script non si ferma.

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

Non importa quante volte lo premo o quanto velocemente lo faccio. Non riesco a fermarlo.
Fai il test e realizzalo da solo.

Come soluzione laterale, la sto fermando Ctrl+Z, la ferma e poi kill %1.

Cosa sta succedendo esattamente qui ^C?

Risposte:


26

Quello che succede è che sia bashe pingricevere il SIGINT ( bashessendo non interattivi, sia pinge bashgestiscono nello stesso gruppo processo che è stato creato e insieme come gruppo processo in primo piano del terminale dalla shell interattiva è stato eseguito lo script da).

Tuttavia, bashgestisce SIGINT in modo asincrono, solo dopo la chiusura del comando attualmente in esecuzione. bashesce solo dopo aver ricevuto quel SIGINT se il comando attualmente in esecuzione muore di un SIGINT (cioè il suo stato di uscita indica che è stato ucciso da SIGINT).

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

Sopra, bash, she sleepricevere SIGINT quando si preme Ctrl-C, ma shle uscite di solito con un codice di uscita 0, quindi bashignora il SIGINT, ed è per questo che vediamo "qui".

ping, almeno quello di iputils, si comporta così. Se interrotto, stampa le statistiche ed esce con uno stato di uscita 0 o 1 a seconda che i ping abbiano o meno risposto. Quindi, quando premi Ctrl-C mentre pingè in esecuzione, bashnota che hai premuto Ctrl-Cnei suoi gestori SIGINT, ma poiché pingesce normalmente, bashnon esce.

Se aggiungi un sleep 1in quel ciclo e premi Ctrl-Cmentre sleepè in esecuzione, perché sleepnon ha un gestore speciale su SIGINT, morirà e segnalerà bashche è morto di un SIGINT, e in quel caso bashuscirà (in realtà si ucciderà con SIGINT in modo che per segnalare l'interruzione al suo genitore).

Quanto al perché bashsi comporti così, non ne sono sicuro e noto che il comportamento non è sempre deterministico. Ho appena fatto la domanda sulla bashmailing list di sviluppo ( Aggiornamento : @Jilles ha ora individuato il motivo nella sua risposta ).

L'unica altra shell che ho trovato che si comporta in modo simile è ksh93 (Update, come menzionato da @Jilles, così fa FreeBSDsh ). Lì, SIGINT sembra essere chiaramente ignorato. Ed ksh93esce ogni volta che un comando viene ucciso da SIGINT.

Ottieni lo stesso comportamento di cui bashsopra ma anche:

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

Non genera "test". Cioè, esce (uccidendosi con SIGINT lì) se il comando che stava aspettando muore di SIGINT, anche se esso stesso non ha ricevuto quel SIGINT.

Una soluzione sarebbe aggiungere un:

trap 'exit 130' INT

Nella parte superiore dello script per forzare bashl'uscita al momento della ricezione di un SIGINT (notare che in ogni caso, SIGINT non verrà elaborato in modo sincrono, solo dopo la chiusura del comando attualmente in esecuzione).

Idealmente, vorremmo segnalare al nostro genitore che siamo morti per un SIGINT (quindi se si tratta di un altro bashscript, ad esempio, anche quello bashscript viene interrotto). Fare un exit 130non è lo stesso che morire di SIGINT (anche se alcune shell imposteranno $?lo stesso valore per entrambi i casi), tuttavia è spesso usato per segnalare un decesso di SIGINT (sui sistemi in cui SIGINT è 2 che è il più).

Tuttavia, per bash, ksh93o FreeBSD sh, che non funziona. Quel 130 stato di uscita non è considerato un decesso da SIGINT e uno script genitore non si interromperà lì.

Quindi, un'alternativa forse migliore sarebbe ucciderci con SIGINT dopo aver ricevuto SIGINT:

trap '
  trap - INT # restore default INT handler
  kill -s INT "$$"
' INT

1
La risposta di Jilles spiega il "perché". A titolo di esempio illustrativo , considerare  for f in *.txt; do vi "$f"; cp "$f" newdir; done. Se l'utente digita Ctrl + C durante la modifica di uno dei file, vivisualizza solo un messaggio. Sembra ragionevole che il ciclo continui dopo che l'utente ha finito di modificare il file. (E sì, lo so che potresti dirlo vi *.txt; cp *.txt newdir; sto solo presentando il forloop come esempio.)
Scott

@Scott, buon punto. Anche se vi( vimalmeno) disabilita tty isigdurante la modifica (non funziona inavvertitamente durante l'esecuzione :!cmd, e ciò si applicherebbe molto in quel caso).
Stéphane Chazelas,

@ Tim, vedi la mia modifica per la correzione sulla tua modifica.
Stéphane Chazelas il

@ StéphaneChazelas Grazie. Quindi è perché pingesce con 0 dopo aver ricevuto SIGINT. Ho trovato un comportamento simile quando uno script bash contiene sudoinvece di ping, ma sudoesce con 1 dopo aver ricevuto SIGINT. unix.stackexchange.com/questions/479023/…
Tim

13

La spiegazione è che bash implementa WCE (wait e cooperative exit) per SIGINT e SIGQUIT per http://www.cons.org/cracauer/sigint.html . Ciò significa che se bash riceve SIGINT o SIGQUIT in attesa dell'uscita di un processo, attenderà fino a quando il processo termina e uscirà da solo se il processo è terminato su quel segnale. Ciò garantisce che i programmi che utilizzano SIGINT o SIGQUIT nella loro interfaccia utente funzionino come previsto (se il segnale non ha causato la chiusura del programma, lo script continuerà normalmente).

Un aspetto negativo appare con i programmi che catturano SIGINT o SIGQUIT ma poi terminano a causa sua ma usando un'uscita normale () invece di rinviare il segnale a se stessi. Potrebbe non essere possibile interrompere gli script che richiamano tali programmi. Penso che la vera soluzione ci sia in tali programmi come ping e ping6.

Un comportamento simile è implementato da ksh93 e / bin / sh di FreeBSD, ma non dalla maggior parte delle altre shell.


Grazie, ha molto senso. Noto che FreeBSD sh non si interrompe nemmeno quando il cmd esce con exit (130), il che è un modo comune per segnalare la morte di SIGINT di un bambino (mksh fa un exit(130)esempio se si interrompe mksh -c 'sleep 10;:').
Stéphane Chazelas,

5

Come si suppone, ciò è dovuto al fatto che SIGINT viene inviato al processo subordinato e che la shell continua dopo la chiusura del processo.

Per gestirlo in modo migliore, è possibile verificare lo stato di uscita dei comandi in esecuzione. Il codice di ritorno Unix codifica sia il metodo con cui è uscito un processo (chiamata di sistema o segnale) sia a quale valore è stato passato exit()o quale segnale ha terminato il processo. Tutto ciò è piuttosto complicato, ma il modo più rapido di usarlo è sapere che un processo che è stato terminato dal segnale avrà un codice di ritorno diverso da zero. Pertanto, se controlli il codice di ritorno nel tuo script, puoi uscire da te stesso se il processo figlio è stato terminato, eliminando la necessità di ineleganze come sleepchiamate non necessarie . Un modo rapido per eseguire questa operazione in tutto lo script è utilizzare set -e, anche se potrebbe essere necessario apportare alcune modifiche ai comandi il cui stato di uscita è diverso da zero.


1
Set -e non funziona correttamente in bash a meno che tu non stia usando bash-4
schily,

Cosa significa "non funziona correttamente"? L'ho usato con successo su bash 3, ma probabilmente ci sono alcuni casi limite.
Tom Hunt,

In pochi semplici casi, bash3 è uscito per errore. Ciò non è avvenuto tuttavia nel caso generale. Come risultato tipico, make non si è fermato quando la creazione di un target non è riuscita e questo è stato fatto da un makefile che ha funzionato su un elenco di target nelle sottodirectory. David Korn e io abbiamo dovuto spedire molte settimane con il manutentore di bash per convincerlo a risolvere il bug per bash4.
schily,

4
Si noti che il problema qui è che pingritorna con uno stato di uscita 0 alla ricezione di SIGINT e che bashquindi ignora il SIGINT che ha ricevuto se stesso in questo caso. Aggiungere un "set -e" o controllare lo stato di uscita non aiuta qui. L'aggiunta di una trap esplicita su SIGINT sarebbe di aiuto.
Stéphane Chazelas,

4

Il terminale nota control-c e invia un INTsegnale al gruppo di processi in primo piano, che qui include la shell, poiché pingnon ha creato un nuovo gruppo di processi in primo piano. Questo è facile da verificare tramite il trapping INT.

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

Se il comando in esecuzione ha creato un nuovo gruppo di processi in primo piano, control-c passerà a quel gruppo di processi e non alla shell. In tal caso, la shell dovrà ispezionare i codici di uscita, poiché non verrà segnalata dal terminale.

( INTTrattamento in gusci può essere incredibilmente complicata, tra l'altro, come shell talvolta deve ignorare il segnale, e talvolta non Fonte immersione se curiosa, o riflettere:. tail -f /etc/passwd; echo foo)


In questo caso, il problema non è la gestione del segnale, ma il fatto che bash controlli il lavoro nello script, anche se non dovrebbe, vedere la mia risposta per ulteriori informazioni
schily,

Affinché SIGINT passi al nuovo gruppo di processi, il comando dovrebbe anche eseguire un ioctl () sul terminale per renderlo il gruppo di processi in primo piano del terminale. pingnon ha motivo di avviare un nuovo gruppo di processi qui e la versione del ping (iputils su Debian) con cui posso riprodurre il problema dell'OP non crea un gruppo di processi.
Stéphane Chazelas,

1
Nota che non è il terminale che invia SIGINT, è la disciplina di linea del dispositivo tty (il driver (codice nel kernel) del dispositivo / dev / ttysomething) quando riceve un carattere senza caratteri di escape (di solito di solito ^ V) ^ C dal terminal.
Stéphane Chazelas,

2

Bene, ho provato ad aggiungere un sleep 1alla sceneggiatura di bash e bang!
Ora sono in grado di fermarlo con due Ctrl+C.

Quando si preme Ctrl+C, SIGINTviene inviato un segnale al processo attualmente eseguito, il cui comando è stato eseguito all'interno del loop. Quindi, il processo subshell continua l'esecuzione del comando successivo nel ciclo, che avvia un altro processo. Per poter arrestare lo script è necessario inviare due SIGINTsegnali, uno per interrompere il comando corrente in esecuzione e uno per interrompere il processo di subshell .

Nello script senza la sleepchiamata, premendo Ctrl+Cmolto velocemente e molte volte non sembra funzionare, e non è possibile uscire dal ciclo. La mia ipotesi è che premere due volte non sia abbastanza veloce per farlo nel momento giusto tra l'interruzione del processo corrente eseguito e l'inizio di quello successivo. Ogni volta Ctrl+Cpremuto verrà inviato un SIGINTa un processo eseguito all'interno del ciclo, ma nessuno dei due alla subshell .

Nello script con sleep 1, questa chiamata sospenderà l'esecuzione per un secondo e, quando interrotta dal primo Ctrl+C(primo SIGINT), la subshell impiegherà più tempo per eseguire il comando successivo. Quindi ora, il secondo Ctrl+C(secondo SIGINT) andrà alla subshell e l'esecuzione dello script terminerà.


Ti sbagli, su una shell correttamente funzionante, è sufficiente una singola ^ C vedi la mia risposta per lo sfondo.
schily,

Bene, considerando che hai ricevuto il voto negativo, e attualmente la tua risposta ha un punteggio -1, non sono molto convinto di dover prendere sul serio la tua risposta.
nipote del

Il fatto che alcune persone declassino non è sempre correlato alla qualità di una risposta. Se devi digitare due volte ^ c, sei sicuramente vittima di un bug bash. Hai provato una shell diversa? Hai provato la vera Bourne Shell?
schily,

Se la shell funziona correttamente, esegue tutto da uno script nello stesso gruppo di processi e quindi è sufficiente un singolo ^ c.
schily,

1
Il comportamento descritto da @nephewtom in questa risposta può essere spiegato da diversi comandi nello script che si comportano diversamente quando ricevono Ctrl-C. Se è presente un sonno, è estremamente probabile che Ctrl-C venga ricevuto mentre il sonno è in esecuzione (supponendo che tutto il resto del ciclo sia veloce). Il sonno viene ucciso, con valore di uscita 130. Il genitore del sonno, una shell, nota che il sonno è stato ucciso da Sigint ed esce. Ma se lo script non contiene sleep, allora Ctrl-C va invece al ping, il che reagisce uscendo con 0, quindi la shell genitore continua ad eseguire il comando successivo.
Jonathan Hartley,

0

Prova questo:

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

Ora cambia la prima riga in:

#!/bin/sh

e riprova - vedi se il ping è ora interrompibile.


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

ad esempio, pgrep -f firefoxeseguirà grep il PID dell'esecuzione firefoxe salverà questo PID in un file chiamato any_file_name. Il comando 'sed' aggiungerà l' killinizio del numero PID nel file 'any_file_name'. La terza riga sarà un any_file_namefile eseguibile. La quarta riga eliminerà il PID disponibile nel file any_file_name. Scrivere le quattro righe sopra in un file ed eseguire quel file può fare il Control- C. Funzionando assolutamente bene per me.


0

Se qualcuno è interessato a una correzione per questa bashfunzionalità, e non tanto alla filosofia che sta dietro , ecco una proposta:

Non eseguire direttamente il comando problematico, ma da un wrapper che a) attende che termini b) non si scherza con i segnali ec) non implementa il meccanismo WCE stesso, ma muore semplicemente alla ricezione di a SIGINT.

Un tale wrapper potrebbe essere realizzato con awk+ la sua system()funzione.

$ 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

Inserisci uno script come OP:

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

-3

Sei vittima di un noto bug bash. Bash esegue il controllo del lavoro per gli script, il che è un errore.

Quello che succede è che bash esegue i programmi esterni in un gruppo di processi diverso da quello utilizzato per lo script stesso. Quando il processgroup TTY è impostato sul processgroup dell'attuale processo in primo piano, viene eliminato solo questo processo in primo piano e il ciclo nello script della shell continua.

Per verificare: recuperare e compilare una recente Bourne Shell che implementa pgrp (1) come programma integrato, quindi aggiungere un / bin / sleep 100 (o / usr / bin / sleep a seconda della piattaforma) al ciclo di script e quindi avviare il Bourne Shell. Dopo aver usato ps (1) per ottenere gli ID processo per il comando sleep e il bash che esegue lo script, chiama pgrp <pid>e sostituisci "<pid>" con l'ID processo del sleep e il bash che esegue lo script. Vedrai diversi ID del gruppo di processi. Adesso chiama qualcosa del generepgrp < /dev/pts/7 (sostituisci il nome tty con il tty usato dallo script) per ottenere l'attuale gruppo di processi tty. Il gruppo di processi TTY è uguale al gruppo di processi del comando sleep.

Per risolvere: usare una shell diversa.

Le recenti fonti di Bourne Shell sono nel mio pacchetto di strumenti schily che puoi trovare qui:

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


Di che versione bashè? AFAIK lo bashfa solo se si passa l'opzione -m o -i.
Stéphane Chazelas,

Sembra che questo non si applica più alla bash4 ma quando l'OP ha tali problemi, lui sembra usare bash3
Schily

Non è possibile riprodurre con bash3.2.48 né bash 3.0.16 né bash-2.05b (provato con bash -c 'ps -j; ps -j; ps -j').
Stéphane Chazelas,

Questo succede sicuramente quando chiami bash come /bin/sh -ce. Ho dovuto aggiungere una brutta soluzione a smakequella che uccide esplicitamente il gruppo di processi per un comando attualmente in esecuzione al fine di consentire ^Cdi interrompere una chiamata stratificata. Hai verificato se bash ha modificato il gruppo di processi dall'id gruppo di processi con cui è stato avviato?
schily,

ARGV0=sh bash -ce 'ps -j; ps -j; ps -j'riporta lo stesso pgid per ps e bash in tutte e 3 le invocazioni di ps. (ARGV0 = sh è il zshmodo di passare argv [0]).
Stéphane Chazelas,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.