
У меня есть программа, которая игнорирует SIGINT, но которую я хочу запустить на переднем плане. Я хотел бы найти способ принудительно закрыть ее по Ctrl-C. Есть ли способ написать оболочку (которую вы бы назвали ./wrapper.sh my_program
), которая заставила бы неправильно работающую программу завершить работу, возможно, обнаружив проигнорированный SIGINT и сгенерировав SIGKILL?
Этот ответэто полная противоположность тому, что я ищу — я хотел бы заставить программу, которая игнорирует сигнал, завершить работу при получении сигнала SIGINT.
решение1
Давайте создадим оболочку, которая «конвертирует» SIGINT в SIGKILL.
Нам нужен процесс, который выполняется my_program
и получает SIGINT при Ctrl+ c. Когда он получает SIGINT, он должен отправить SIGKILL в my_program
. Обратите внимание, что нам не нужно my_program
на самом деле получать SIGINT, вызванный Ctrl+ c; достаточно, чтобы он получил SIGKILL, вызванный дополнительным процессом.
Соответствующие факты:
- Стандартный способ запуска одного процесса вместе с другим — это запуск одного из них асинхронно в фоновом режиме (т. е. с завершением
&
). - Когда вы нажимаете Ctrl+ c, ваш эмулятор терминала отправляет сигнал SIGINT процессам в группе процессов переднего плана.
- Некоторые (простые) оболочки запускают все в одной группе процессов. Если им приказано запустить команду в фоновом режиме, они перенаправят ее stdin в
/dev/null
или эквивалентный файл, чтобы команда не могла украсть ввод. - Другие оболочки могут запускать каждую команду в отдельной группе процессов. Если им приказано запустить команду в фоновом режиме, то они оставят ее stdin как есть; тем не менее, команда в фоновом режиме не сможет украсть ввод с управляющего терминала из-за
SIGTTIN
. Это позволяет оболочке переместить задачу из фонового режима на передний план, информируя терминал о новой группе процессов переднего плана. Механизм называется управлением задачами и может быть отключен. В скриптах он отключен по умолчанию. - В любом случае процесс в фоновом режиме не может читать с терминала. Это означает, что мы не должны работать
my_program
в фоновом режиме в случае, если stdin — это терминал иmy_program
нужно читать с него. - К сожалению, в некоторых оболочках мы также не должны запускать другой процесс в фоновом режиме. Некоторые оболочки используют отдельные группы процессов, даже если управление заданиями отключено. Другой процесс, не находящийся в группе процессов переднего плана, не получит SIGINT при Ctrl+ c, поэтому он не сможет «преобразовать» его в SIGKILL.
- В моем Debian 10
posh
есть оболочка, которая запускает все в одной группе процессов.
Это приводит к следующей обертке:
#!/usr/bin/env posh
( trap 'kill -s KILL 0' INT
while kill -s 0 "$$" 2>/dev/null; do sleep 1; done
) &
exec "$@"
exec "$@"
запускается my_program
(или что-то еще, что вы укажете), возможно, с аргументами. Благодаря exec
my_program
заменит оболочку. Он не только сможет читать из stdin, который может быть терминалом; его PID будет таким же, как у замененной оболочки. В этом смысле оболочка прозрачна. Кроме того, у этого есть приятная особенность: PID my_program
известен до my_program
запуска, и мы можем легко использовать его в другом процессе (в данном случае в подоболочке в фоновом режиме), чтобы определить, когда он my_program
действительно завершится.
trap
"Преобразует" SIGINT в SIGKILL. Примечание отправляет kill -s KILL 0
SIGKILL всей группе процессов, включая потомков , my_program
если они находятся в группе (если вы хотите убить my_program
только , то используйте kill -s KILL "$$"
вместо этого ). Тем не менее kill -s 0 "$$"
проверяет существование my_program
только .
Есть альтернатива, которая не требует оболочки, которая запускает все в одной группе процессов. Хитрость в том, что процессы в конвейере должны запускаться в одной группе процессов; и с помощью умного перенаправления вы можете построить конвейер, где части не связаны друг с другом.
#!/bin/sh -
exec 9>&1
( "$@"; kill -s TERM 0 ) >&9 9>&- | (
trap 'kill -s KILL 0' INT
while :; do sleep 1; done
)
В этом варианте my_program
не будет заменяться оболочка. Второй kill
— «преобразовать» SIGINT в SIGKILL. Первый kill
— завершить цикл в случае my_program
выхода в обстоятельствах, когда цикл в противном случае выжил бы.
Есть несколько сценариев, которые могут работать не так, как вы ожидаете (с любой оберткой). Среди них:
- Если
my_program
разветвляется и выходит, оставляя настоящую работу потомку. Проблемная программа, о которой идет речь, скорее всего, не ведет себя так, поскольку вы пытались ее + Ctrl. cНо в целом это может быть так. - Если
my_program
порождаются дочерние процессы в другой группе(ах) процессов, и вы хотите завершить их вместе с их родителем. - Если
my_program
настраивает терминал так, чтобы он не отправлял SIGINT при Ctrl+ c. Если вы подозреваете, что это происходит, улучшите обертку, поместивstty -F /dev/tty intr ^C
междуsleep 1
иdone
. В этом случае программа может даже не игнорировать SIGINT, она может просто убедиться, что не получит его от терминала. Так что, возможно, SIGKILL — это излишество; возможно, достаточно восстановить эту функциональность терминала, и Ctrl+ cначнет работать.
Из комментария:
Программы, игнорирующие SIGINT, обычно делают это по очень веской причине.
Правда. Попробуйте SIGTERM или SIGHUP вместо SIGKILL. Возможно, рассматриваемая программа не игнорирует хотя бы один из них и завершается корректно с надлежащей очисткой.