Как убить фоновые процессы / задания при выходе из моего сценария оболочки?

194

Я ищу способ навести порядок, когда выйдет мой скрипт верхнего уровня.

Особенно, если я хочу использовать set -e, я хотел бы, чтобы фоновый процесс умер при выходе из скрипта.

Elmarco
источник

Ответы:

186

Чтобы навести порядок, trapможно использовать. Он может предоставить список вещей, выполняемых при поступлении определенного сигнала:

trap "echo hello" SIGINT

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

trap "killall background" EXIT

Это встроенный, поэтому help trapдаст вам информацию (работает с Bash). Если вы хотите убить только фоновые задания, вы можете сделать

trap 'kill $(jobs -p)' EXIT

Остерегайтесь использовать одиночный ', чтобы предотвратить $()немедленную замену оболочки .

Йоханнес Шауб - Литб
источник
тогда как ты убиваешь только ребенка ? (или я что-то
упускаю
18
killall убивает ваших детей, но не вас
orip
4
kill $(jobs -p)не работает в dash, потому что выполняет подстановку команд в подоболочке (см. Подстановка команд в man dash)
user1431317
9
это killall backgroundдолжно быть заполнителем? backgroundнет в
Эван Бенн
170

Это работает для меня (улучшилось благодаря комментаторам):

trap "trap - SIGTERM && kill -- -$$" SIGINT SIGTERM EXIT
  • kill -- -$$отправляет SIGTERM всей группе процессов, таким образом убивая также потомков.

  • Указание сигнала EXITполезно при использовании set -e(подробнее здесь ).

tokland
источник
1
В целом должно работать хорошо, но дочерние процессы могут менять группы процессов. С другой стороны, он не требует контроля за работой, а также может привести к тому, что другие процессы будут пропускать некоторые процессы внука.
Майкл
5
Обратите внимание: «kill 0» также убивает родительский скрипт bash. Вы можете использовать «kill - - $ BASHPID», чтобы убить только дочерние элементы текущего сценария. Если у вас нет BASHPID в вашей bash-версии, вы можете экспортировать BASHPID = $ (sh -c 'echo $ PPID')
ACyclic
2
Спасибо за приятное и понятное решение! К сожалению, это segfaults Bash 4.3, который допускает рекурсию ловушек. Я столкнулся с этим на 4.3.30(1)-releaseOSX, и это также подтверждается на Ubuntu . Впрочем , есть и обвоиусный оборот :)
скозин
4
Я не совсем понимаю -$$. Он оценивается как '- <PID> `например -1234. На встроенной man-странице kill // главная черточка указывает сигнал для отправки. Однако - возможно, это блокирует, но в противном случае начальная черта не документирована. Любая помощь?
Эван Бенн
4
@EvanBenn: проверка man 2 kill, которая объясняет, что когда PID является отрицательным, сигнал отправляется всем процессам в группе процессов с предоставленным идентификатором ( en.wikipedia.org/wiki/Process_group ). Это сбивает с толку, что это не упоминается в man 1 killили man bash, и может рассматриваться как ошибка в документации.
user001
111

Обновление: https://stackoverflow.com/a/53714583/302079 улучшает это, добавляя статус выхода и функцию очистки.

trap "exit" INT TERM
trap "kill 0" EXIT

Зачем конвертировать INTи TERMвыходить? Потому что оба должны вызывать kill 0без входа в бесконечный цикл.

Почему триггер kill 0на EXIT? Потому что нормальные выходы скрипта kill 0тоже должны срабатывать .

Почему kill 0? Потому что вложенные вложенные оболочки также должны быть убиты. Это уничтожит все дерево процессов .

Коркман
источник
3
Единственное решение для моего случая на Debian.
MindlessRanger
3
Ни ответ Йоханнеса Шауба, ни ответ, предоставленный Tokland, не смогли уничтожить фоновые процессы, запущенные моим сценарием оболочки (в Debian). Это решение сработало. Я не знаю, почему за этот ответ не проголосовали больше. Не могли бы вы подробнее рассказать о том, что именно kill 0означает / делает?
Йош
7
Это потрясающе, но также убивает мою родительскую оболочку :-(
vidstige
5
Это решение буквально излишне. kill 0 (внутри моего скрипта) разрушил всю мою сессию X! Возможно, в некоторых случаях kill 0 может быть полезным, но это не меняет того факта, что это не общее решение, и его следует избегать, если это возможно, если только нет веских причин для его использования. Было бы неплохо добавить предупреждение о том, что он может уничтожить родительскую оболочку или даже весь сеанс X, а не только фоновые задания скрипта!
Лиссанро Райен
3
Хотя это может быть интересным решением при некоторых обстоятельствах, как указывает @vidstige, это убьет всю группу процессов, которая включает в себя процесс запуска (т.е. родительскую оболочку в большинстве случаев). Определенно не то, что вы хотите, когда вы запускаете скрипт через IDE.
Созрев
21

trap 'kill $ (jobs -p)' EXIT

Я бы внес лишь незначительные изменения в ответ Йоханнеса и использовал бы задания -pr, чтобы ограничить уничтожение запущенными процессами и добавить еще несколько сигналов в список:

trap 'kill $(jobs -pr)' SIGINT SIGTERM EXIT
Raytraced
источник
Почему бы не убить остановленные работы тоже? В Bash EXIT ловушка будет запускаться также в случае SIGINT и SIGTERM, поэтому ловушка будет вызываться дважды в случае такого сигнала.
Ярно
14

trap 'kill 0' SIGINT SIGTERM EXITРешение , описанное в @ tokland отвечают очень приятно, но последняя Bash падает с ошибкой segmantation при ее использовании. Это потому, что Bash, начиная с v. 4.3, допускает рекурсию ловушек, которая становится бесконечной в этом случае:

  1. процесс оболочки получает SIGINTили SIGTERMили EXIT;
  2. сигнал попадает в ловушку, выполняется kill 0, что отправляет SIGTERMвсем процессам в группе, включая саму оболочку;
  3. перейти к 1 :)

Это можно обойти, вручную отменив регистрацию ловушки:

trap 'trap - SIGTERM && kill 0' SIGINT SIGTERM EXIT

Более причудливый способ, который позволяет печатать полученный сигнал и избегает сообщений "Termination:":

#!/usr/bin/env bash

trap_with_arg() { # from https://stackoverflow.com/a/2183063/804678
  local func="$1"; shift
  for sig in "$@"; do
    trap "$func $sig" "$sig"
  done
}

stop() {
  trap - SIGINT EXIT
  printf '\n%s\n' "recieved $1, killing children"
  kill -s SIGINT 0
}

trap_with_arg 'stop' EXIT SIGINT SIGTERM SIGHUP

{ i=0; while (( ++i )); do sleep 0.5 && echo "a: $i"; done } &
{ i=0; while (( ++i )); do sleep 0.6 && echo "b: $i"; done } &

while true; do read; done

UPD : добавлен минимальный пример; улучшена stopфункция, позволяющая избежать захвата ненужных сигналов и скрыть сообщения «Ter прекращено:» с выхода. Спасибо Тревору Бойду Смиту за предложения!

skozin
источник
в stop()вас предоставить первый аргумент как номер сигнала , но тогда вы жёстко , что сигналы были сняты с регистрации. вместо жесткого кодирования отменяемых сигналов вы можете использовать первый аргумент для отмены регистрации в stop()функции (это может привести к остановке других рекурсивных сигналов (кроме трех жестко закодированных)).
Тревор Бойд Смит
@TrevorBoydSmith, я думаю, это не сработает. Например, оболочка может быть уничтожена SIGINT, но kill 0отправляет SIGTERM, что снова попадет в ловушку. Это не приведет к бесконечной рекурсии, потому что SIGTERMбудет захвачено во время второго stopвызова.
скозин
Наверное, trap - $1 && kill -s $1 0должно работать лучше. Я проверю и обновлю этот ответ. Спасибо за хорошую идею! :)
скозин
Нет, trap - $1 && kill -s $1 0тоже не получится, потому что мы не можем убить EXIT. Но действительно достаточно сделать de-trap TERM, потому что killпосылает этот сигнал по умолчанию.
скозин
Я проверял рекурсию с EXIT, trapобработчик сигнала всегда выполняется только один раз.
Тревор Бойд Смит
9

Чтобы быть в безопасности, я считаю, что лучше определить функцию очистки и вызвать ее из ловушки:

cleanup() {
        local pids=$(jobs -pr)
        [ -n "$pids" ] && kill $pids
}
trap "cleanup" INT QUIT TERM EXIT [...]

или вообще избегать функции:

trap '[ -n "$(jobs -pr)" ] && kill $(jobs -pr)' INT QUIT TERM EXIT [...]

Зачем? Потому что простое использование trap 'kill $(jobs -pr)' [...]предполагает, что будут выполняться фоновые задания, когда сигнализируется условие прерывания. Когда нет рабочих мест, вы увидите следующее (или похожее) сообщение:

kill: usage: kill [-s sigspec | -n signum | -sigspec] pid | jobspec ... or kill -l [sigspec]

потому что jobs -prпусто - я закончил в этой «ловушке» (каламбур).

tdaitx
источник
Этот тест [ -n "$(jobs -pr)" ]не работает на моем Bash. Я использую GNU bash, версия 4.2.46 (2) -релиз (x86_64-redhat-linux-gnu). Сообщение «kill: использование» продолжает появляться.
Доу ван дер Лист
Я подозреваю, что это связано с тем, что jobs -prне возвращает PID дочерних элементов фоновых процессов. Он не разрушает все дерево процессов, а только обрезает корни.
Доу ван дер Лист
2

Хорошая версия, которая работает под Linux, BSD и MacOS X. Сначала пытается отправить SIGTERM, и, если это не удается, убивает процесс через 10 секунд.

KillJobs() {
    for job in $(jobs -p); do
            kill -s SIGTERM $job > /dev/null 2>&1 || (sleep 10 && kill -9 $job > /dev/null 2>&1 &)

    done
}

TrapQuit() {
    # Whatever you need to clean here
    KillJobs
}

trap TrapQuit EXIT

Обратите внимание, что работа не включает в себя процессы внучат

Орсирис де Йонг
источник
2
function cleanup_func {
    sleep 0.5
    echo cleanup
}

trap "exit \$exit_code" INT TERM
trap "exit_code=\$?; cleanup_func; kill 0" EXIT

# exit 1
# exit 0

Как https://stackoverflow.com/a/22644006/10082476 , но с добавленным кодом выхода

Делавэр
источник
Откуда exit_codeприходят из в INT TERMловушку?
Ярно
1

Другой вариант - установить сценарий в качестве лидера группы процессов и перехватить killpg в вашей группе процессов при выходе.

orip
источник
Как вы устанавливаете процесс в качестве лидера группы процессов? Что такое "killpg"?
Ярно
0

Так что скрипт загрузки скрипта. Запустите killall(или все, что доступно в вашей ОС) команду, которая выполняется, как только сценарий будет завершен.

Oli
источник
0

jobs -p не работает во всех оболочках, если вызывается в под-оболочке, возможно, если его вывод не перенаправлен в файл, но не в канал. (Я предполагаю, что первоначально он был предназначен только для интерактивного использования.)

Как насчет следующего:

trap 'while kill %% 2>/dev/null; do jobs > /dev/null; done' INT TERM EXIT [...]

Вызов «jobs» необходим в dash-оболочке Debian, которая не может обновить текущее задание («%%»), если оно отсутствует.

michaeljt
источник
Хм интересный подход, но, похоже, не работает. Подумайте о scipt trap 'echo in trap; set -x; trap - TERM EXIT; while kill %% 2>/dev/null; do jobs > /dev/null; done; set +x' INT TERM EXIT; sleep 100 & while true; do printf .; sleep 1; doneЕсли вы запустите его в Bash (5.0.3) и попытаетесь завершиться, похоже, существует бесконечный цикл. Однако, если вы прекратите его снова, это работает. Даже с помощью Dash (0.5.10.2-6) вы должны завершить его дважды.
Ярно
0

Я адаптировал ответ @ tokland в сочетании со знаниями из http://veithen.github.io/2014/11/16/sigterm-propagation.html, когда заметил, что trapэто не сработает, если я запускаю процесс переднего плана (без фона &):

#!/bin/bash

# killable-shell.sh: Kills itself and all children (the whole process group) when killed.
# Adapted from http://stackoverflow.com/a/2173421 and http://veithen.github.io/2014/11/16/sigterm-propagation.html
# Note: Does not work (and cannot work) when the shell itself is killed with SIGKILL, for then the trap is not triggered.
trap "trap - SIGTERM && echo 'Caught SIGTERM, sending SIGTERM to process group' && kill -- -$$" SIGINT SIGTERM EXIT

echo $@
"$@" &
PID=$!
wait $PID
trap - SIGINT SIGTERM EXIT
wait $PID

Пример того, как это работает:

$ bash killable-shell.sh sleep 100
sleep 100
^Z
[1]  + 31568 suspended  bash killable-shell.sh sleep 100

$ ps aux | grep "sleep"
niklas   31568  0.0  0.0  19640  1440 pts/18   T    01:30   0:00 bash killable-shell.sh sleep 100
niklas   31569  0.0  0.0  14404   616 pts/18   T    01:30   0:00 sleep 100
niklas   31605  0.0  0.0  18956   936 pts/18   S+   01:30   0:00 grep --color=auto sleep

$ bg
[1]  + 31568 continued  bash killable-shell.sh sleep 100

$ kill 31568
Caught SIGTERM, sending SIGTERM to process group
[1]  + 31568 terminated  bash killable-shell.sh sleep 100

$ ps aux | grep "sleep"
niklas   31717  0.0  0.0  18956   936 pts/18   S+   01:31   0:00 grep --color=auto sleep
NH2
источник
0

Просто для разнообразия я опубликую вариант https://stackoverflow.com/a/2173421/102484 , потому что это решение приводит к сообщению «Прекращено» в моей среде:

trap 'test -z "$intrap" && export intrap=1 && kill -- -$$' SIGINT SIGTERM EXIT
noonex
источник