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).


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

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.


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.


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

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.


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.


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

AFAICT, isso nada mais é do que uma reformulação da resposta de Michael Homer .
— Scott
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.