O que é uma técnica confiável para matar processos em segundo plano na finalização do script?

5

Eu uso scripts de shell para reagir a eventos do sistema e atualizar exibições de status no meu gerenciador de janelas. Por exemplo, um script determina o status atual do Wi-Fi ouvindo várias fontes:

  1. associar / dissociar eventos de wpa_supplicant
  2. endereço muda de ip (então eu sei quando o dhcpcd atribuiu um endereço)
  3. um processo de timer (então a intensidade do sinal é atualizada de tempos em tempos)

Para alcançar a multiplexação, acabo gerando processos em segundo plano:

{ wpa_cli -p /var/run/wpa_supplicant -i wlan0 -a echo &
ip monitor address &
while sleep 30; do echo; done } |
while read line; do update_wifi_status; done &

ou seja, a configuração é que sempre que qualquer uma das fontes de evento enviar uma linha, meu status do Wi-Fi é atualizado. Todo o pipeline é executado em segundo plano (a final '&') porque também vejo outra origem de evento que faz com que meu script termine:

wait_for_termination
kill $!

O kill deve limpar os processos em segundo plano, mas desta forma não funciona bem. Os processos 'wpa_cli' e 'ip' sempre sobrevivem, pelo menos, e nem morrem em seu próximo evento (em teoria eles devem obter um SIGPIPE; acho que o processo de leitura ainda deve estar ativo também).

A questão é: como confiantemente [e elegantemente!] limpar todos os processos em segundo plano gerados?

    
por sqweek 30.01.2013 / 12:29

3 respostas

7

A solução super simples é adicionar isso no final do script:

kill -- -$$

Explicação:

$$ nos dá o PID do shell em execução. Então, kill $$ enviaria um SIGTERM para o processo de shell. No entanto, se negarmos o PID, kill enviará um SIGTERM para cada processo no grupo de processos . Precisamos do -- antes, então kill sabe que -$$ é um ID do grupo de processos e não um sinalizador.

Observe que isso depende do shell em execução ser líder do grupo de processos! Caso contrário, $$ (o PID) não corresponderá ao ID do grupo de processos e você enviará um sinal para quem sabe onde (bem, provavelmente em nenhum lugar, como é improvável que seja um grupo de processos com uma ID correspondente, se não formos um líder de grupo).

Quando o shell é iniciado, ele cria um novo grupo de processos [1]. Todo processo bifurcado torna-se um membro desse grupo de processos, a menos que eles alterem explicitamente seu grupo de processos por meio de um syscall ( setpgid ).

A maneira mais fácil de garantir um script específico é executada como um líder do grupo de processos, que é iniciado usando setsid . Por exemplo, tenho alguns desses scripts de status que inicio a partir de um script pai:

#!/bin/sh
wifi_status &
bat_status &

Escrito assim, os scripts de Wi-Fi e bateria são executados com o mesmo grupo de processos que o script pai e kill -- -$$ não funciona. A correção é:

#!/bin/sh
setsid wifi_status &
setsid bat_status &

Eu achei pstree -p -g útil para visualizar o processo & IDs de grupos de processos.

Obrigado a todos que contribuíram e me fizeram cavar um pouco mais, aprendi coisas! :)

[1] há outras circunstâncias em que o shell cria um grupo de processos? por exemplo. ao iniciar um subshell? eu não sei ...

    
por 08.03.2013 / 13:08
1

OK, eu encontrei uma solução decente que não usa cgroups. Não funcionará em face dos processos de bifurcação, como Leonardo Dagnino apontou.

Um dos problemas com o acompanhamento manual de IDs de processo via $! matá-los mais tarde é a condição de corrida inerente - se o processo terminar antes de você matá-lo, o script enviará um sinal para um processo inexistente, ou possivelmente incorreto.

Podemos verificar a finalização do processo dentro do shell por meio do wait embutido, mas só podemos aguardar a finalização de todos processos em segundo plano ou um único pid. Em ambos os casos, os blocos aguardam , o que o torna inadequado para a tarefa de verificar se um determinado PID ainda está sendo executado.

Na busca por uma solução para o que foi dito acima, tropecei no comando jobs , que eu pensava estar disponível apenas para shells interativos. Acontece que funciona bem scripts, e automaticamente acompanha os processos em segundo plano que lançamos - se um processo foi encerrado, ele não vai aparecer na lista de empregos mais.

Portanto, o comando:

trap 'kill $(jobs -p)' EXIT

é suficiente para garantir - em casos simples - a finalização de processos em segundo plano quando o shell atual é encerrado.

No meu caso, uma não é suficiente, porque eu estou lançando o processo de segundo plano a partir de um subshell também, e os traps são limpos para cada nova sub-camada. Então, eu preciso fazer a mesma armadilha dentro do subshell:

{ trap 'kill $(jobs -p)' EXIT
wpa_cli -p /var/run/wpa_supplicant -i wlan0 -a echo &
ip monitor address &
while echo; do sleep 30; done } |
while read line; do update_wifi_status; done &

Por fim, jobs -p apenas fornece o pid do último processo no pipeline (assim como $!). Como você pode ver, estou gerando processos em segundo plano no primeiro processo do pipeline em segundo plano, então quero sinalizar esse pid também.

O primeiro processo 'pid pode ser obtido de jobs , mas não tenho certeza de como portabilidade isso pode ser alcançado. Usando bash, recebo saída neste estilo:

$ sleep 20 | sleep 20 &
$ jobs -l
[1]+ 25180 Running                 sleep 20
     25181                       | sleep 20 &

Assim, usando um comando kill modificado do script pai, posso sinalizar todos os processos no pipeline:

wait_for_termination
kill $(jobs -l |awk '$2 == "|" {print $1; next} {print $2}')
    
por 17.02.2013 / 04:39
0

Você pode obter o PID de cada um deles colocando algo como

PID1=$!

após o wpa_cli,

PID2=$!

depois do ip, e no final do script você mata os dois:

kill $PID1
kill $PID2

No entanto, isso não funcionará se os processos se separarem. Então os cgroups seriam a melhor solução.

    
por 30.01.2013 / 16:29