Я пытаюсь обработать SIGINT
( CtrlC) таким образом, что если пользователь случайно нажмет ctrl-c, ему будет выдано сообщение «Вы хотите выйти? (y/n)». Если он введет «да», то выйдите из скрипта. Если «нет», то продолжите с того места, где произошло прерывание. По сути, мне нужно CtrlCработать аналогично SIGTSTP
( CtrlZ), но немного по-другому. Я пробовал разные способы добиться этого, но не получил ожидаемых результатов. Ниже приведены несколько сценариев, которые я пробовал.
Дело 1
Сценарий:play.sh
#!/bin/sh
function stop()
{
while true; do
read -rep $'\nDo you wish to stop playing?(y/n)' yn
case $yn in
[Yy]* ) echo "Thanks for playing !!!"; exit 1;;
[Nn]* ) break;;
* ) echo "Please answer (y/n)";;
esac
done
}
trap 'stop' SIGINT
echo "going to sleep"
for i in {1..100}
do
echo "$i"
sleep 3
done
echo "end of sleep"
При запуске приведенного выше скрипта я получаю ожидаемые результаты.
Выход:
$ play.sh
going to sleep
1
^C
Do you wish to stop playing?(y/n)y
Thanks for playing !!!
$ play.sh
going to sleep
1
2
^C
Do you wish to stop playing?(y/n)n
3
4
^C
Do you wish to stop playing?(y/n)y
Thanks for playing !!!
$
Случай: 2
Я перенес for
цикл в новый скрипт loop.sh
, таким образом, play.sh
он становится родительским процессом и loop.sh
дочерним процессом.
Сценарий:play.sh
#!/bin/sh
function stop()
{
while true; do
read -rep $'\nDo you wish to stop playing?(y/n)' yn
case $yn in
[Yy]* ) echo "Thanks for playing !!!"; exit 1;;
[Nn]* ) break;;
* ) echo "Please answer (y/n)";;
esac
done
}
trap 'stop' SIGINT
loop.sh
Сценарий:loop.sh
#!/bin/sh
echo "going to sleep"
for i in {1..100}
do
echo "$i"
sleep 3
done
echo "end of sleep"
В этом случае результат не соответствует ожидаемому.
Выход:
$ play.sh
going to sleep
1
2
^C
Do you wish to stop playing?(y/n)y
Thanks for playing !!!
$ play.sh
going to sleep
1
2
3
4
^C
Do you wish to stop playing?(y/n)n
$
Я понимаю, что когда процесс получает SIGINT
сигнал, он распространяет его на все дочерние процессы, поэтому мой 2-й случай терпит неудачу. Есть ли способ избежать SIGINT
распространения на дочерние процессы и таким образом сделать так, чтобы loop.sh
работа выполнялась точно так же, как в 1-м случае?
Примечание: Это всего лишь пример моего реального приложения. Приложение, над которым я работаю, имеет несколько дочерних скриптов в play.sh
и loop.sh
. Я должен убедиться, что приложение при получении SIGINT
не должно завершаться, а должно выводить пользователю сообщение.
решение1
Отличный классический вопрос об управлении заданиями и сигналами с хорошими примерами! Я разработал урезанный тестовый сценарий, чтобы сосредоточиться на механике обработки сигналов.
Для этого после запуска дочерних процессов (loop.sh) в фоновом режиме вызовите wait
, а при получении сигнала INT — kill
группу процессов, PGID которой равен вашему PID.
Для рассматриваемого сценария
play.sh
это можно сделать следующим образом:В
stop()
функции заменитеexit 1
наkill -TERM -$$ # note the dash, negative PID, kills the process group
Запустить
loop.sh
как фоновый процесс (здесь можно запустить несколько фоновых процессов и управлять имиplay.sh
)loop.sh &
Добавьте
wait
в конец скрипта ожидание всех детей.wait
Когда ваш скрипт запускает процесс, этот дочерний процесс становится членом группы процессов с PGID, равным PID родительского процесса, находящегося $$
в родительской оболочке.
Например, скрипт trap.sh
запустил три sleep
процесса в фоновом режиме и теперь wait
выполняет их. Обратите внимание, что столбец идентификатора группы процессов (PGID) совпадает с PID родительского процесса:
PID PGID STAT COMMAND
17121 17121 T sh trap.sh
17122 17121 T sleep 600
17123 17121 T sleep 600
17124 17121 T sleep 600
В Unix и Linux вы можете отправить сигнал каждому процессу в этой группе процессов, вызвав kill
с отрицательным значением PGID
. Если вы дадите kill
отрицательное число, оно будет использоваться как -PGID. Поскольку PID скрипта ( $$
) совпадает с его PGID, вы можете указать kill
свою группу процессов в оболочке с помощью
kill -TERM -$$ # note the dash before $$
вам необходимо указать номер или имя сигнала, в противном случае некоторые реализации kill выдадут сообщение «Недопустимая опция» или «Неверная спецификация сигнала».
Простой код ниже иллюстрирует все это. Он устанавливает trap
обработчик сигнала, порождает 3 потомка, затем переходит в бесконечный цикл ожидания, ожидая, чтобы убить себя командой kill
группы процессов в обработчике сигнала.
$ cat trap.sh
#!/bin/sh
signal_handler() {
echo
read -p 'Interrupt: ignore? (y/n) [Y] >' answer
case $answer in
[nN])
kill -TERM -$$ # negative PID, kill process group
;;
esac
}
trap signal_handler INT
for i in 1 2 3
do
sleep 600 &
done
wait # don't exit until process group is killed or all children die
Вот пример запуска:
$ ps -o pid,pgid,stat,args
PID PGID STAT COMMAND
8073 8073 Ss /bin/bash
17111 17111 R+ ps -o pid,pgid,stat,args
$
OK, никаких дополнительных процессов не запущено. Запустите тестовый скрипт, прервите его ( ^C
), выберите игнорирование прерывания, а затем приостановите его ( ^Z
):
$ sh trap.sh
^C
Interrupt: ignore? (y/n) [Y] >y
^Z
[1]+ Stopped sh trap.sh
$
Проверьте запущенные процессы, обратите внимание на номера групп процессов ( PGID
):
$ ps -o pid,pgid,stat,args
PID PGID STAT COMMAND
8073 8073 Ss /bin/bash
17121 17121 T sh trap.sh
17122 17121 T sleep 600
17123 17121 T sleep 600
17124 17121 T sleep 600
17143 17143 R+ ps -o pid,pgid,stat,args
$
Выведем наш тестовый скрипт на передний план ( fg
) и снова прервем ( ^C
), на этот раз выбравнетигнорировать:
$ fg
sh trap.sh
^C
Interrupt: ignore? (y/n) [Y] >n
Terminated
$
Проверьте запущенные процессы, больше никакого сна:
$ ps -o pid,pgid,stat,args
PID PGID STAT COMMAND
8073 8073 Ss /bin/bash
17159 17159 R+ ps -o pid,pgid,stat,args
$
Примечание о вашей оболочке:
Мне пришлось изменить ваш код, чтобы он запустился на моей системе. У вас есть #!/bin/sh
первая строка в ваших скриптах, но скрипты используют расширения (из bash или zsh), которые недоступны в /bin/sh.
решение2
«Я понимаю, что когда процесс получает сигнал SIGINT, он распространяет сигнал на все дочерние процессы»
Откуда у вас эта неверная идея?
$ perl -E '$SIG{INT}=sub { say "ouch $$" }; if (fork()) { say "parent $$"; sleep 3; kill 2, $$ } else { say "child $$"; sleep 99 }'
parent 25831
child 25832
ouch 25831
$
Если бы имело место распространение, как утверждается, можно было бы ожидать «ой» от дочернего процесса.
В действительности:
«Всякий раз, когда мы нажимаем клавишу прерывания на нашем терминале (часто DELETE или Control-C) или клавишу выхода (часто Control-обратная косая черта), это приводит к отправке либо сигнала прерывания, либо сигнала выхода всем процессам в группе процессов переднего плана» — У. Ричард Стивенс. «Расширенное программирование в среде UNIX®». Addison-Wesley. 1993. стр. 246.
Это можно наблюдать через:
$ perl -E '$SIG{INT}=sub { say "ouch $$" }; fork(); sleep 99'
^Couch 25971
ouch 25972
$
Поскольку все процессы группы процессов переднего плана будут получать сигнал SIGINT
от Control+C, вам необходимо спроектировать все процессы так, чтобы они правильно обрабатывали или игнорировали этот сигнал, в зависимости от ситуации, или, возможно, сделать так, чтобы подпроцессы стали новой группой процессов переднего плана, чтобы родительский процесс (например, оболочка) не видел сигнал, поскольку он больше не находится в группе процессов переднего плана.
решение3
В play.sh
исходном файле цикла такой:
source ./loop.sh
Подоболочка, из которой был выполнен выход из-за ловушки, не может быть возвращена после выхода из ловушки.
решение4
Проблема в sleep
.
Когда тыCTRL+C
ты убиваешь sleep
- не твойsh
(даже если синтаксис в вашем случае #!/bin/sh
немного подозрителен). Однако вся группа процессов получает сигнал, поскольку вы не устанавливаете свой обработчик в loop.sh
(который является подоболочкой) — он сразу же завершается.
Вам нужна ловушка в loop.sh
. Ловушки очищаются для каждой запущенной подоболочки, если они явно не указаныtrap ''
SIG
игнорируется родителем.
Вам также необходимо контролировать -m
задания в родительском процессе, чтобы он отслеживал своих дочерних процессов. wait
Например, работает только при управлении заданиями. -m
Режим контроля — это то, как оболочки взаимодействуют с терминалами.
sh -cm ' . /dev/fd/3' 3<<"" # `-m`onitor is important
sh -c ' kill -INT "$$"'& # get the sig#
wait %%;INT=$? # keep it in $INT
trap " stty $(stty -g;stty -icanon)" EXIT # lose canonical input
loop()( trap exit INT # this is a child
while [ "$#" -le 100 ] # same as yours
do echo "$#" # writing the iterator
set "" "$@" # iterating
sleep 3 # sleeping
done
)
int(){ case $1 in # handler fn
($INT) printf "\33[2K\rDo you want to stop playing? "
case $(dd bs=1 count=1; echo >&3) in
([Nn]) return
esac; esac; exit "$1"
} 3>&2 >&2 2>/dev/null
while loop ||
int "$?"
do :; done
0
1
2
3
4
Do you want to stop playing? n
0
1
2
3
Do you want to stop playing? N
0
1
Do you want to stop playing? y
[mikeserv@desktop tmp]$