Colete códigos de saída de processos paralelos em segundo plano (subcascas)

18

Digamos que tenhamos um script bash assim:

echo "x" &
echo "y" &
echo "z" &
.....
echo "Z" &
wait

existe uma maneira de coletar os códigos de saída dos subcascas / subprocessos? Procurando uma maneira de fazer isso e não consegue encontrar nada. Eu preciso executar esses subshells em paralelo, caso contrário, isso seria mais fácil.

Estou procurando uma solução genérica (tenho um número desconhecido / dinâmico de subprocessos para executar em paralelo).

Alexander Mills
fonte
1
Sugiro que você descubra o que deseja e faça uma nova pergunta, tentando esclarecer exatamente o comportamento que está procurando (talvez com pseudocódigo ou exemplo maior).
Michael Homer
3
Na verdade, acho que a pergunta está boa agora - tenho um número dinâmico de subprocessos. Eu preciso coletar todos os códigos de saída. Isso é tudo.
Alexander Mills

Respostas:

6

A resposta de Alexander Mills, que usa handleJobs, me deu um ótimo ponto de partida, mas também me deu esse erro

aviso: run_pending_traps: valor inválido em trap_list [17]: 0x461010

O que pode ser um problema básico de condição de corrida

Em vez disso, apenas armazenei o pid de cada criança e esperei e obtive o código de saída para cada criança especificamente. Acho esse limpador em termos de subprocessos gerando subprocessos em funções e evitando o risco de esperar por um processo pai no qual pretendia esperar pelo filho. É mais claro o que acontece porque não está usando a armadilha.

#!/usr/bin/env bash

# it seems it does not work well if using echo for function return value, and calling inside $() (is a subprocess spawned?) 
function wait_and_get_exit_codes() {
    children=("$@")
    EXIT_CODE=0
    for job in "${children[@]}"; do
       echo "PID => ${job}"
       CODE=0;
       wait ${job} || CODE=$?
       if [[ "${CODE}" != "0" ]]; then
           echo "At least one test failed with exit code => ${CODE}" ;
           EXIT_CODE=1;
       fi
   done
}

DIRN=$(dirname "$0");

commands=(
    "{ echo 'a'; exit 1; }"
    "{ echo 'b'; exit 0; }"
    "{ echo 'c'; exit 2; }"
    )

clen=`expr "${#commands[@]}" - 1` # get length of commands - 1

children_pids=()
for i in `seq 0 "$clen"`; do
    (echo "${commands[$i]}" | bash) &   # run the command via bash in subshell
    children_pids+=("$!")
    echo "$i ith command has been issued as a background job"
done
# wait; # wait for all subshells to finish - its still valid to wait for all jobs to finish, before processing any exit-codes if we wanted to
#EXIT_CODE=0;  # exit code of overall script
wait_and_get_exit_codes "${children_pids[@]}"

echo "EXIT_CODE => $EXIT_CODE"
exit "$EXIT_CODE"
# end
Arberg
fonte
legal, eu acho que for job in "${childen[@]}"; dodeve ser for job in "${1}"; do, porém, para maior clareza
Alexander Mills
a única preocupação que tenho com esse script é se children_pids+=("$!")realmente está capturando o pid desejado para o sub shell.
Alexander Mills
1
Eu testei com "$ {1}" e não funciona. Estou passando um array para a função e, aparentemente, isso precisa de atenção especial no bash. $! é o pid do último trabalho gerado, consulte tldp.org/LDP/abs/html/internalvariables.html Parece funcionar corretamente nos meus testes, e agora estou usando o script cache_dirs no unRAID, e parece que seu trabalho. Estou usando o bash 4.4.12.
Arberg
agradável sim parece que você está correto
Alexander Mills
20

Use waitcom um PID, que irá:

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.

Você precisará salvar o PID de cada processo à medida que avança:

echo "x" & X=$!
echo "y" & Y=$!
echo "z" & Z=$!

Você também pode ativar o controle de tarefas no script set -me usar uma %nespecificação de tarefas, mas quase certamente não deseja - o controle de tarefas tem muitos outros efeitos colaterais .

waitretornará o mesmo código que o processo finalizado. Você pode usar wait $Xa qualquer momento (razoável) posteriormente para acessar o código final $?ou simplesmente usá-lo como verdadeiro / falso:

echo "x" & X=$!
echo "y" & Y=$!
...
wait $X
echo "job X returned $?"

wait fará uma pausa até que o comando seja concluído, se ainda não o tiver feito.

Se você quiser evitar parar assim, você pode definir um trapemSIGCHLD , contar o número de terminações, e lidar com todos os waits de uma só vez quando temos tudo acabado. Você provavelmente pode usar o uso waitsozinho quase o tempo todo.

Michael Homer
fonte
1
ughh, desculpe, eu preciso executar esses subshells em paralelo, vou especificar que na questão ...
Alexander Mills
deixa pra lá, talvez isso funcione com a minha configuração ... onde o comando wait entra em ação no seu código? Eu não sigo #
Alexander Mills
1
@AlexanderMills Eles estão funcionando paralelamente. Se você tiver um número variável, use uma matriz. (como por exemplo, aqui, que pode ser uma duplicata).
Michael Homer
sim graças eu vou verificar isso, se pertence o comando de espera para a sua resposta, então por favor adicioná-lo
Alexander Mills
Você roda wait $Xem qualquer ponto posterior (razoável).
Michael Homer
5

Se você tiver uma boa maneira de identificar os comandos, poderá imprimir o código de saída em um arquivo tmp e acessar o arquivo específico em que está interessado:

#!/bin/bash

for i in `seq 1 5`; do
    ( sleep $i ; echo $? > /tmp/cmd__${i} ) &
done

wait

for i in `seq 1 5`; do # or even /tmp/cmd__*
    echo "process $i:"
    cat /tmp/cmd__${i}
done

Não se esqueça de remover os arquivos tmp.

Rolf
fonte
4

Use a compound command- coloque a declaração entre parênteses:

( echo "x" ; echo X: $? ) &
( true ; echo TRUE: $? ) &
( false ; echo FALSE: $? ) &

dará a saída

x
X: 0
TRUE: 0
FALSE: 1

Uma maneira realmente diferente de executar vários comandos em paralelo é usando o GNU Parallel . Faça uma lista de comandos para executar e coloque-os no arquivo list:

cat > list
sleep 2 ; exit 7
sleep 3 ; exit 55
^D

Execute todos os comandos em paralelo e colete os códigos de saída no arquivo job.log:

cat list | parallel -j0 --joblog job.log
cat job.log

e a saída é:

Seq     Host    Starttime       JobRuntime      Send    Receive Exitval Signal  Command
1       :       1486892487.325       1.976      0       0       7       0       sleep 2 ; exit 7
2       :       1486892487.326       3.003      0       0       55      0       sleep 3 ; exit 55
hschou
fonte
ok obrigado, existe uma maneira de gerar isso? Eu não tenho apenas três subprocessos, tenho Zprocessos.
Alexander Mills
Eu atualizei a pergunta original para refletir que eu estou procurando uma solução genérica, graças
Alexander Mills
Uma maneira de gerar isso pode ser usar uma construção de loop?
Alexander Mills
Looping? Você tem uma lista fixa de comandos ou é controlada pelo usuário? Não sei se entendi o que você está tentando fazer, mas talvez PIPESTATUSseja algo que você deva conferir. Isso seq 10 | gzip -c > seq.gz ; echo ${PIPESTATUS[@]}retorna 0 0(código de saída do primeiro e do último comando).
Hschou #
Sim, controlado essencialmente pelo usuário
Alexander Mills
2

esse é o script genérico que você está procurando. A única desvantagem é que seus comandos estão entre aspas, o que significa que o destaque da sintaxe via IDE não funcionará realmente. Caso contrário, tentei algumas das outras respostas e essa é a melhor. Esta resposta incorpora a idéia de usar wait <pid>dada por @Michael, mas vai um passo além usando o trapcomando que parece funcionar melhor.

#!/usr/bin/env bash

set -m # allow for job control
EXIT_CODE=0;  # exit code of overall script

function handleJobs() {
     for job in `jobs -p`; do
         echo "PID => ${job}"
         CODE=0;
         wait ${job} || CODE=$?
         if [[ "${CODE}" != "0" ]]; then
         echo "At least one test failed with exit code => ${CODE}" ;
         EXIT_CODE=1;
         fi
     done
}

trap 'handleJobs' CHLD  # trap command is the key part
DIRN=$(dirname "$0");

commands=(
    "{ echo 'a'; exit 1; }"
    "{ echo 'b'; exit 0; }"
    "{ echo 'c'; exit 2; }"
)

clen=`expr "${#commands[@]}" - 1` # get length of commands - 1

for i in `seq 0 "$clen"`; do
    (echo "${commands[$i]}" | bash) &   # run the command via bash in subshell
    echo "$i ith command has been issued as a background job"
done

wait; # wait for all subshells to finish

echo "EXIT_CODE => $EXIT_CODE"
exit "$EXIT_CODE"
# end

obrigado a @michael homer por me colocar no caminho certo, mas usar o trapcomando é a melhor abordagem para o AFAICT.

Alexander Mills
fonte
1
Você também pode usar a interceptação SIGCHLD para processar os filhos quando eles saírem, como imprimir o status naquele momento. Ou atualizar um contador de progresso: declarar uma função, em seguida, usar "armadilha function_name CHLD" embora isso também pode exigir uma opção para ser ligado em um shell não interativo, como possivelmente "-m set"
Chunko
1
Além disso, "wait -n" aguardará qualquer filho e retornará o status de saída desse filho no $? variável. Assim, você pode imprimir o progresso à medida que cada um sai. No entanto, observe que, a menos que você use a armadilha do CHLD, você pode perder algumas crianças que saem dessa maneira.
Chunko
@Chunko thanks! essa é uma boa informação. Você poderia atualizar a resposta com algo que considera melhor?
Alexander Mills
obrigado @Chunko, a armadilha funciona melhor, você está certo. Com a espera <pid>, eu fui avisado.
Alexander Mills
Você pode explicar como e por que você acredita que a versão com a armadilha é melhor do que a versão sem ela? (Eu acredito que há melhor e, portanto, que é pior, porque é mais complexo com nenhum benefício.)
Scott
1

Outra variação da resposta de @rolf:

Outra maneira de salvar o status de saída seria algo como

mkdir /tmp/status_dir

e depois tem cada script

script_name="${0##*/}"  ## strip path from script name
tmpfile="/tmp/status_dir/${script_name}.$$"
do something
rc=$?
echo "$rc" > "$tmpfile"

Isso fornece um nome exclusivo para cada arquivo de status, incluindo o nome do script que o criou e seu ID do processo (caso mais de uma instância do mesmo script esteja em execução), que você pode salvar para referência posteriormente e colocá-los todos no diretório no mesmo local para excluir o subdiretório inteiro quando terminar.

Você pode até salvar mais de um status de cada script fazendo algo como

tmpfile="$(/bin/mktemp -q "/tmp/status_dir/${script_name}.$$.XXXXXX")"

que cria o arquivo como antes, mas adiciona uma sequência aleatória exclusiva a ele.

Ou você pode simplesmente acrescentar mais informações de status ao mesmo arquivo.

Joe
fonte
1

script3será executado somente se script1e script2são bem sucedidos e script1e script2será executado em paralelo:

./script1 &
process1=$!

./script2 &
process2=$!

wait $process1
rc1=$?

wait $process2
rc2=$?

if [[ $rc1 -eq 0 ]] && [[ $rc2 -eq 0  ]];then
./script3
fi
Venkata
fonte
AFAICT, isso nada mais é do que uma reformulação da resposta de Michael Homer .
Scott