Невозможно остановить bash-скрипт с помощью Ctrl + C

42

Я написал простой скрипт bash с циклом для печати даты и проверки связи с удаленным компьютером:

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

Когда я запускаю его из терминала, я не могу его остановить Ctrl+C. Кажется, он отправляет в ^Cтерминал, но сценарий не останавливается.

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

Независимо от того, сколько раз я нажимаю это или как быстро я делаю это. Я не могу это остановить.
Сделай тест и осознай сам.

Как побочное решение, я прекращаю это Ctrl+Z, это останавливает это и затем kill %1.

Что именно здесь происходит ^C?

nephewtom
источник

Ответы:

26

То , что происходит в том , что как bashи pingполучить SIGINT ( bashбудучи не интерактивными, как pingи bashработать в одной и той же группы процессов , которая была создана и установленной в группе процессов переднего плана терминала в интерактивной оболочке запускался скрипт с).

Однако bashобрабатывает этот SIGINT асинхронно, только после завершения работающей в данный момент команды. bashвыход происходит только после получения этого SIGINT, если текущая выполняемая команда умирает от SIGINT (т. е. ее состояние выхода указывает, что она была уничтожена SIGINT).

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

Выше bash, shи sleepполучить SIGINT , когда я нажимаю Ctrl-C, но shвыходит обычно с кодом 0 на выходе, поэтому bashигнорирует SIGINT, поэтому мы видим , «здесь».

pingПо крайней мере, один из iputils, ведет себя так. При прерывании он печатает статистику и завершает работу со статусом выхода 0 или 1 в зависимости от того, были ли получены ответы на его запросы. Таким образом, когда вы нажимаете Ctrl-C во время pingработы, bashзаметки, которые вы нажимали Ctrl-Cв его обработчиках SIGINT, но, поскольку он pingзавершается нормально, bashне завершаются.

Если вы добавите sleep 1в этот цикл и нажмете Ctrl-Cпока sleepвыполняется, потому что sleepне имеет специального обработчика на SIGINT, он умрет и сообщит, bashчто он умер от SIGINT, и в этом случае bashвыйдет (на самом деле он убьет себя с SIGINT, так что сообщить о прерывании его родителю).

Относительно того, почему bashведет себя так, я не уверен, и я отмечаю, что поведение не всегда детерминировано. Я только что задал вопрос в bashсписке рассылки для разработчиков ( Обновление : @Jilles теперь нашел причину в своем ответе ).

Единственная другая оболочка, которая, как я обнаружил, ведет себя аналогично, это ksh93 (обновление, как упомянуто @Jilles, так же, как и FreeBSDsh ). Там, SIGINT, кажется, явно игнорируется. И ksh93выходит всякий раз, когда SIGINT убивает команду.

Вы получаете то же поведение, что и bashвыше, но также:

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

Не выводит «тест». То есть он завершает свою работу (убивая себя там с помощью SIGINT), если команда, которой он ожидал, умирает от SIGINT, даже если она сама не получила этот SIGINT.

Обходной путь будет сделать добавить:

trap 'exit 130' INT

В верхней части скрипта необходимо принудительно bashзавершить работу после получения SIGINT (обратите внимание, что в любом случае SIGINT не будет обрабатываться синхронно, только после завершения текущей выполняемой команды).

В идеале мы хотели бы сообщить нашему родителю, что мы умерли от SIGINT (так что, если это, bashнапример, другой сценарий, этот bashсценарий также прерывается). Выполнение операции - exit 130это не то же самое, что умирание SIGINT (хотя для некоторых оболочек будет установлено $?одинаковое значение в обоих случаях), однако оно часто используется для сообщения о смерти SIGINT (в системах, где SIGINT равен 2, что является наибольшим).

Однако для bash, ksh93или FreeBSD sh, это не работает. Этот 130 статус выхода не рассматривается SIGINT как смерть, и родительский сценарий не прервет его там.

Поэтому, возможно, лучшей альтернативой было бы убить себя с помощью SIGINT при получении SIGINT:

trap '
  trap - INT # restore default INT handler
  kill -s INT "$$"
' INT
Стефан Шазелас
источник
1
Ответ Джиллс объясняет «почему». В качестве наглядного примера рассмотрим  for f in *.txt; do vi "$f"; cp "$f" newdir; done. Если пользователь вводит Ctrl + C во время редактирования одного из файлов, viпросто отображается сообщение. Представляется разумным, что цикл должен продолжаться после того, как пользователь закончит редактирование файла. (И да, я знаю, что вы могли бы сказать vi *.txt; cp *.txt newdir; я просто представляю forцикл в качестве примера.)
Скотт
@ Скотт, хорошая мысль. Хотя vi( vimпо крайней мере, по крайней мере) отключает tty isigпри редактировании ( :!cmdхотя это не очевидно, когда вы запускаете , и это очень применимо в этом случае).
Стефан Шазелас
@Tim, смотрите мои правки для исправления ваших правок.
Стефан
@ StéphaneChazelas Спасибо. Так происходит потому, что pingвыходит с 0 после получения SIGINT. Я обнаружил аналогичное поведение, когда скрипт bash содержит sudoвместо ping, но sudoзавершается с 1 после получения SIGINT. unix.stackexchange.com/questions/479023/…
Тим
13

Объяснение заключается в том, что bash реализует WCE (ожидание и совместный выход) для SIGINT и SIGQUIT по адресу http://www.cons.org/cracauer/sigint.html . Это означает, что если bash получает SIGINT или SIGQUIT в ожидании выхода из процесса, он будет ожидать выхода из процесса и сам выйдет, если процесс завершится по этому сигналу. Это гарантирует, что программы, которые используют SIGINT или SIGQUIT в своем пользовательском интерфейсе, будут работать должным образом (если сигнал не вызвал завершение программы, сценарий продолжится в обычном режиме).

Недостатком является появление программ, которые перехватывают SIGINT или SIGQUIT, но затем завершают работу из-за этого, но используют обычный метод exit () вместо повторной отправки сигнала самим себе. Может быть невозможно прервать сценарии, которые вызывают такие программы. Я думаю, что реальное исправление существует в таких программах, как ping и ping6.

Подобное поведение реализовано в ksh93 и FreeBSD / bin / sh, но не в большинстве других оболочек.

jilles
источник
Спасибо, это имеет большой смысл. Я отмечаю, что FreeBSD sh также не прерывается при выходе из cmd с exit (130), что является обычным способом сообщить о смерти SIGINT ребенка (например, mksh делает это, exit(130)если вы прерываете mksh -c 'sleep 10;:').
Стефан Шазелас
5

Как вы предполагаете, это связано с отправкой SIGINT подчиненному процессу и продолжением работы оболочки после завершения этого процесса.

Чтобы справиться с этим лучше, вы можете проверить состояние завершения команд, которые выполняются. Код возврата Unix кодирует как метод, которым завершился процесс (системный вызов или сигнал), так и какое значение было передано exit()или какой сигнал завершил процесс. Все это довольно сложно, но самый быстрый способ использовать его - это знать, что процесс, который был прерван сигналом, будет иметь ненулевой код возврата. Таким образом, если вы проверите код возврата в своем скрипте, вы можете выйти самостоятельно, если дочерний процесс был прерван, устраняя необходимость в неэлегансах, таких как ненужные sleepвызовы. Быстрый способ сделать это на протяжении всего сценария - использовать его set -e, хотя для команд, чей статус выхода является ненулевым, может потребоваться несколько настроек.

Том Хант
источник
1
Установка -e не работает правильно в bash, если вы не используете bash-4
schily
Что значит "не работает правильно"? Я успешно использовал его на bash 3, но, возможно, есть некоторые крайние случаи.
Том Хант
В нескольких простых случаях bash3 завершал работу при ошибке. Это, однако, не произошло в общем случае. Как типичный результат, make не остановился, когда создать цель не удалось, и это было из make-файла, который работал со списком целей в подкаталогах. Дэвиду Корну и мне пришлось много недель переписываться с сопровождающим bash, чтобы убедить его исправить ошибку в bash4.
Сhily
4
Обратите внимание, что проблема здесь в том, что pingвозвращается с состоянием выхода 0 после получения SIGINT, а bashзатем игнорирует SIGINT, который он получил сам, если это так. Добавление «set -e» или проверка состояния выхода здесь не помогут. Добавление явной ловушки на SIGINT поможет.
Стефан Шазелас,
4

Терминал замечает control-c и отправляет INTсигнал в группу процессов переднего плана, которая здесь включает в себя оболочку, поскольку pingне создал новую группу процессов переднего плана. Это легко проверить с помощью захвата INT.

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

Если выполняемая команда создала новую группу процессов переднего плана, то control-c перейдет к этой группе процессов, а не к оболочке. В этом случае оболочка должна будет проверить коды выхода, так как терминал не будет сигнализировать об этом.

( INTОбращение в оболочках может быть неправдоподобно сложным, кстати, так как оболочка иногда нужно игнорировать сигнал, а иногда и не Источник погружение , если любопытно, или вдумайтесь:. tail -f /etc/passwd; echo foo)

thrig
источник
В этом случае проблема не в обработке сигналов, а в том, что bash выполняет jobcontrol в сценарии, хотя этого не должно быть, см. Мой ответ для получения дополнительной информации
schily
Чтобы SIGINT мог перейти в новую группу процессов, команда также должна выполнить ioctl () с терминалом, чтобы сделать его приоритетной группой процессов терминала. pingу меня нет причины запускать новую группу процессов здесь, и версия ping (iputils в Debian), с которой я могу воспроизвести проблему OP, не создает группу процессов.
Стефан Шазелас
1
Обратите внимание, что это не терминал, который отправляет SIGINT, это линейная дисциплина устройства tty (драйвер (код в ядре) устройства / dev / ttysomething) при получении неэкранированного (обычно lnext ^ V) символа ^ C из терминала.
Стефан Шазелас
2

Ну, я попытался добавить sleep 1в сценарий bash, и взрыва!
Теперь я могу остановить это с двумя Ctrl+C.

При нажатии Ctrl+C, A SIGINTсигнал посылается на данный момент процесса, выполняемого команда , которая была запущена внутри цикла. Затем процесс subshell продолжает выполнение следующей команды в цикле, которая запускает другой процесс. Чтобы иметь возможность остановить сценарий, необходимо отправить два SIGINTсигнала: один для прерывания текущей команды при выполнении и один для прерывания процесса подоболочки .

В сценарии без sleepвызова нажатие Ctrl+Cдействительно быстрое и много раз, кажется, не работает, и невозможно выйти из цикла. Я предполагаю, что двойное нажатие недостаточно быстро, чтобы сделать его как раз в нужный момент между прерыванием текущего выполненного процесса и началом следующего. Каждое Ctrl+Cнажатие отправляет SIGINTпроцесс, выполняемый внутри цикла, но ни в подоболочку .

В сценарии с sleep 1этим вызовом выполнение будет приостановлено на одну секунду, а при прерывании первой Ctrl+C(первой SIGINT) подоболочке потребуется больше времени для выполнения следующей команды. Итак, теперь второй Ctrl+C(второй SIGINT) перейдет в подоболочку , и выполнение скрипта закончится.

nephewtom
источник
Вы ошибаетесь, на правильно работающей оболочке достаточно одного ^ C, см. Мой ответ для фона.
Сhily
Что ж, учитывая, что за вас проголосовали, и в настоящее время ваш ответ имеет -1 балл, я не очень уверен, что должен воспринимать ваш ответ серьезно.
племянник
Тот факт, что некоторые люди понижают голос, не всегда связан с качеством ответа. Если вам нужно набрать два раза ^ c, вы определенно являетесь жертвой ошибки bash. Вы пробовали другую оболочку? Вы пробовали настоящий Bourne Shell?
Щил
Если оболочка работает правильно, она запускает все из сценария в той же группе процессов, и тогда достаточно одного ^ c.
Щил
1
Поведение @nephewtom, описанное в этом ответе, может быть объяснено различными командами в сценарии, которые ведут себя по-разному при получении Ctrl-C. Если спящий режим присутствует, в подавляющем большинстве случаев вероятность того, что Ctrl-C будет получен во время выполнения спящего режима (при условии, что все остальное в цикле выполняется быстро). Сон прерывается с выходным значением 130. Родитель сна, оболочка, замечает, что сон был убит sigint, и выходит. Но если сценарий не содержит спящего режима, вместо этого Ctrl-C переходит к ping, который реагирует, выходя с 0, поэтому родительская оболочка продолжает выполнение следующей команды.
Джонатан Хартли
0

Попробуй это:

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

Теперь измените первую строку на:

#!/bin/sh

и попробуйте еще раз - посмотрите, не прерывается ли пинг.

Воробей
источник
0
pgrep -f process_name > any_file_name
sed -i 's/^/kill /' any_file_name
chmod 777 any_file_name
./any_file_name

например pgrep -f firefox, grep PID выполнения firefoxи сохранит этот PID в файл с именем any_file_name. Команда sed добавит killв начало номера PID в файле any_file_name имя. Третья строка будет any_file_nameфайл исполняемого файла. Теперь четвертая строка уничтожит PID, доступный в файле any_file_name. Запись вышеупомянутых четырех строк в файл и выполнение этого файла может сделать Control- C. Работает абсолютно нормально для меня.

user2176228
источник
0

Если кто-то заинтересован в исправлении этого bash функции, а не в философии, стоящей за ней , вот предложение:

Не запускайте проблемную команду напрямую, но из оболочки, которая а) ожидает ее завершения, б) не связывается с сигналами и в) делает не реализует сам механизм WCE, а просто умирает при получении а SIGINT.

Такую обертку можно сделать с помощью awk+ своейsystem() функции.

$ 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

Вставьте в сценарий, как ОП:

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

Вы - жертва хорошо известной ошибки bash. Bash делает jobcontrol для скриптов, что является ошибкой.

В результате bash запускает внешние программы в группе процессов, отличной от той, что используется для самого скрипта. Поскольку для группы процессов TTY задана группа процессов текущего переднего плана, уничтожается только этот передний процесс, и цикл в сценарии оболочки продолжается.

Чтобы проверить: выберите и скомпилируйте недавнюю оболочку Bourne, которая реализует pgrp (1) как встроенную программу, затем добавьте / bin / sleep 100 (или / usr / bin / sleep в зависимости от вашей платформы) в цикл сценария и затем запустите Борн Шелл. После того, как вы использовали ps (1) для получения идентификаторов процесса для команды sleep и bash, который запускает сценарий, вызовите pgrp <pid>и замените «<pid>» идентификатором процесса sleep и bash, который запускает сценарий. Вы увидите разные идентификаторы группы процессов. Теперь вызовите что-то вроде pgrp < /dev/pts/7(замените имя tty на tty, используемое сценарием), чтобы получить текущую группу процессов tty. Группа процессов TTY равна группе процессов команды sleep.

Чтобы исправить: используйте другую оболочку.

Последние источники Bourne Shell находятся в моем пакете инструментов schily, который вы можете найти здесь:

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

Шили
источник
Какая версия bashэто? AFAIK bashделает это только если вы передаете опцию -m или -i.
Стефан Шазелас
Кажется, что это больше не относится к bash4, но когда у ОП есть такие проблемы, он, похоже, использует bash3
schily
Не может воспроизвести с bash3.2.48, ни bash 3.0.16, ни bash-2.05b (пробовал с bash -c 'ps -j; ps -j; ps -j').
Стефан Шазелас
Это определенно происходит, когда вы называете bash as /bin/sh -ce. Мне пришлось добавить некрасивый обходной путь, smakeкоторый явно убивает группу процессов для текущей запущенной команды, чтобы позволить ^Cпрервать многослойный вызов make. Вы проверяли, изменил ли bash группу процессов по идентификатору группы процессов, с которым она была инициирована?
Сhily
ARGV0=sh bash -ce 'ps -j; ps -j; ps -j'действительно сообщает один и тот же pgid для ps и bash во всех вызовах 3 ps. (ARGV0 = sh - это zshспособ передать argv [0]).
Стефан Шазелас