Nie można zatrzymać skryptu bash za pomocą Ctrl + C


42

Napisałem prosty skrypt bash z pętlą do drukowania daty i pingowania na zdalną maszynę:

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

Kiedy uruchamiam go z terminala, nie jestem w stanie go zatrzymać Ctrl+C. Wygląda na to, że wysyła ^Cterminal do terminala, ale skrypt się nie zatrzymuje.

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

Bez względu na to, ile razy go naciskam i jak szybko to robię. Nie jestem w stanie tego zatrzymać.
Wykonaj test i zrealizuj sam.

Jako rozwiązanie poboczne zatrzymuję to Ctrl+Z, co zatrzymuje to i wtedy kill %1.

Co się tu właściwie dzieje ^C?

Odpowiedzi:


26

Co się dzieje, że zarówno bashi pingotrzyma SIGINT ( bashistota nie interaktywne, zarówno pingi bashdziałać w tej samej grupie procesów, które zostały utworzone i ustawione jako grupy procesów planie terminala przez interaktywnej powłoki uruchomiono ten skrypt z).

Jednak bashobsługuje SIGINT asynchronicznie, dopiero po zakończeniu aktualnie uruchomionej komendy. bashkończy działanie dopiero po otrzymaniu tego SIGINT, jeśli aktualnie uruchomione polecenie umiera z SIGINT (tzn. jego status wyjścia wskazuje, że został zabity przez SIGINT).

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

Powyżej bash, shi sleepotrzymać SIGINT po naciśnięciu Ctrl-C, ale shwychodzi zwykle z kodem 0 wyjścia, więc bashpomija SIGINT, dlatego widzimy „tutaj”.

ping, przynajmniej ten z iputils, zachowuje się tak. Po przerwaniu drukuje statystyki i wychodzi ze statusem wyjścia 0 lub 1 w zależności od tego, czy odpowiedzi na ping zostały wysłane. Tak więc, gdy naciśniesz Ctrl-C podczas pingdziałania, bashnotatki, które nacisnąłeś Ctrl-Cw jego procedurach obsługi SIGINT, ale ponieważ pingwychodzą normalnie, bashnie wychodzą.

Jeśli dodasz sleep 1w tej pętli i naciśniesz Ctrl-Cpodczas sleepdziałania, ponieważ sleepnie ma specjalnego modułu obsługi w SIGINT, umrze i zgłosi bash, że zmarł w wyniku SIGINT, i w takim przypadku bashzakończy działanie (faktycznie zabije się za pomocą SIGINT, tak że zgłosić przerwanie rodzicowi).

Co do tego, dlaczego bashtak się zachowuje, nie jestem pewien i zauważam, że zachowanie nie zawsze jest deterministyczne. Właśnie zadałem pytanie na bashliście mailingowej dla programistów ( aktualizacja : @Jilles podał teraz powód swojej odpowiedzi ).

Jedyną znalezioną przeze mnie powłoką, która zachowuje się podobnie, jest ksh93 (aktualizacja, o której wspomina @Jilles, podobnie jak FreeBSDsh ). Tam SIGINT wydaje się wyraźnie ignorowany. I ksh93kończy działanie, gdy polecenie jest zabijane przez SIGINT.

Otrzymujesz takie samo zachowanie jak bashpowyżej, ale także:

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

Nie wyświetla „testu”. Oznacza to, że kończy działanie (zabijając się tam za pomocą SIGINT), jeśli polecenie, na które czekał, umiera SIGINT, nawet jeśli samo nie otrzymało tego SIGINT.

Obejściem byłoby dodanie:

trap 'exit 130' INT

W górnej części skryptu, aby wymusić bashwyjście po otrzymaniu SIGINT (pamiętaj, że w każdym razie SIGINT nie będzie przetwarzany synchronicznie, tylko po zakończeniu aktualnie uruchomionej komendy).

Najlepiej byłoby, gdybyśmy chcieli zgłosić naszemu rodzicowi, że zmarliśmy z powodu SIGINT (aby bashna przykład w przypadku innego skryptu ten bashskrypt również został przerwany). Wykonanie an exit 130to nie to samo, co umieranie SIGINT (chociaż niektóre powłoki ustawią się $?na tę samą wartość w obu przypadkach), jednak często jest używane do zgłaszania śmierci przez SIGINT (w systemach, w których SIGINT to 2, co jest najbardziej).

Jednak dla bash, ksh93lub FreeBSD sh, że nie działa. Ten status wyjścia 130 nie jest uważany przez SIGINT za śmierć, a skrypt nadrzędny nie przerywałby tam.

Zatem prawdopodobnie lepszą alternatywą byłoby zabicie się SIGINT po otrzymaniu SIGINT:

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

1
Odpowiedź Jillesa wyjaśnia „dlaczego”. Jako przykład ilustrujący , rozważ  for f in *.txt; do vi "$f"; cp "$f" newdir; done. Jeśli użytkownik wpisze Ctrl + C podczas edycji jednego z plików, vipo prostu wyświetli komunikat. Wydaje się uzasadnione, że pętla powinna być kontynuowana po zakończeniu edycji pliku przez użytkownika. (I tak, wiem, że można powiedzieć vi *.txt; cp *.txt newdir; po prostu przesyłam forpętlę jako przykład.)
Scott,

@ Scott, dobry punkt. Chociaż vi( vimprzynajmniej dobrze ) wyłącza tty isigpodczas edycji (nie jest to oczywiście przy uruchamianiu :!cmd, i to bardzo by miało zastosowanie w tym przypadku).
Stéphane Chazelas,

@ Tymczas, zobacz moją edycję w celu korekty swojej edycji.
Stéphane Chazelas,

@ StéphaneChazelas Thanks. Jest tak, ponieważ pingkończy się na 0 po otrzymaniu SIGINT. Znalazłem podobne zachowanie, gdy skrypt bash zawiera sudozamiast ping, ale sudowychodzi z 1 po otrzymaniu SIGINT. unix.stackexchange.com/questions/479023/…
Tim

13

Wyjaśnienie jest takie, że bash implementuje WCE (oczekiwanie i kooperacyjne wyjście) dla SIGINT i SIGQUIT na http://www.cons.org/cracauer/sigint.html . Oznacza to, że jeśli bash otrzyma SIGINT lub SIGQUIT podczas oczekiwania na zakończenie procesu, będzie czekał na zakończenie procesu i sam się opuści, jeśli proces zakończy się na tym sygnale. Zapewnia to, że programy używające SIGINT lub SIGQUIT w interfejsie użytkownika będą działać zgodnie z oczekiwaniami (jeśli sygnał nie spowodował zakończenia programu, skrypt będzie kontynuował normalnie).

Minusem pojawia się w przypadku programów, które wyłapują SIGINT lub SIGQUIT, ale kończą z tego powodu, ale używają normalnego wyjścia () zamiast ponownie wysyłając sygnał do siebie. Przerwanie skryptów wywołujących takie programy może być niemożliwe. Myślę, że prawdziwą poprawką jest w takich programach jak ping i ping6.

Podobne zachowanie jest implementowane przez ksh93 i FreeBSD's / bin / sh, ale nie przez większość innych powłok.


Dzięki, to ma sens. Zauważam, że FreeBSD sh również nie przerywa, gdy cmd kończy działanie z wyjściem (130), co jest częstym sposobem zgłaszania śmierci przez SIGINT dziecka (mksh robi to exit(130)na przykład, jeśli przerwiesz mksh -c 'sleep 10;:').
Stéphane Chazelas,

5

Jak można się domyślić, jest to spowodowane wysłaniem SIGINT do podrzędnego procesu i kontynuowaniem powłoki po zakończeniu tego procesu.

Aby lepiej sobie z tym poradzić, możesz sprawdzić status wyjścia z uruchomionych poleceń. Uniksowy kod powrotu koduje zarówno metodę, z której proces zakończył się (wywołanie systemowe lub sygnał), jak i jaka wartość została przekazana exit()lub jaki sygnał zakończył proces. To wszystko jest dość skomplikowane, ale najszybszym sposobem użycia jest wiedzieć, że proces zakończony sygnałem będzie miał niezerowy kod powrotu. Tak więc, jeśli sprawdzisz kod powrotu w skrypcie, możesz wyjść z siebie, jeśli proces potomny został zakończony, eliminując potrzebę nieelegancji, takich jak niepotrzebne sleepwywołania. Szybkim sposobem na zrobienie tego w całym skrypcie jest użycie set -e, choć może wymagać kilku poprawek dla poleceń, których status wyjścia jest niezerowy.


1
Zestaw -e nie działa poprawnie w bash, chyba że używasz bash-4
schily

Co oznacza „nie działa poprawnie”? Użyłem go z powodzeniem na bash 3, ale prawdopodobnie są pewne przypadki krawędzi.
Tom Hunt

W kilku prostych przypadkach bash3 zakończył działanie po błędzie. W ogólnym przypadku tak się jednak nie stało. Jako typowy wynik, make nie zatrzymał się, gdy utworzenie celu nie powiodło się, i to z makefile, który pracował na liście celów w podkatalogach. David Korn i ja musieliśmy pisać wiele tygodni z opiekunem bash, aby przekonać go do naprawienia błędu w bash4.
schily

4
Zauważ, że problem polega na tym, że pingpo otrzymaniu SIGINT wraca ze statusem wyjścia 0, a bashnastępnie ignoruje otrzymany SIGINT, jeśli tak jest. Dodanie „zestawu -e” lub sprawdzenie statusu wyjścia nie pomoże tutaj. Pomocne byłoby dodanie wyraźnej pułapki w SIGINT.
Stéphane Chazelas

4

Terminal zauważa kontrolkę-c i wysyła INTsygnał do grupy procesów pierwszego planu, która tutaj obejmuje powłokę, ponieważ pingnie utworzyła nowej grupy procesów pierwszego planu. Łatwo to zweryfikować za pomocą pułapki INT.

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

Jeśli uruchamiane polecenie utworzyło nową grupę procesów pierwszego planu, wtedy control-c przejdzie do tej grupy procesów, a nie do powłoki. W takim przypadku powłoka będzie musiała sprawdzić kody wyjścia, ponieważ nie będzie sygnalizowana przez terminal.

( INTObsługa w powłokach można fabulously skomplikowane, przez sposób, w zależności od powłoki czasami musi zignorować sygnału, a czasami nie Źródło nurkowania jeśli ciekawy lub Ponder. tail -f /etc/passwd; echo foo)


W tym przypadku problemem nie jest obsługa sygnałów, ale fakt, że bash wykonuje kontrolę zadań w skrypcie, chociaż nie powinien, zobacz moją odpowiedź, aby uzyskać więcej informacji
schily

Aby SIGINT mógł przejść do nowej grupy procesów, polecenie musiałoby również wykonać ioctl () na terminalu, aby uczynić go pierwszą grupą procesów terminala. pingnie ma powodu, aby założyć tutaj nową grupę procesów, a wersja ping (iputils na Debianie), z którą mogę odtworzyć problem PO, nie tworzy grupy procesów.
Stéphane Chazelas

1
Zauważ, że to nie terminal wysyła SIGINT, to dyscyplina liniowa urządzenia tty (sterownik (kod w jądrze) urządzenia / dev / ttysomething) po otrzymaniu nieskalowanego (przez następne zwykle ^ V) ^ C znaku z terminalu.
Stéphane Chazelas

2

Próbowałem dodać a sleep 1do skryptu bash i huk!
Teraz mogę to zatrzymać dwoma Ctrl+C.

Po naciśnięciu przycisku Ctrl+C, A SIGINTsygnał jest wysyłany do procesu, który aktualnie wykonywanego komenda została uruchomiona wewnątrz pętli. Następnie proces podpowłoki kontynuuje wykonywanie następnego polecenia w pętli, co uruchamia inny proces. Aby móc zatrzymać skrypt, należy wysłać dwa SIGINTsygnały, jeden do przerwania bieżącego polecenia w trakcie wykonywania, a drugi do przerwania procesu podpowłoki .

W skrypcie bez sleepwywołania naciskanie Ctrl+Cnaprawdę szybko i wiele razy wydaje się nie działać i nie można wyjść z pętli. Domyślam się, że dwukrotne naciśnięcie nie jest wystarczająco szybkie, aby zrobić to w odpowiednim momencie między przerwaniem aktualnie wykonywanego procesu a rozpoczęciem następnego. Każde Ctrl+Cnaciśnięcie wyśle ​​a SIGINTdo procesu wykonanego w pętli, ale nie do podpowłoki .

W skrypcie z sleep 1to wywołanie zawiesi wykonanie na jedną sekundę, a gdy zostanie przerwane przez pierwszą Ctrl+C(pierwszą SIGINT), podpowłoka zajmie więcej czasu na wykonanie następnego polecenia. Zatem teraz drugi Ctrl+C(drugi SIGINT) przejdzie do podpowłoki i zakończy się wykonywanie skryptu.


Mylisz się, na poprawnie działającej powłoce wystarczy pojedynczy ^ C, patrz moja odpowiedź na tło.
schily

Cóż, biorąc pod uwagę, że zostałeś odrzucony, a obecnie twoja odpowiedź ma wynik -1, nie jestem bardzo przekonany, że powinienem potraktować twoją odpowiedź poważnie.
nephewtom

Fakt, że niektórzy głosują negatywnie, nie zawsze jest związany z jakością odpowiedzi. Jeśli musisz wpisać dwa razy ^ c, na pewno jesteś ofiarą błędu bash. Próbowałeś innej powłoki? Próbowałeś prawdziwej powłoki Bourne'a?
schily

Jeśli powłoka działa poprawnie, uruchamia wszystko od skryptu w tej samej grupie procesów, a wtedy wystarczy pojedynczy ^ c.
schily

1
Zachowanie @nephewtom opisane w tej odpowiedzi można wyjaśnić różnymi poleceniami w skrypcie, które zachowują się inaczej, gdy otrzymują Ctrl-C. Jeśli sen jest obecny, jest bardzo prawdopodobne, że Ctrl-C zostanie odebrany podczas wykonywania snu (zakładając, że wszystko inne w pętli jest szybkie). Sen zostaje zabity, z wartością wyjściową 130. Rodzic snu, skorupa, zauważa, że ​​sen został zabity przez siginta i wychodzi. Ale jeśli skrypt nie zawiera trybu uśpienia, wówczas Ctrl-C przechodzi do pingowania, co reaguje wyjściem z 0, więc powłoka nadrzędna kontynuuje wykonywanie następnego polecenia.
Jonathan Hartley,

0

Spróbuj tego:

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

Teraz zmień pierwszy wiersz na:

#!/bin/sh

i spróbuj ponownie - sprawdź, czy ping jest teraz przerywany.


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

na przykład pgrep -f firefoxzamieni PID uruchomionego programu firefoxi zapisze ten PID w pliku o nazwie any_file_name. Polecenie „sed” doda killpoczątek numeru PID do pliku „nazwa_pliku_ dowolna”. Trzeci wiersz będzie any_file_namezawierał plik wykonywalny. Teraz czwarta linia zabije PID dostępny w pliku any_file_name. Zapisanie powyższych czterech wierszy w pliku i wykonanie tego pliku może wykonać Control- C. Działa dla mnie absolutnie dobrze.


0

Jeśli ktoś jest zainteresowany naprawą tej bashfunkcji, a nie tyle filozofią, która się za nią kryje , oto propozycja:

Nie uruchamiaj problematycznego polecenia bezpośrednio, ale z opakowania, które a) czeka na zakończenie, b) nie zadziera z sygnałami ic) nie implementuje samego mechanizmu WCE, ale po prostu umiera po otrzymaniu SIGINT.

Takie opakowanie można wykonać za pomocą awk+ jego system()funkcji.

$ 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

Umieść skrypt jak OP:

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

-3

Jesteś ofiarą znanego błędu bash. Bash wykonuje kontrolę zadań dla skryptów, co jest błędem.

To, co się dzieje, polega na tym, że bash uruchamia programy zewnętrzne w innej grupie procesów niż używa dla samego skryptu. Ponieważ grupa procesów TTY jest ustawiona na grupę procesów bieżącego procesu pierwszego planu, tylko ten proces pierwszego planu zostaje zabity, a pętla w skrypcie powłoki jest kontynuowana.

Aby zweryfikować: pobierz i skompiluj najnowszą powłokę Bourne'a, która implementuje pgrp (1) jako wbudowany program, a następnie dodaj / bin / sleep 100 (lub / usr / bin / sleep w zależności od platformy) do pętli skryptów, a następnie uruchom Bourne Shell. Po użyciu ps (1) w celu uzyskania identyfikatorów procesu dla polecenia sleep i bash, który uruchamia skrypt, wywołaj pgrp <pid>i zamień „<pid>” na identyfikator procesu sleep i bash, który uruchamia skrypt. Zobaczysz różne identyfikatory grup procesów. Teraz wywołaj coś w stylu pgrp < /dev/pts/7(zastąp nazwę tty nazwą tty używaną przez skrypt), aby uzyskać bieżącą grupę procesów tty. Grupa procesów TTY jest równa grupie procesów polecenia uśpienia.

Aby naprawić: użyj innej powłoki.

Najnowsze źródła Bourne Shell są w moim pakiecie narzędzi schily, które można znaleźć tutaj:

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


Co to za wersja bash? AFAIK bashrobi to tylko wtedy, gdy przekażesz opcję -m lub -i.
Stéphane Chazelas

Wydaje się, że to nie ma zastosowania do bash4 ale gdy PO ma takich problemów, wydaje się używać bash3
Schily

Nie można powielać z bash3.2.48, ani bash 3.0.16, ani bash-2.05b (wypróbowany z bash -c 'ps -j; ps -j; ps -j').
Stéphane Chazelas

Z pewnością dzieje się tak, gdy nazywasz bash as /bin/sh -ce. Musiałem dodać brzydkie obejście, smakektóre wyraźnie zabija grupę procesów dla aktualnie uruchomionego polecenia, aby umożliwić ^Cprzerwanie warstwowego wywołania wykonywania. Czy sprawdziłeś, czy bash zmienił grupę procesów z identyfikatora grupy procesów, którym została zainicjowana?
schily

ARGV0=sh bash -ce 'ps -j; ps -j; ps -j'zgłasza ten sam pgid dla ps i bash we wszystkich 3 wywołaniach ps. (ARGV0 = sh jest zshsposobem na przekazanie argv [0]).
Stéphane Chazelas
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.