Como eu mato processos / trabalhos em segundo plano quando meu script de shell sai?

193

Estou procurando uma maneira de limpar a bagunça quando meu script de nível superior terminar.

Especialmente se eu quiser usar set -e, gostaria que o processo em segundo plano morresse quando o script sair.

elmarco
fonte

Respostas:

186

Para limpar alguma bagunça, trappode ser usado. Pode fornecer uma lista de coisas executadas quando um sinal específico chega:

trap "echo hello" SIGINT

mas também pode ser usado para executar algo se o shell sair:

trap "killall background" EXIT

É um builtin, portanto help trap, você fornecerá informações (funciona com o bash). Se você quiser apenas matar trabalhos em segundo plano, poderá fazer

trap 'kill $(jobs -p)' EXIT

Cuidado para usar o single ', para impedir que o shell substitua o $()imediatamente.

Johannes Schaub - litb
fonte
então como você mata todos os filhos ? (ou estou
sentindo
18
killall mata seus filhos, mas você não
orip
3
kill $(jobs -p)não funciona no traço, porque ele executa a substituição de comando em um subshell (ver Command Substituição no homem traço)
user1431317
7
é killall backgroundsuposto ser um espaço reservado? backgroundnão está na página de manual ...
Evan Benn
171

Isso funciona para mim (aprimorado graças aos comentaristas):

trap "trap - SIGTERM && kill -- -$$" SIGINT SIGTERM EXIT
  • kill -- -$$envia um SIGTERM para todo o grupo de processos, matando também descendentes.

  • A especificação do sinal EXITé útil ao usar set -e(mais detalhes aqui ).

tokland
fonte
1
Deve funcionar bem em geral, mas os processos filhos podem alterar os grupos de processos. Por outro lado, não requer controle de trabalho e também pode perder alguns processos netos por outras soluções.
michaeljt
5
Observe que "kill 0" também matará um script bash pai. Você pode usar "kill - - $ BASHPID" para matar apenas os filhos do script atual. Se você não tiver o $ BASHPID na sua versão do bash, poderá exportar o BASHPID = $ (sh -c 'echo $ PPID')
ACyclic
2
Obrigado pela solução agradável e clara! Infelizmente, ele segmenta o Bash 4.3, o que permite a recursão de interceptação. Encontrei isso no 4.3.30(1)-releaseOSX e também está confirmado no Ubuntu . Há uma wokaround obvoius , embora :)
skozin
4
Eu não entendo direito -$$. Ele avalia como '- <PID> `por exemplo -1234. Na página de manual kill // página de manual interna, um traço inicial especifica o sinal a ser enviado. No entanto - provavelmente bloqueia isso, mas o traço principal não está documentado de outra forma. Qualquer ajuda?
Evan Benn
4
@EvanBenn: Check man 2 kill, que explica que quando um PID é negativo, o sinal é enviado para todos os processos no grupo de processos com o ID fornecido ( en.wikipedia.org/wiki/Process_group ). É confuso que isso não seja mencionado em man 1 killou man bashe possa ser considerado um bug na documentação.
User001
111

Atualização: https://stackoverflow.com/a/53714583/302079 aprimora isso adicionando o status de saída e uma função de limpeza.

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

Por que converter INTe TERMsair? Porque ambos devem acionar o kill 0sem inserir um loop infinito.

Por gatilho kill 0on EXIT? Como saídas de script normais devem acionarkill 0 .

Por que kill 0? Porque subcascas aninhadas precisam ser mortas também. Isso derrubará toda a árvore do processo .

korkman
fonte
3
A única solução para o meu caso no Debian.
precisa
3
Nem a resposta de Johannes Schaub nem a resposta do tokland conseguiram matar os processos em segundo plano que meu script de shell iniciou (no Debian). Esta solução funcionou. Não sei por que essa resposta não é mais votada. Você poderia expandir mais sobre o que exatamente kill 0significa / faz?
Josch
7
Isto é incrível, mas também mata a minha shell pai :-(
vidstige
5
Esta solução é literalmente um exagero. kill 0 (dentro do meu script) arruinou toda a minha sessão do X! Talvez em alguns casos o kill 0 possa ser útil, mas isso não altera o fato de que não é uma solução geral e deve ser evitado, se possível, a menos que haja uma boa razão para usá-lo. Seria bom adicionar um aviso de que ele pode matar o shell pai ou mesmo toda a sessão X, não apenas trabalhos em segundo plano de um script!
Lissanro Rayen
3
Embora isso possa ser uma solução interessante em algumas circunstâncias, como apontado por @vidstige, isso matará todo o grupo de processos, que inclui o processo de inicialização (ou seja, o shell pai na maioria dos casos). Definitivamente, não é algo que você deseja quando está executando um script por meio de um IDE.
matpen
22

trap 'kill $ (jobs -p)' EXIT

Eu faria apenas pequenas alterações na resposta de Johannes e usaria jobs -pr para limitar a interrupção dos processos em execução e adicionar mais alguns sinais à lista:

trap 'kill $(jobs -pr)' SIGINT SIGTERM EXIT
traçado de raio
fonte
14

A trap 'kill 0' SIGINT SIGTERM EXITsolução descrita na resposta do @ tokland é muito boa, mas o Bash mais recente falha com uma falha de segmantação ao usá-lo. Isso porque o Bash, a partir da versão 4.3, permite a recursão de interceptação, que se torna infinita neste caso:

  1. processo de shell recebe SIGINTou SIGTERMou EXIT;
  2. o sinal fica preso, em execução kill 0, que envia SIGTERMpara todos os processos do grupo, incluindo o próprio shell;
  3. vá para 1 :)

Para solucionar isso, cancele o registro manual da armadilha:

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

A maneira mais elegante, que permite imprimir o sinal recebido e evita mensagens "Terminated:":

#!/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 : exemplo mínimo adicionado; stopFunção aprimorada para evitar a captura de sinais desnecessários e ocultar as mensagens "Terminadas:" da saída. obrigado Trevor Boyd Smith pelas sugestões!

skozin
fonte
em stop()que você fornecer o primeiro argumento como o número de sinal, mas depois você codificar o que sinais estão sendo registro cancelado. em vez de codificar os sinais que estão sendo cancelados o registro, você pode usar o primeiro argumento para cancelar o registro na stop()função (isso poderia interromper outros sinais recursivos (exceto os 3 codificados permanentemente)).
Trevor Boyd Smith
@TrevorBoydSmith, isso não funcionaria como esperado, eu acho. Por exemplo, o shell pode ser morto com SIGINT, mas kill 0envia SIGTERM, que ficará preso novamente. Isso não produzirá recursão infinita, porém, porque SIGTERMserá retido durante a segunda stopchamada.
Skokin
Provavelmente, trap - $1 && kill -s $1 0deve funcionar melhor. Vou testar e atualizar esta resposta. Obrigado pela boa ideia! :)
skozin 16/02
Não, trap - $1 && kill -s $1 0não funcionaria também, pois não podemos matar EXIT. Mas é realmente suficiente capturar TERM, porque killenvia esse sinal por padrão.
Skokin
Testei a recursão com EXIT, o trapmanipulador de sinal sempre é executado apenas uma vez.
Trevor Boyd Smith
9

Por segurança, acho melhor definir uma função de limpeza e chamá-la de trap:

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

ou evitando a função completamente:

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

Por quê? Porque simplesmente usando trap 'kill $(jobs -pr)' [...]uma parte do princípio de que não vai ser trabalhos de fundo em execução quando a condição armadilha é sinalizado. Quando não há trabalhos, será exibida a seguinte mensagem (ou similar):

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

porque jobs -prestá vazio - terminei nessa 'armadilha' (trocadilhos).

tdaitx
fonte
Este caso de teste [ -n "$(jobs -pr)" ]não funciona no meu bash. Eu uso o GNU bash, versão 4.2.46 (2) -release (x86_64-redhat-linux-gnu). A mensagem "kill: use" continua aparecendo.
Douwe van der Leest 27/05/19
Eu suspeito que isso tenha a ver com o fato de jobs -prnão retornar os PIDs dos filhos dos processos em segundo plano. Não destrói toda a árvore do processo, apenas apara as raízes.
Douwe van der Leest 27/05/19
2

Uma boa versão que funciona com Linux, BSD e MacOS X. Primeiro, tente enviar o SIGTERM e, se não tiver êxito, interrompe o processo após 10 segundos.

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

Observe que os trabalhos não incluem processos de netos.

Orsiris de Jong
fonte
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

Como https://stackoverflow.com/a/22644006/10082476 , mas com código de saída adicionado

Delaware
fonte
1

Outra opção é ter o script definido como o líder do grupo de processos e prender um killpg no seu grupo de processos na saída.

orip
fonte
0

Então escreva o carregamento do script. Execute um killallcomando (ou o que estiver disponível no seu sistema operacional) que é executado assim que o script é concluído.

Oli
fonte
0

jobs -p não funciona em todos os shells se chamado em um sub-shell, possivelmente a menos que sua saída seja redirecionada para um arquivo, mas não para um canal. (Presumo que ele foi originalmente destinado apenas para uso interativo.)

E o que se segue:

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

A chamada para "jobs" é necessária com o shell do Debian, que falha ao atualizar o job atual ("%%") se ele estiver ausente.

michaeljt
fonte
0

Fiz uma adaptação da resposta do @ tokland combinada com o conhecimento de http://veithen.github.io/2014/11/16/sigterm-propagation.html quando percebi que trapisso não é acionado se eu estiver executando um processo em primeiro plano (sem fundo &):

#!/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

Exemplo disso funcionando:

$ 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
fonte
0

Apenas pela diversidade, publicarei variações de https://stackoverflow.com/a/2173421/102484 , porque essa solução leva à mensagem "Terminado" no meu ambiente:

trap 'test -z "$intrap" && export intrap=1 && kill -- -$$' SIGINT SIGTERM EXIT
noonex
fonte