Obter código de saída de um processo em segundo plano


129

Eu tenho um comando chamado CMD do meu script principal do bourne que leva uma eternidade.

Quero modificar o script da seguinte maneira:

  1. Execute o comando CMD em paralelo como um processo em segundo plano ( CMD &).
  2. No script principal, tenha um loop para monitorar o comando gerado a cada poucos segundos. O loop também ecoa algumas mensagens para stdout indicando o progresso do script.
  3. Saia do loop quando o comando gerado terminar.
  4. Capture e relate o código de saída do processo gerado.

Alguém pode me dar dicas para fazer isso?


1
...e o vencedor é?
TrueY 07/06

1
@TrueY .. bob não está logado desde o dia em que fez a pergunta. É improvável que saibamos!
ghoti

Respostas:


126

1: No bash, $!mantém o PID do último processo em segundo plano que foi executado. Isso vai te dizer qual processo monitorar, de qualquer maneira.

4: wait <n>aguarda até que o processo com PID <n>seja concluído (ele será bloqueado até a conclusão do processo, portanto, talvez você não queira chamá-lo até ter certeza de que o processo está concluído) e, em seguida, retornará o código de saída do processo concluído.

2, 3: psou ps | grep " $! "pode dizer se o processo ainda está em execução. Depende de você entender o resultado e decidir o quão próximo ele está do acabamento. ( ps | grepnão é à prova de idiotas. Se você tiver tempo, poderá encontrar uma maneira mais robusta de saber se o processo ainda está em execução).

Aqui está um script esqueleto:

# simulate a long process that will have an identifiable exit code
(sleep 15 ; /bin/false) &
my_pid=$!

while   ps | grep " $my_pid "     # might also need  | grep -v grep  here
do
    echo $my_pid is still in the ps output. Must still be running.
    sleep 3
done

echo Oh, it looks like the process is done.
wait $my_pid
# The variable $? always holds the exit code of the last command to finish.
# Here it holds the exit code of $my_pid, since wait exits with that code. 
my_status=$?
echo The exit status of the process was $my_status

15
ps -p $my_pid -o pid=nem grepé necessário.
Pausado até novo aviso.

54
kill -0 $!é a melhor maneira de saber se um processo ainda está em execução. Na verdade, ele não envia nenhum sinal, apenas verifica se o processo está ativo, usando um shell embutido em vez de processos externos. Como man 2 killdiz: "Se sig for 0, nenhum sinal será enviado, mas a verificação de erros ainda será executada; isso pode ser usado para verificar a existência de um ID do processo ou do grupo de processos".
ephemient

14
@ephemient kill -0retornará diferente de zero se você não tiver permissão para enviar sinais para um processo em execução. Infelizmente, ele retorna 1nesse caso e no caso em que o processo não existe. Isso o torna útil, a menos que você não seja o proprietário do processo - o que pode ser o caso mesmo dos processos criados por você, se uma ferramenta sudoestiver envolvida ou se eles estiverem configurados (e possivelmente descartarem privs).
Craig Ringer

13
waitnão retorna o código de saída na variável $?. Ele apenas retorna o código de saída e $?é o código de saída do último programa em primeiro plano.
precisa

7
Para muitas pessoas votando no kill -0. Aqui está uma referência revisada por pares da SO que mostra que o comentário de CraigRinger é legítimo em relação a: kill -0retornará diferente de zero para processos em execução ... mas ps -psempre retornará 0 para qualquer processo em execução .
Trevor Boyd Smith

57

Foi assim que resolvi quando tive uma necessidade semelhante:

# Some function that takes a long time to process
longprocess() {
        # Sleep up to 14 seconds
        sleep $((RANDOM % 15))
        # Randomly exit with 0 or 1
        exit $((RANDOM % 2))
}

pids=""
# Run five concurrent processes
for i in {1..5}; do
        ( longprocess ) &
        # store PID of process
        pids+=" $!"
done

# Wait for all processes to finish, will take max 14s
# as it waits in order of launch, not order of finishing
for p in $pids; do
        if wait $p; then
                echo "Process $p success"
        else
                echo "Process $p fail"
        fi
done

Eu gosto dessa abordagem.
Kemin Zhou

Obrigado! Esta me parece a abordagem mais simples.
Luke Davis

4
Esta solução não atende ao requisito nº 2: um loop de monitoramento por processo em segundo plano. waits faz com que o script aguarde até o final de (cada) processo.
DKroot

Abordagem simples e agradável .. já procuramos esta solução há algum tempo ..
Santosh Kumar Arjunan

Isso não funciona .. ou não faz o que você deseja: ele não verifica os status de saída dos processos em segundo plano?
conny

10

O pid de um processo filho em segundo plano é armazenado em $! . Você pode armazenar todos os pids dos processos filhos em uma matriz, por exemplo, PIDS [] .

wait [-n] [jobspec or pid …]

Aguarde até que o processo filho especificado por cada ID de processo pid ou especificação de tarefa jobspec saia e retorne o status de saída do último comando esperado. Se uma especificação de trabalho for fornecida, todos os processos no trabalho serão aguardados. Se nenhum argumento for fornecido, todos os processos filhos ativos no momento serão aguardados e o status de retorno será zero. Se a opção -n for fornecida, aguarde o término de qualquer trabalho e retorne seu status de saída. Se nem jobspec nem pid especificarem um processo filho ativo do shell, o status de retorno será 127.

Use o comando wait, você pode esperar pelo término de todos os processos filhos, enquanto isso, você pode obter o status de saída de cada processo filho via $? e armazene o status no STATUS [] . Então você pode fazer algo dependendo do status.

Eu tentei as 2 soluções a seguir e elas funcionam bem. solution01 é mais conciso, enquanto solution02 é um pouco complicado.

solution01

#!/bin/bash

# start 3 child processes concurrently, and store each pid into array PIDS[].
process=(a.sh b.sh c.sh)
for app in ${process[@]}; do
  ./${app} &
  PIDS+=($!)
done

# wait for all processes to finish, and store each process's exit code into array STATUS[].
for pid in ${PIDS[@]}; do
  echo "pid=${pid}"
  wait ${pid}
  STATUS+=($?)
done

# after all processed finish, check their exit codes in STATUS[].
i=0
for st in ${STATUS[@]}; do
  if [[ ${st} -ne 0 ]]; then
    echo "$i failed"
  else
    echo "$i finish"
  fi
  ((i+=1))
done

solution02

#!/bin/bash

# start 3 child processes concurrently, and store each pid into array PIDS[].
i=0
process=(a.sh b.sh c.sh)
for app in ${process[@]}; do
  ./${app} &
  pid=$!
  PIDS[$i]=${pid}
  ((i+=1))
done

# wait for all processes to finish, and store each process's exit code into array STATUS[].
i=0
for pid in ${PIDS[@]}; do
  echo "pid=${pid}"
  wait ${pid}
  STATUS[$i]=$?
  ((i+=1))
done

# after all processed finish, check their exit codes in STATUS[].
i=0
for st in ${STATUS[@]}; do
  if [[ ${st} -ne 0 ]]; then
    echo "$i failed"
  else
    echo "$i finish"
  fi
  ((i+=1))
done

Eu tentei e provei que funciona bem. Você pode ler minha explicação no código.
Terry

Leia " Como escrevo uma boa resposta? ", Onde você encontrará as seguintes informações: ... tente mencionar quaisquer limitações, suposições ou simplificações em sua resposta. A brevidade é aceitável, mas explicações mais completas são melhores. Sua resposta é, portanto, aceitável, mas você tem chances muito melhores de obter votos se puder elaborar sobre o problema e sua solução. :-)
Noel Widmer

1
pid=$!; PIDS[$i]=${pid}; ((i+=1))pode ser escrito de maneira mais simples, como o PIDS+=($!)que simplesmente se anexa à matriz sem precisar usar uma variável separada para indexação ou o próprio pid. O mesmo se aplica à STATUSmatriz.
codeforester 5/05

1
@ codeforester, obrigado por sua sugestão, modifiquei meu código inicial em solution01, parece mais conciso.
Terry

O mesmo se aplica a outros lugares onde você está adicionando coisas a uma matriz.
codeforester

8

Como vejo quase todas as respostas, use utilitários externos (principalmente ps) para pesquisar o estado do processo em segundo plano. Existe uma solução mais unixesh, captando o sinal SIGCHLD. No manipulador de sinal, é necessário verificar qual processo filho foi parado. Isso pode ser feito kill -0 <PID>embutido (universal) ou verificando a existência de /proc/<PID>diretório (específico do Linux) ou usando o jobsembutido (específico. jobs -ltambém relata o pid. Nesse caso, o terceiro campo da saída pode ser Parado | Em execução | Concluído | Sair. )

Aqui está o meu exemplo.

O processo iniciado é chamado loop.sh. Ele aceita -xou um número como argumento. For -xé saídas com o código de saída 1. Para um número, espera num * 5 segundos. A cada 5 segundos, ele imprime seu PID.

O processo do iniciador é chamado launch.sh:

#!/bin/bash

handle_chld() {
    local tmp=()
    for((i=0;i<${#pids[@]};++i)); do
        if [ ! -d /proc/${pids[i]} ]; then
            wait ${pids[i]}
            echo "Stopped ${pids[i]}; exit code: $?"
        else tmp+=(${pids[i]})
        fi
    done
    pids=(${tmp[@]})
}

set -o monitor
trap "handle_chld" CHLD

# Start background processes
./loop.sh 3 &
pids+=($!)
./loop.sh 2 &
pids+=($!)
./loop.sh -x &
pids+=($!)

# Wait until all background processes are stopped
while [ ${#pids[@]} -gt 0 ]; do echo "WAITING FOR: ${pids[@]}"; sleep 2; done
echo STOPPED

Para obter mais explicações, consulte: Falha ao iniciar um processo a partir do script bash


1
Como estamos falando do Bash, o loop for poderia ter sido escrito como: for i in ${!pids[@]};usando a expansão de parâmetros.
PlasmaBinturong

7
#/bin/bash

#pgm to monitor
tail -f /var/log/messages >> /tmp/log&
# background cmd pid
pid=$!
# loop to monitor running background cmd
while :
do
    ps ax | grep $pid | grep -v grep
    ret=$?
    if test "$ret" != "0"
    then
        echo "Monitored pid ended"
        break
    fi
    sleep 5

done

wait $pid
echo $?

2
Aqui está um truque para evitar o grep -v. Você pode limitar a pesquisa ao início da linha: grep '^'$pidalém disso, você pode fazer isso de ps p $pid -o pid=qualquer maneira. Além disso, tail -fnão vai terminar até você matá-lo, então não acho que seja uma maneira muito boa de demonstrar isso (pelo menos sem apontar isso). Você pode redirecionar a saída do seu pscomando para /dev/nullou ela será exibida na tela a cada iteração. Suas exitcausas waitsão ignoradas - provavelmente deve ser break. Mas não são os while/ pse os waitredundantes?
Pausado até novo aviso.

5
Por que todo mundo esquece kill -0 $pid? Na verdade, ele não envia nenhum sinal, apenas verifica se o processo está ativo, usando um shell embutido em vez de processos externos.
ephemient

3
Porque você só pode matar um processo que você possui:bash: kill: (1) - Operation not permitted
errant.info

2
O loop é redundante. Apenas espere. Menos código => menos casos extremos.
Brais Gabin

@Brais Gabin O circuito de monitorização é requisito # 2 da pergunta
DKroot

5

Eu mudaria sua abordagem um pouco. Em vez de verificar a cada poucos segundos se o comando ainda está ativo e relatar uma mensagem, tenha outro processo que relate a cada poucos segundos que o comando ainda está sendo executado e depois elimine esse processo quando o comando terminar. Por exemplo:

#! / bin / sh

cmd () {sono 5; saída 24; }

cmd & # Executa o processo de execução longa
pid = $! # Grave o pid

# Gerar um processo que reporte continuamente que o comando ainda está sendo executado
while echo "$ (date): $ pid ainda está em execução"; dorme 1; feito &
eco = $!

# Defina uma armadilha para matar o repórter quando o processo terminar
armadilha 'kill $ echoer' 0

# Aguarde o processo terminar
se esperar $ pid; então
    eco "cmd com êxito"
outro
    eco "cmd FAILED !! (retornou $?)"
fi

ótimo modelo, obrigado por compartilhar! Eu acredito que em vez de armadilha, nós também podemos fazer while kill -0 $pid 2> /dev/null; do X; done, espero que seja útil para alguém no futuro que lê esta mensagem;)
punkbit

3

Nossa equipe tinha a mesma necessidade de um script remoto executado por SSH que atingia o tempo limite após 25 minutos de inatividade. Aqui está uma solução com o loop de monitoramento verificando o processo em segundo plano a cada segundo, mas imprimindo apenas a cada 10 minutos para suprimir um tempo limite de inatividade.

long_running.sh & 
pid=$!

# Wait on a background job completion. Query status every 10 minutes.
declare -i elapsed=0
# `ps -p ${pid}` works on macOS and CentOS. On both OSes `ps ${pid}` works as well.
while ps -p ${pid} >/dev/null; do
  sleep 1
  if ((++elapsed % 600 == 0)); then
    echo "Waiting for the completion of the main script. $((elapsed / 60))m and counting ..."
  fi
done

# Return the exit code of the terminated background process. This works in Bash 4.4 despite what Bash docs say:
# "If neither jobspec nor pid specifies an active child process of the shell, the return status is 127."
wait ${pid}

2

Um exemplo simples, semelhante às soluções acima. Isso não requer o monitoramento de nenhuma saída do processo. O próximo exemplo usa cauda para seguir a saída.

$ echo '#!/bin/bash' > tmp.sh
$ echo 'sleep 30; exit 5' >> tmp.sh
$ chmod +x tmp.sh
$ ./tmp.sh &
[1] 7454
$ pid=$!
$ wait $pid
[1]+  Exit 5                  ./tmp.sh
$ echo $?
5

Use tail para seguir a saída do processo e saia quando o processo estiver concluído.

$ echo '#!/bin/bash' > tmp.sh
$ echo 'i=0; while let "$i < 10"; do sleep 5; echo "$i"; let i=$i+1; done; exit 5;' >> tmp.sh
$ chmod +x tmp.sh
$ ./tmp.sh
0
1
2
^C
$ ./tmp.sh > /tmp/tmp.log 2>&1 &
[1] 7673
$ pid=$!
$ tail -f --pid $pid /tmp/tmp.log
0
1
2
3
4
5
6
7
8
9
[1]+  Exit 5                  ./tmp.sh > /tmp/tmp.log 2>&1
$ wait $pid
$ echo $?
5

1

Outra solução é monitorar processos através do sistema de arquivos proc (mais seguro que o ps / grep combo); quando você inicia um processo, ele tem uma pasta correspondente em / proc / $ pid, para que a solução possa ser

#!/bin/bash
....
doSomething &
local pid=$!
while [ -d /proc/$pid ]; do # While directory exists, the process is running
    doSomethingElse
    ....
else # when directory is removed from /proc, process has ended
    wait $pid
    local exit_status=$?
done
....

Agora você pode usar a variável $ exit_status como quiser.


Não funciona no bash? Syntax error: "else" unexpected (expecting "done")
benjaoming 10/02

1

Com esse método, seu script não precisa aguardar o processo em segundo plano, você precisará monitorar apenas um arquivo temporário para o status de saída.

FUNCmyCmd() { sleep 3;return 6; };

export retFile=$(mktemp); 
FUNCexecAndWait() { FUNCmyCmd;echo $? >$retFile; }; 
FUNCexecAndWait&

agora, seu script pode fazer qualquer outra coisa enquanto você apenas precisa monitorar o conteúdo do retFile (ele também pode conter qualquer outra informação que você desejar, como o horário de saída).

PS .: btw, eu codifiquei o pensamento no bash


0

Isso pode estar além da sua pergunta, no entanto, se você estiver preocupado com o tempo de execução dos processos, talvez esteja interessado em verificar o status da execução de processos em segundo plano após um intervalo de tempo. É fácil verificar quais PIDs filhos ainda estão sendo executados pgrep -P $$, no entanto, eu vim com a seguinte solução para verificar o status de saída desses PIDs que já expiraram:

cmd1() { sleep 5; exit 24; }
cmd2() { sleep 10; exit 0; }

pids=()
cmd1 & pids+=("$!")
cmd2 & pids+=("$!")

lasttimeout=0
for timeout in 2 7 11; do
  echo -n "interval-$timeout: "
  sleep $((timeout-lasttimeout))

  # you can only wait on a pid once
  remainingpids=()
  for pid in ${pids[*]}; do
     if ! ps -p $pid >/dev/null ; then
        wait $pid
        echo -n "pid-$pid:exited($?); "
     else
        echo -n "pid-$pid:running; "
        remainingpids+=("$pid")
     fi
  done
  pids=( ${remainingpids[*]} )

  lasttimeout=$timeout
  echo
done

quais saídas:

interval-2: pid-28083:running; pid-28084:running; 
interval-7: pid-28083:exited(24); pid-28084:running; 
interval-11: pid-28084:exited(0); 

Nota: Você pode mudar $pidspara uma variável de sequência em vez de matriz para simplificar as coisas, se quiser.


0

Minha solução foi usar um canal anônimo para passar o status para um loop de monitoramento. Não há arquivos temporários usados ​​para trocar status, nada para limpar. Se você não tiver certeza sobre o número de trabalhos em segundo plano, pode ser a condição de interrupção [ -z "$(jobs -p)" ].

#!/bin/bash

exec 3<> <(:)

{ sleep 15 ; echo "sleep/exit $?" >&3 ; } &

while read -u 3 -t 1 -r STAT CODE || STAT="timeout" ; do
    echo "stat: ${STAT}; code: ${CODE}"
    if [ "${STAT}" = "sleep/exit" ] ; then
        break
    fi
done
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.