Как убить и дождаться завершения фоновых процессов в сценарии оболочки, когда я нажимаю Ctrl + C?

24

Я пытаюсь настроить сценарий оболочки так, чтобы он запускал фоновые процессы, и когда я Ctrlc сценарий оболочки, он убивает дочерних элементов, а затем завершается.

Лучшее, что мне удалось придумать, это это. Похоже, чтоkill 0 -INT также убивает сценарий до того, как произойдет ожидание, поэтому сценарий оболочки умирает до завершения дочерних процессов.

Любые идеи о том, как я могу заставить этот сценарий оболочки ждать смерти детей после отправки INT?

#!/bin/bash
trap 'killall' INT

killall() {
    echo "**** Shutting down... ****"
    kill 0 -INT
    wait # Why doesn't this wait??
    echo DONE
}

process1 &
process2 &
process3 &

cat # wait forever
slipheed
источник
Я нашел этот вопрос, выполнив поиск «переключатель перехвата перехвата оболочки», спасибо, что поделились командой trap, очень полезно выполнить команду после выхода из приложения с помощью Ctrl + C, когда приложение не завершается корректно через кнопку закрытия GUI.
Baptx

Ответы:

22

Ваш kill команда обратная.

Как и во многих командах UNIX, параметры, начинающиеся с минуса, должны предшествовать другим аргументам.

Если ты пишешь

kill -INT 0

он видит в -INTкачестве опции, и посылает SIGINTк 0( 0это специальный номер означает все процессы в текущей группе процессов).

Но если вы напишите

kill 0 -INT

он видит 0, решает, что больше нет вариантов, поэтому использует SIGTERMпо умолчанию. И отправляет это в текущую группу процессов, так же, как если бы вы сделали

kill -TERM 0 -INT    

(он также попытался бы отправить SIGTERMв -INT, что вызвало бы синтаксическую ошибку, но он отправил SIGTERMбы 0сначала, и никогда не заходил так далеко.)

Таким образом, ваш основной скрипт получает SIGTERMдо того, как он запустит waitи echo DONE.

Добавлять

trap 'echo got SIGTERM' TERM

наверху, сразу после

trap 'killall' INT

и запустите его снова, чтобы доказать это.

Как указывает Стефан Шазелас, ваши дети ( process1и т. Д.) Будут игнорироватьSIGINT По умолчанию по умолчанию.

В любом случае, я думаю, что отправка SIGTERMбудет иметь больше смысла.

Наконец, я не уверен, kill -process groupгарантировано ли будет идти в первую очередь к детям. Игнорирование сигналов при выключении может быть хорошей идеей.

Итак, попробуйте это:

#!/bin/bash
trap 'killall' INT

killall() {
    trap '' INT TERM     # ignore INT and TERM while shutting down
    echo "**** Shutting down... ****"     # added double quotes
    kill -TERM 0         # fixed order, send TERM not INT
    wait
    echo DONE
}

./process1 &
./process2 &
./process3 &

cat # wait forever
Mikel
источник
Спасибо, это работает! Похоже, это было проблемой.
Slheed
9

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

(trap - INT; exec process1) &
(trap - INT; exec process2) &
trap '' INT
wait

Потому что process1 и process2 получают SIGINT, когда вы нажимаете Ctrl-C, так как они являются частью одной группы процессов, которая является основной группой процессов терминала.

Приведенный выше код будет работать с pdksh и zsh, которые в этом отношении не соответствуют POSIX.

С другими оболочками вам придется использовать что-то еще для восстановления обработчика по умолчанию для SIGINT, например:

perl -e '$SIG{INT}=DEFAULT; exec "process1"' &

или используйте другой сигнал, такой как SIGTERM.

Стефан Шазелас
источник
2

Для тех, кто просто хочет убить процесс и дождаться его смерти, но не бесконечно :

Максимальное время ожидания 60 секунд для каждого типа сигнала.

Предупреждение. Этот ответ никоим образом не связан с перехватом сигнала уничтожения и его отправкой.

# close_app_sub GREP_STATEMENT SIGNAL DURATION_SEC
# GREP_STATEMENT must not match itself!
close_app_sub() {
    APP_PID=$(ps -x | grep "$1" | grep -oP '^\s*\K[0-9]+' --color=never)
    if [ ! -z "$APP_PID" ]; then
        echo "App is open. Trying to close app (SIGNAL $2). Max $3sec."
        kill $2 "$APP_PID"
        WAIT_LOOP=0
        while ps -p "$APP_PID" > /dev/null 2>&1; do
            sleep 1
            WAIT_LOOP=$((WAIT_LOOP+1))
            if [ "$WAIT_LOOP" = "$3" ]; then
                break
            fi
        done
    fi
    APP_PID=$(ps -x | grep "$1" | grep -oP '^\s*\K[0-9]+' --color=never)
    if [ -z "$APP_PID" ]; then return 0; else return "$APP_PID"; fi
}

close_app() {
    close_app_sub "$1" "-HUP" "60"
    close_app_sub "$1" "-TERM" "60"
    close_app_sub "$1" "-SIGINT" "60"
    close_app_sub "$1" "-KILL" "60"
    return $?
}

close_app "[f]irefox"

Он выбирает приложение для уничтожения по имени или аргументам. Сохраняйте скобки для первой буквы названия приложения, чтобы избежать совпадения с самим grep.

С некоторыми изменениями вы можете напрямую использовать PID или более простое pidof process_nameвместо psоператора.

Детали кода: последний шаг - получить PID без пробелов.

KrisWebDev
источник
1

Если вам нужно управлять некоторыми фоновыми процессами, почему бы не использовать функции управления заданиями bash?

$ gedit &
[1] 2581
$ emacs &
[2] 2594
$ jobs
[1]-  Running                 gedit &
[2]+  Running                 emacs &
$ jobs -p
2581
2594
$ kill -2 `jobs -p`
$ jobs
[1]-  Interrupt               gedit
[2]+  Done                    emacs
Максим
источник
0

Так что я тоже возился с этим. В Bash вы можете определять функции и делать с ними причудливые вещи. Я и мои коллеги используем terminator, поэтому для пакетного выполнения мы обычно создаем целую кучу окон-терминаторов, если хотим просмотреть вывод (менее элегантный, чем tmuxвкладки, но вы можете использовать более похожий на GUI интерфейс, ха-ха).

Вот настройки, которые я придумал, а также пример того, что вы можете запустить:

#!/bin/bash
custom()
{
    terun 'something cool' 'yes'
    run 'xclock'
    terun 'echoes' 'echo Hello; echo Goodbye; read'
    func()
    {
        local i=0
        while :
        do
            echo "Iter $i"
            let i+=1
            sleep 0.25
        done
    }
    export -f func
    terun 'custom func' 'func'
}

# [ Setup ]
run()
{
    "$@" &
    # Give process some time to start up so windows are sequential
    sleep 0.05
}
terun()
{
    run terminator -T "$1" -e "$2"
}
finish()
{
    procs="$(jobs -p)"
    echo "Kill: $procs"
    # Ignore process that are already dead
    kill $procs 2> /dev/null
}
trap 'finish' 2

custom

echo 'Press <Ctrl+C> to kill...'
wait

Запуск это

$ ./program_spawner.sh 
Press <Ctrl+C> to kill...
^CKill:  9835
9840
9844
9850
$ 

Edit @Maxim, только что увидел ваше предложение, что делает его намного проще! Благодарность!

eacousineau
источник