Захват SIGINT с использованием исключения KeyboardInterrupt работает в терминале, а не в script

Я пытаюсь поймать SIGINT (или прерывание клавиатуры) в программе Python 2.7. Вот как выглядит мой тест Python script test:

#!/usr/bin/python

import time

try:
    time.sleep(100)
except KeyboardInterrupt:
    pass
except:
    print "error"

Далее у меня есть оболочка script test.sh:

./test & pid=$!
sleep 1
kill -s 2 $pid

Когда я запускаю script с помощью bash, или sh, или что-то bash test.sh, процесс Python test остается запущенным и не может быть уничтожен с помощью SIGINT. Если я копирую команду test.sh и вставляю ее в терминал (bash), процесс Python test отключается.

Я не могу понять, что происходит, что я хотел бы понять. Итак, где разница и почему?

Это не о том, как поймать SIGINT в Python! Согласно docs - это способ, который должен работать:

Python устанавливает небольшое количество обработчиков сигналов по умолчанию: SIGPIPE... и SIGINT преобразуется в исключение KeyboardInterrupt

Это действительно ловит KeyboardInterrupt, когда SIGINT отправляется kill, если программа запускается непосредственно из оболочки, но когда программа запускается из bash script на фоновом режиме, кажется, что KeyboardInterrupt никогда не поднимается.

Ответ 1

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

Маска сигнала игнорируемых сигналов наследуется от родительского процесса, а обработанные сигналы reset - SIG_DFL. Поэтому в случае игнорирования SIGINT условие if (Handlers[SIGINT].func == DefaultHandler) в источнике не запускается, а обработчик по умолчанию не установлен, python не переопределяет настройки, выполненные родительским процессом в этом случае.

Итак, попробуйте показать обработчик использованного сигнала в разных ситуациях:

# invocation from interactive shell
$ python -c "import signal; print(signal.getsignal(signal.SIGINT))"
<built-in function default_int_handler>

# background job in interactive shell
$ python -c "import signal; print(signal.getsignal(signal.SIGINT))" &
<built-in function default_int_handler>

# invocation in non interactive shell
$ sh -c 'python -c "import signal; print(signal.getsignal(signal.SIGINT))"'
<built-in function default_int_handler>

# background job in non-interactive shell
$ sh -c 'python -c "import signal; print(signal.getsignal(signal.SIGINT))" &'
1

Итак, в последнем примере SIGINT установлено значение 1 (SIG_IGN). Это то же самое, что и при запуске фонового задания в оболочке script, поскольку по умолчанию они не являются интерактивными (если вы не используете параметр -i в shebang).

Таким образом, это вызвано тем, что оболочка игнорирует сигнал при запуске фонового задания в неинтерактивном сеансе оболочки, а не непосредственно с помощью python. По крайней мере bash и dash ведут себя так, я не пробовал другие оболочки.

Существует два варианта решения этой проблемы:

  • вручную установите обработчик сигнала по умолчанию:

    import signal
    signal.signal(signal.SIGINT, signal.default_int_handler)
    
  • добавьте параметр -i к shebang оболочки script, например:

    #!/bin/sh -i
    

edit: это поведение описано в руководстве bash:

СИГНАЛЫ
...
Когда управление заданиями не действует, асинхронные команды игнорируют SIGINT и SIGQUIT в дополнение к этим унаследованным обработчикам.

который применяется к неинтерактивным оболочкам, поскольку по умолчанию они отключены, и на самом деле они указаны в POSIX: Командный язык оболочки