Como matar um processo filho após um determinado tempo limite no Bash?

178

Eu tenho um script bash que inicia um processo filho que trava (na verdade trava) de tempos em tempos e sem motivo aparente (código fechado, para que não haja muito o que fazer). Como resultado, eu gostaria de poder iniciar esse processo por um determinado período de tempo e eliminá-lo se não retornar com êxito após um determinado período de tempo.

Tem alguma simples e robusta de conseguir isso usando o bash?

PS: diga-me se esta pergunta é mais adequada para falha do servidor ou superusuário.

Greg
fonte
Você pode
usar o seguinte comando
Resposta muito completa aqui: stackoverflow.com/a/58873049/2635443
Orsiris de Jong

Respostas:

260

(Como visto em: Entrada no BASH FAQ # 68: "Como executo um comando e o aborto (timeout) após N segundos?" )

Se você não se importa de baixar algo, use timeout( sudo apt-get install timeout) e use-o como: (a maioria dos sistemas já o instalou, caso contrário, use sudo apt-get install coreutils)

timeout 10 ping www.goooooogle.com

Se você não deseja fazer o download de algo, faça o tempo limite internamente:

( cmdpid=$BASHPID; (sleep 10; kill $cmdpid) & exec ping www.goooooogle.com )

Caso deseje fazer um tempo limite para um código bash mais longo, use a segunda opção da seguinte maneira:

( cmdpid=$BASHPID; 
    (sleep 10; kill $cmdpid) \
   & while ! ping -w 1 www.goooooogle.com 
     do 
         echo crap; 
     done )
Ignacio Vazquez-Abrams
fonte
8
Re resposta de Ignacio, caso mais alguém se pergunte o que eu fiz: o cmdpid=$BASHPIDnão aceitará o pid do shell de chamada , mas o (primeiro) subshell iniciado por ele (). O (sleep... coisa chama um segundo subnível no primeiro subnível que esperar 10 segundos em segundo plano e matar o primeiro subnível que, depois de ter lançado o processo de subnível assassino, passa a executar a sua carga de trabalho ...
Jamadagni
17
timeoutfaz parte do GNU coreutils, portanto já deve estar instalado em todos os sistemas GNU.
Sameer
1
@Sameer: ​​Somente a partir da versão 8. #
Ignacio Vazquez-Abrams
3
Não tenho 100% de certeza disso, mas até onde eu sei (e sei o que minha página de manual me disse) timeoutagora faz parte dos coreutils.
benaryorg
5
Este comando não 'termina cedo'. Ele sempre mata o processo no tempo limite - mas não lida com a situação em que não ocorreu o tempo limite.
hawkeye
28
# Spawn a child process:
(dosmth) & pid=$!
# in the background, sleep for 10 secs then kill that process
(sleep 10 && kill -9 $pid) &

ou para obter os códigos de saída também:

# Spawn a child process:
(dosmth) & pid=$!
# in the background, sleep for 10 secs then kill that process
(sleep 10 && kill -9 $pid) & waiter=$!
# wait on our worker process and return the exitcode
exitcode=$(wait $pid && echo $?)
# kill the waiter subshell, if it still runs
kill -9 $waiter 2>/dev/null
# 0 if we killed the waiter, cause that means the process finished before the waiter
finished_gracefully=$?
Dan
fonte
8
Você não deve usar kill -9antes de tentar sinais de que um processo pode processar primeiro.
Pausado até novo aviso.
É verdade, eu estava indo para uma correção rápida e no entanto apenas supor que ele quer que o processo de mortos instantaneamente, porque ele disse que ele trava
Dan
8
Essa é realmente uma solução muito ruim. E se dosmthterminar em 2 segundos, outro processo recebe o antigo pid e você mata o novo?
Teleporting Goat
A reciclagem de PID funciona atingindo o limite e contornando. É muito improvável que outro processo reutilize o PID dentro dos 8 segundos restantes, a menos que o sistema esteja completamente inoperante.
kittydoor
13
sleep 999&
t=$!
sleep 10
kill $t
DigitalRoss
fonte
Isso gera uma espera excessiva. E se um comando real ( sleep 999aqui) frequentemente terminar mais rápido que o sono imposto ( sleep 10)? E se eu quiser dar uma chance de até 1 minuto, 5 minutos? E se eu tiver um monte de tais casos no meu script :)
it3xl
3

Eu também tive essa pergunta e achei mais duas coisas muito úteis:

  1. A variável SECONDS no bash.
  2. O comando "pgrep".

Então, eu uso algo assim na linha de comando (OSX 10.9):

ping www.goooooogle.com & PING_PID=$(pgrep 'ping'); SECONDS=0; while pgrep -q 'ping'; do sleep 0.2; if [ $SECONDS = 10 ]; then kill $PING_PID; fi; done

Como este é um loop, incluí um "sleep 0.2" para manter a CPU fresca. ;-)

(BTW: ping é um mau exemplo de qualquer maneira, você apenas usaria a opção "-t" (timeout) incorporada.)

Ulrich
fonte
1

Supondo que você tenha (ou possa criar facilmente) um arquivo pid para rastrear o pid da criança, você poderá criar um script que verifique o horário do arquivo pid e interrompa / repita o processo conforme necessário. Em seguida, basta colocar o script no crontab para executar aproximadamente no período necessário.

Deixe-me saber se você precisar de mais detalhes. Se isso não parece adequado às suas necessidades, e o iniciante?

kojiro
fonte
1

Uma maneira é executar o programa em um subshell e se comunicar com o subshell através de um pipe nomeado com o read comando Dessa forma, você pode verificar o status de saída do processo em execução e comunicá-lo de volta através do canal.

Aqui está um exemplo de tempo limite do yescomando após 3 segundos. Ele obtém o PID do processo usando pgrep(possivelmente só funciona no Linux). Há também algum problema com o uso de um pipe, pois um processo de abertura de um pipe para leitura será interrompido até que ele também seja aberto para gravação e vice-versa. Portanto, para evitar que o readcomando seja interrompido, "abri" o pipe para leitura com um subshell em segundo plano. (Outra maneira de impedir que um congelamento abra o pipe de leitura e gravação, ou seja read -t 5 <>finished.pipe, isso também pode não funcionar, exceto no Linux.)

rm -f finished.pipe
mkfifo finished.pipe

{ yes >/dev/null; echo finished >finished.pipe ; } &
SUBSHELL=$!

# Get command PID
while : ; do
    PID=$( pgrep -P $SUBSHELL yes )
    test "$PID" = "" || break
    sleep 1
done

# Open pipe for writing
{ exec 4>finished.pipe ; while : ; do sleep 1000; done } &  

read -t 3 FINISHED <finished.pipe

if [ "$FINISHED" = finished ] ; then
  echo 'Subprocess finished'
else
  echo 'Subprocess timed out'
  kill $PID
fi

rm finished.pipe
Gavin Smith
fonte
0

Aqui está uma tentativa que tenta evitar matar um processo depois que ele já saiu, o que reduz a chance de matar outro processo com o mesmo ID de processo (embora seja provavelmente impossível evitar completamente esse tipo de erro).

run_with_timeout ()
{
  t=$1
  shift

  echo "running \"$*\" with timeout $t"

  (
  # first, run process in background
  (exec sh -c "$*") &
  pid=$!
  echo $pid

  # the timeout shell
  (sleep $t ; echo timeout) &
  waiter=$!
  echo $waiter

  # finally, allow process to end naturally
  wait $pid
  echo $?
  ) \
  | (read pid
     read waiter

     if test $waiter != timeout ; then
       read status
     else
       status=timeout
     fi

     # if we timed out, kill the process
     if test $status = timeout ; then
       kill $pid
       exit 99
     else
       # if the program exited normally, kill the waiting shell
       kill $waiter
       exit $status
     fi
  )
}

Use like run_with_timeout 3 sleep 10000, que roda sleep 10000mas termina após 3 segundos.

Isso é como outras respostas que usam um processo de tempo limite em segundo plano para interromper o processo filho após um atraso. Eu acho que isso é quase o mesmo que a resposta estendida de Dan ( https://stackoverflow.com/a/5161274/1351983 ), exceto que o shell de tempo limite não será eliminado se já tiver terminado.

Após a conclusão desse programa, ainda haverá alguns processos de "suspensão" remanescentes em execução, mas eles devem ser inofensivos.

Essa pode ser uma solução melhor do que minha outra resposta, pois não usa o recurso de shell não portátil read -te não pgrep.

Gavin Smith
fonte
Qual é a diferença entre (exec sh -c "$*") &e sh -c "$*" &? Especificamente, por que usar o primeiro em vez do último?
Justin C
0

Aqui está a terceira resposta que enviei aqui. Este lida com interrupções de sinal e limpa processos em segundo plano quando SIGINTé recebido. Ele usa o truque $BASHPIDe execusado na resposta superior para obter o PID de um processo (neste caso, $$em uma shinvocação). Ele usa um FIFO para se comunicar com um subshell que é responsável por matar e limpar. (Isso é como o pipe na minha segunda resposta , mas ter um pipe nomeado significa que o manipulador de sinais também pode escrever nele.)

run_with_timeout ()
{
  t=$1 ; shift

  trap cleanup 2

  F=$$.fifo ; rm -f $F ; mkfifo $F

  # first, run main process in background
  "$@" & pid=$!

  # sleeper process to time out
  ( sh -c "echo \$\$ >$F ; exec sleep $t" ; echo timeout >$F ) &
  read sleeper <$F

  # control shell. read from fifo.
  # final input is "finished".  after that
  # we clean up.  we can get a timeout or a
  # signal first.
  ( exec 0<$F
    while : ; do
      read input
      case $input in
        finished)
          test $sleeper != 0 && kill $sleeper
          rm -f $F
          exit 0
          ;;
        timeout)
          test $pid != 0 && kill $pid
          sleeper=0
          ;;
        signal)
          test $pid != 0 && kill $pid
          ;;
      esac
    done
  ) &

  # wait for process to end
  wait $pid
  status=$?
  echo finished >$F
  return $status
}

cleanup ()
{
  echo signal >$$.fifo
}

Eu tentei evitar as condições da corrida o máximo possível. No entanto, uma fonte de erro que não pude remover é quando o processo termina quase ao mesmo tempo que o tempo limite. Por exemplo, run_with_timeout 2 sleep 2ou run_with_timeout 0 sleep 0. Para mim, este último dá um erro:

timeout.sh: line 250: kill: (23248) - No such process

como ele está tentando matar um processo que já saiu por si só.

Gavin Smith
fonte