Неможливо зупинити скрипт 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?

Відповіді:


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цикл як приклад.)
Скотт,

@Scott, хороший момент. Хоча vi( vimщонайменше) вимикає tty isigпід час редагування (це не очевидно, коли ви запускаєте :!cmd, і це дуже застосувало б у цьому випадку).
Stéphane Chazelas

@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, але потім припиняються через нього, але використовують звичайний вихід () замість того, щоб повторно подати сигнал собі. Можливо, не вдасться перервати скрипти, які викликають такі програми. Я думаю, справжнє виправлення є в таких програмах, як ping і ping6.

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


Дякую, це має багато сенсу. Я зауважу, FreeBSD sh не припиняє, коли cmd виходить із виходом (130), що є загальним способом повідомляти про смерть SIGINT дитини (mksh робить, exit(130)наприклад, якщо ви перериваєтесь mksh -c 'sleep 10;:').
Стефан Шазелас

5

Як ви вважаєте, це пов’язано з тим, що SIGINT надсилається до підлеглого процесу, і оболонка, що продовжується після цього процесу, закінчується.

Щоб краще впоратися з цим, ви можете перевірити стан виходу команд, які виконуються. Код повернення Unix кодує як метод, за допомогою якого процес завершився (системний виклик або сигнал), так і яке значення було передано exit()або який сигнал завершив процес. Це все досить складно, але найшвидший спосіб його використання - це знати, що процес, який був припинений сигналом, матиме ненульовий код повернення. Таким чином, якщо ви перевірите код повернення у своєму сценарії, ви можете вийти з себе, якщо дочірній процес було припинено, усуваючи потребу в неелементах, як непотрібні sleepдзвінки. Швидкий спосіб зробити це у вашому сценарії - це використовувати set -e, хоча це може вимагати декількох налаштувань для команд, статус виходу яких очікується ненульовим.


1
Set -e не працює правильно в bash, якщо ви не використовуєте bash-4
schily

Що означає "не працює правильно"? Я успішно використовував його на bash 3, але, мабуть, є певні крайні випадки.
Том Хант

У кількох простих випадках bash3 дійсно вийшов із помилки. Однак цього в загальному випадку не сталося. Як типовий результат, make не зупинявся, коли створення цілі не вдалося, і це було зроблено з makefile, який працював над списком цілей у підкаталогах. Ми з Девідом Корном мусили багато тижнів відправляти пошту з обслуговувачем bash, щоб переконати його виправити помилку bash4.
schily

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)


У цьому випадку проблема полягає не в обробці сигналів, а в тому, що bash виконує контроль за сценарієм, хоча і не повинен, дивіться мою відповідь для отримання додаткової інформації
schily

Для того, щоб SIGINT перейшов до нової групи процесів, команда також повинна буде зробити ioctl () до терміналу, щоб зробити його групою переднього плану терміналу. pingне має підстав запускати тут нову групу процесів, і версія ping (iputils на Debian), з якою я можу відтворити проблему OP, не створює групу процесів.
Stéphane Chazelas

1
Зауважте, що не термінал надсилає SIGINT, це дисципліна рядка пристрою tty (драйвер (код у ядрі) пристрою / dev / ttysomething) після отримання нерозміщеного символу (як правило, звичайно ^ V) ^ C від терміналу.
Стефан Шазелас

2

Ну, я спробував додати sleep 1баш-скрипт, і баг!
Тепер я можу зупинити це з двома Ctrl+C.

При натисканні Ctrl+C, A SIGINTсигнал посилається на даний момент процесу, виконуваного команда , яка була запущена всередині циклу. Потім процес допоміжної оболонки продовжує виконувати наступну команду в циклі, яка запускає інший процес. Щоб мати змогу зупинити скрипт, необхідно надіслати два SIGINTсигнали, один для переривання поточної команди у виконанні та другий для переривання процесу підзарядки .

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

У скрипті з sleep 1, цей виклик призупинить виконання на одну секунду, а коли перерветься першим Ctrl+C(першим SIGINT), підпакет знадобиться більше часу для виконання наступної команди. Отже, другий Ctrl+C(друга SIGINT) піде на субоболочке , і виконання скрипта закінчиться.


Ви помиляєтесь, що на правильно працюючій оболонці достатньо одного C, див. Мою відповідь для фону.
schily

Ну, враховуючи, що ви проголосували, і на даний момент у вашій відповіді є оцінка -1, я не дуже переконаний, що варто сприймати вашу відповідь серйозно.
nephewtom

Той факт, що деякі люди беруть участь у голосуванні, не завжди пов'язаний з якістю відповіді. Якщо вам потрібно ввести два рази ^ c, ви, безумовно, стаєте жертвою баш-помилки. Ви спробували іншу оболонку? Ви спробували справжню Борн Шелл?
schily

Якщо оболонка працює правильно, вона запускає все, починаючи зі сценарію в одній групі процесів, і тоді достатньо одного ^ c.
schily

1
Поведінка @nephewtom, описана в цій відповіді, може бути пояснена різними командами в сценарії, що поводяться по-різному, коли вони отримують Ctrl-C. Якщо сон присутній, велика ймовірність, що Ctrl-C буде отриманий під час виконання сну (якщо припустити, що все в циклі швидко). Сон вбивається, зі значенням виходу 130. Батько сну, оболонка, помічає, що сон був убитий підписом, і виходить. Але якщо сценарій не містить сну, тоді 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, зніме PID запущеного firefoxта збереже цей PID у файлі під назвою any_file_name. Команда 'sed' додасть на killпочатку номера PID у файлі 'any_file_name'. Третій рядок буде any_file_nameфайлом виконуваним. Тепер четвертий рядок знищить доступний у файлі PID any_file_name. Запис вищевказаних чотирьох рядків у файл та виконання цього файлу може зробити Control- C. Працює для мене абсолютно чудово.


0

Якщо когось цікавить виправлення цієї bashфункції, а не стільки філософія, що стоїть за нею , ось вам пропозиція:

Не запускайте проблематичну команду безпосередньо, але з обгортки, яка а) чекає, коли вона закінчиться; б) не возиться зі сигналами; в) не реалізує сам механізм ВКЕ, а просто вмирає після отримання 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

-3

Ви жертва добре відомого баш-багу. Bash виконує контроль за сценаріями, що є помилкою.

Що відбувається, це те, що bash запускає зовнішні програми в іншій групі процесів, ніж вона використовується для самого сценарію. Оскільки група процесів TTY встановлена ​​в групі процесів поточного процесу переднього плану, тільки цей процес переднього плану загине, і цикл у сценарії оболонки продовжується.

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

Для виправлення: використовуйте іншу оболонку.

Останні джерела Борна Шелла знаходяться в моєму пакеті інструментів 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 як /bin/sh -ce. Мені довелося додати некрасиве вирішення, smakeяке явно вбиває групу процесів для поточно запущеної команди, щоб дозволити ^Cперервати багатошаровий дзвінок. Ви перевірили, чи змінив bash групу процесів з ідентифікатора групи процесів, з якого вона була ініційована?
schily

ARGV0=sh bash -ce 'ps -j; ps -j; ps -j'повідомляє про той самий pgid для ps та bash у всіх викликах 3 PS. (ARGV0 = sh - zshспосіб передачі argv [0]).
Стефан Шазелас
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.