bash - programas - sentencias de control en shell script
¿Cómo esperar en bash varios subprocesos para finalizar y devolver el código de salida!=0 cuando cualquier subproceso termina con el código!=0? (27)
¿Cómo esperar en un script de bash varios subprocesos generados a partir de ese script para finalizar y devolver el código de salida! = 0 cuando cualquiera de los subprocesos termina con el código! = 0?
Guión simple:
#!/bin/bash
for i in `seq 0 9`; do
doCalculations $i &
done
wait
La secuencia de comandos anterior esperará a los 10 subprocesos generados, pero siempre dará el estado de salida 0 (ver help wait
). ¿Cómo puedo modificar este script para que descubra los estados de salida de los subprocesos generados y devuelva el código de salida 1 cuando cualquiera de los subprocesos termina con el código! = 0?
¿Hay alguna solución mejor para eso que recolectar los PID de los subprocesos, esperarlos en orden y sumar los estados de salida?
¿Qué tal simplemente?
#!/bin/bash
pids=""
for i in `seq 0 9`; do
doCalculations $i &
pids="$pids $!"
done
wait $pids
...code continued here ...
Actualizar:
Como lo señalaron varios comentaristas, lo anterior espera a que se completen todos los procesos antes de continuar, pero no sale y falla si uno de ellos falla, se puede hacer que se haga con la siguiente modificación sugerida por @Bryan, @SamBrightman y otros :
#!/bin/bash
pids=""
RESULT=0
for i in `seq 0 9`; do
doCalculations $i &
pids="$pids $!"
done
for pid in $pids; do
wait $pid || let "RESULT=1"
done
if [ "$RESULT" == "1" ];
then
exit 1
fi
...code continued here ...
Acabo de modificar un script para poner en segundo plano y paralelizar un proceso.
Hice algunos experimentos (en Solaris con bash y ksh) y descubrí que ''esperar'' genera el estado de salida si no es cero, o una lista de trabajos que devuelven una salida distinta de cero cuando no se proporciona un argumento PID. P.ej
Golpetazo:
$ sleep 20 && exit 1 &
$ sleep 10 && exit 2 &
$ wait
[1]- Exit 2 sleep 20 && exit 2
[2]+ Exit 1 sleep 10 && exit 1
Ksh:
$ sleep 20 && exit 1 &
$ sleep 10 && exit 2 &
$ wait
[1]+ Done(2) sleep 20 && exit 2
[2]+ Done(1) sleep 10 && exit 1
Esta salida se escribe en stderr, por lo que una solución simple para el ejemplo de OP podría ser:
#!/bin/bash
trap "rm -f /tmp/x.$$" EXIT
for i in `seq 0 9`; do
doCalculations $i &
done
wait 2> /tmp/x.$$
if [ `wc -l /tmp/x.$$` -gt 0 ] ; then
exit 1
fi
Mientras esto:
wait 2> >(wc -l)
También devolverá un conteo pero sin el archivo tmp. Esto también podría ser usado de esta manera, por ejemplo:
wait 2> >(if [ `wc -l` -gt 0 ] ; then echo "ERROR"; fi)
Pero esto no es mucho más útil que el archivo tmp IMO. No pude encontrar una manera útil de evitar el archivo tmp y, al mismo tiempo, evitar ejecutar la "espera" en un subshell, que no funciona en absoluto.
Aquí está el ejemplo simple usando la wait
.
Ejecutar algunos procesos:
$ sleep 10 &
$ sleep 10 &
$ sleep 20 &
$ sleep 20 &
Luego espere con el comando de wait
:
$ wait < <(jobs -p)
O simplemente wait
(sin argumentos) a todos.
Esto esperará a que se completen todos los trabajos en segundo plano.
Si se proporciona la opción -n
, espera a que finalice el siguiente trabajo y devuelve su estado de salida.
Ver: help wait
y help jobs
para la sintaxis.
Sin embargo, el inconveniente es que esto devolverá solo el estado de la última ID, por lo que debe verificar el estado de cada subproceso y almacenarlo en la variable.
O haga su función de cálculo para crear algún archivo en caso de falla (vacío o con registro de falla), luego verifique ese archivo si existe, por ejemplo
$ sleep 20 && true || tee fail &
$ sleep 20 && false || tee fail &
$ wait < <(jobs -p)
$ test -f fail && echo Calculation failed.
Aquí está mi versión que funciona para varios pids, registra advertencias si la ejecución demora demasiado y detiene los subprocesos si la ejecución demora más que un valor determinado.
function WaitForTaskCompletion {
local pids="${1}" # pids to wait for, separated by semi-colon
local soft_max_time="${2}" # If execution takes longer than $soft_max_time seconds, will log a warning, unless $soft_max_time equals 0.
local hard_max_time="${3}" # If execution takes longer than $hard_max_time seconds, will stop execution, unless $hard_max_time equals 0.
local caller_name="${4}" # Who called this function
local exit_on_error="${5:-false}" # Should the function exit program on subprocess errors
Logger "${FUNCNAME[0]} called by [$caller_name]."
local soft_alert=0 # Does a soft alert need to be triggered, if yes, send an alert once
local log_ttime=0 # local time instance for comparaison
local seconds_begin=$SECONDS # Seconds since the beginning of the script
local exec_time=0 # Seconds since the beginning of this function
local retval=0 # return value of monitored pid process
local errorcount=0 # Number of pids that finished with errors
local pidCount # number of given pids
IFS='';'' read -a pidsArray <<< "$pids"
pidCount=${#pidsArray[@]}
while [ ${#pidsArray[@]} -gt 0 ]; do
newPidsArray=()
for pid in "${pidsArray[@]}"; do
if kill -0 $pid > /dev/null 2>&1; then
newPidsArray+=($pid)
else
wait $pid
result=$?
if [ $result -ne 0 ]; then
errorcount=$((errorcount+1))
Logger "${FUNCNAME[0]} called by [$caller_name] finished monitoring [$pid] with exitcode [$result]."
fi
fi
done
## Log a standby message every hour
exec_time=$(($SECONDS - $seconds_begin))
if [ $((($exec_time + 1) % 3600)) -eq 0 ]; then
if [ $log_ttime -ne $exec_time ]; then
log_ttime=$exec_time
Logger "Current tasks still running with pids [${pidsArray[@]}]."
fi
fi
if [ $exec_time -gt $soft_max_time ]; then
if [ $soft_alert -eq 0 ] && [ $soft_max_time -ne 0 ]; then
Logger "Max soft execution time exceeded for task [$caller_name] with pids [${pidsArray[@]}]."
soft_alert=1
SendAlert
fi
if [ $exec_time -gt $hard_max_time ] && [ $hard_max_time -ne 0 ]; then
Logger "Max hard execution time exceeded for task [$caller_name] with pids [${pidsArray[@]}]. Stopping task execution."
kill -SIGTERM $pid
if [ $? == 0 ]; then
Logger "Task stopped successfully"
else
errrorcount=$((errorcount+1))
fi
fi
fi
pidsArray=("${newPidsArray[@]}")
sleep 1
done
Logger "${FUNCNAME[0]} ended for [$caller_name] using [$pidCount] subprocesses with [$errorcount] errors."
if [ $exit_on_error == true ] && [ $errorcount -gt 0 ]; then
Logger "Stopping execution."
exit 1337
else
return $errorcount
fi
}
# Just a plain stupid logging function to replace with yours
function Logger {
local value="${1}"
echo $value
}
Por ejemplo, espere a que terminen los tres procesos, registre una advertencia si la ejecución demora más de 5 segundos, detenga todos los procesos si la ejecución demora más de 120 segundos. No salga del programa en caso de fallas.
function something {
sleep 10 &
pids="$!"
sleep 12 &
pids="$pids;$!"
sleep 9 &
pids="$pids;$!"
WaitForTaskCompletion $pids 5 120 ${FUNCNAME[0]} false
}
# Launch the function
someting
Creo que la forma más sencilla de ejecutar trabajos en paralelo y verificar el estado es usar archivos temporales. Ya hay un par de respuestas similares (por ejemplo, Nietzche-jou y mug896).
#!/bin/bash
rm -f fail
for i in `seq 0 9`; do
doCalculations $i || touch fail &
done
wait
! [ -f fail ]
El código anterior no es seguro para subprocesos. Si le preocupa que el código anterior se ejecute al mismo tiempo que el mismo, es mejor usar un nombre de archivo más exclusivo, como fail. $$. La última línea es para cumplir el requisito: "devolver el código de salida 1 cuando cualquiera de los subprocesos termina con el código! = 0?" Tiré un requisito adicional allí para limpiar. Puede haber sido más claro escribirlo así:
#!/bin/bash
trap ''rm -f fail.$$'' EXIT
for i in `seq 0 9`; do
doCalculations $i || touch fail.$$ &
done
wait
! [ -f fail.$$ ]
Aquí hay un fragmento similar para recopilar resultados de varios trabajos: creo un directorio temporal, cuento los resultados de todas las sub tareas en un archivo separado y luego las vuelco para revisarlas. Esto realmente no coincide con la pregunta, lo estoy lanzando como un bono:
#!/bin/bash
trap ''rm -fr $WORK'' EXIT
WORK=/tmp/$$.work
mkdir -p $WORK
cd $WORK
for i in `seq 0 9`; do
doCalculations $i >$i.result &
done
wait
grep $ * # display the results with filenames and contents
El siguiente código esperará a que se completen todos los cálculos y devolverá el estado de salida 1 si falla alguno de los cálculos de DoCalculations .
#!/bin/bash
for i in $(seq 0 9); do
(doCalculations $i >&2 & wait %1; echo $?) &
done | grep -qv 0 && exit 1
Es posible que la señal de captura de CHLD no funcione porque puede perder algunas señales si llegan simultáneamente.
#!/bin/bash
trap ''rm -f $tmpfile'' EXIT
tmpfile=$(mktemp)
doCalculations() {
echo start job $i...
sleep $((RANDOM % 5))
echo ...end job $i
exit $((RANDOM % 10))
}
number_of_jobs=10
for i in $( seq 1 $number_of_jobs )
do
( trap "echo job$i : exit value : /$? >> $tmpfile" EXIT; doCalculations ) &
done
wait
i=0
while read res; do
echo "$res"
let i++
done < "$tmpfile"
echo $i jobs done !!!
Esto es lo que he encontrado hasta ahora. Me gustaría ver cómo interrumpir el comando de suspensión si un niño termina, para que uno no tenga que ajustar WAITALL_DELAY
al uso de uno.
waitall() { # PID...
## Wait for children to exit and indicate whether all exited with 0 status.
local errors=0
while :; do
debug "Processes remaining: $*"
for pid in "$@"; do
shift
if kill -0 "$pid" 2>/dev/null; then
debug "$pid is still alive."
set -- "$@" "$pid"
elif wait "$pid"; then
debug "$pid exited with zero exit status."
else
debug "$pid exited with non-zero exit status."
((++errors))
fi
done
(("$#" > 0)) || break
# TODO: how to interrupt this sleep when a child terminates?
sleep ${WAITALL_DELAY:-1}
done
((errors == 0))
}
debug() { echo "DEBUG: $*" >&2; }
pids=""
for t in 3 5 4; do
sleep "$t" &
pids="$pids $!"
done
waitall $pids
Esto funciona, ¡debería ser tan bueno, si no mejor, que la respuesta de @HoverHell!
#!/usr/bin/env bash
set -m # allow for job control
EXIT_CODE=0; # exit code of overall script
function foo() {
echo "CHLD exit code is $1"
echo "CHLD pid is $2"
echo $(jobs -l)
for job in `jobs -p`; do
echo "PID => ${job}"
wait ${job} || echo "At least one test failed with exit code => $?" ; EXIT_CODE=1
done
}
trap ''foo $? $$'' CHLD
DIRN=$(dirname "$0");
commands=(
"{ echo "foo" && exit 4; }"
"{ echo "bar" && exit 3; }"
"{ echo "baz" && exit 5; }"
)
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 for all to finish
wait;
echo "EXIT_CODE => $EXIT_CODE"
exit "$EXIT_CODE"
# end
y, por supuesto, he inmortalizado este script, en un proyecto NPM que le permite ejecutar comandos de bash en paralelo, útil para probar:
Estoy pensando que tal vez ejecute doCalculations; echo "$?" >> / tmp / acc en una subshell que se envía al fondo, luego la espera, luego / tmp / acc contendría los estados de salida, uno por línea. Sin embargo, no conozco las consecuencias de los múltiples procesos que se agregan al archivo del acumulador.
Aquí hay una prueba de esta sugerencia:
Archivo: doCalcualtions
#!/bin/sh random -e 20 sleep $? random -e 10
Archivo: intentar
#!/bin/sh rm /tmp/acc for i in $( seq 0 20 ) do ( ./doCalculations "$i"; echo "$?" >>/tmp/acc ) & done wait cat /tmp/acc | fmt rm /tmp/acc
Salida de ejecución ./try
5 1 9 6 8 1 2 0 9 6 5 9 6 0 0 4 9 5 5 9 8
He intentado esto y he combinado todas las mejores partes de los otros ejemplos aquí. Esta secuencia de comandos ejecutará la función checkpids
cuando checkpids
cualquier proceso en segundo plano y checkpids
el estado de salida sin recurrir a sondeo.
#!/bin/bash
set -o monitor
sleep 2 &
sleep 4 && exit 1 &
sleep 6 &
pids=`jobs -p`
checkpids() {
for pid in $pids; do
if kill -0 $pid 2>/dev/null; then
echo $pid is still alive.
elif wait $pid; then
echo $pid exited with zero exit status.
else
echo $pid exited with non-zero exit status.
fi
done
echo
}
trap checkpids CHLD
wait
La solución para esperar varios subprocesos y para salir cuando alguno de ellos sale con un código de estado distinto de cero es mediante el uso de ''wait -n''
#!/bin/bash
wait_for_pids()
{
for (( i = 1; i <= $#; i++ )) do
wait -n $@
status=$?
echo "received status: "$status
if [ $status -ne 0 ] && [ $status -ne 127 ]; then
exit 1
fi
done
}
sleep_for_10()
{
sleep 10
exit 10
}
sleep_for_20()
{
sleep 20
}
sleep_for_10 &
pid1=$!
sleep_for_20 &
pid2=$!
wait_for_pids $pid2 $pid1
El código de estado ''127'' es para un proceso que no existe, lo que significa que el hijo podría haber salido.
Necesitaba esto, pero el proceso de destino no era un hijo del shell actual, en cuyo caso, la wait $PID
no funciona. Encontré la siguiente alternativa en su lugar:
while [ -e /proc/$PID ]; do sleep 0.1 ; done
Eso depende de la presencia de procfs , que puede no estar disponible (Mac no lo proporciona, por ejemplo). Así que para la portabilidad, podrías usar esto en su lugar:
while ps -p $PID >/dev/null ; do sleep 0.1 ; done
No creo que sea posible con la funcionalidad incorporada de Bash.
Puede recibir una notificación cuando un niño sale:
#!/bin/sh
set -o monitor # enable script job control
trap ''echo "child died"'' CHLD
Sin embargo, no hay una forma aparente de obtener el estado de salida del niño en el controlador de señales.
Obtener ese estado secundario suele ser el trabajo de la familia de funciones de wait
en las API POSIX de nivel inferior. Desafortunadamente, el soporte de Bash para eso es limitado: puede esperar un proceso secundario específico (y obtener su estado de salida) o puede esperar a todos y obtener siempre un resultado de 0.
Lo que parece imposible de hacer es el equivalente de waitpid(-1)
, que bloquea hasta que se devuelve cualquier proceso secundario.
Para paralelizar esto ...
for i in $(whatever_list) ; do
do_something $i
done
Traducir a esto ...
for i in $(whatever_list) ; do echo $i ; done | ## execute in parallel...
(
export -f do_something ## export functions (if needed)
export PATH ## export any variables that are required
xargs -I{} --max-procs 0 bash -c '' ## process in batches...
{
echo "processing {}" ## optional
do_something {}
}''
)
- Si ocurre un error en un proceso, no interrumpirá los otros procesos, pero resultará en un código de salida distinto de cero de la secuencia en su totalidad .
- Las funciones y variables de exportación pueden o no ser necesarias, en cualquier caso particular.
- Puede configurar
--max-procs
según el paralelismo que desee (0
significa "todos a la vez"). - GNU Parallel ofrece algunas características adicionales cuando se usa en lugar de
xargs
, pero no siempre se instala de forma predeterminada. - El bucle
for
no es estrictamente necesario en este ejemplo, ya queecho $i
es básicamente la regeneración de la salida de$(whatever_list
). Simplemente creo que el uso de la palabra clavefor
hace que sea un poco más fácil ver lo que está sucediendo. - El manejo de cadenas de Bash puede ser confuso: he encontrado que usar comillas simples funciona mejor para envolver scripts no triviales.
- Puede interrumpir fácilmente toda la operación (usando ^ C o similar), a diferencia del enfoque más directo al paralelismo de Bash .
Aquí hay un ejemplo de trabajo simplificado ...
for i in {0..5} ; do echo $i ; done |xargs -I{} --max-procs 2 bash -c ''
{
echo sleep {}
sleep 2s
}''
Puede haber un caso en el que el proceso se complete antes de esperar el proceso. Si desencadenamos la espera de un proceso que ya ha finalizado, generará un error, ya que pid no es un hijo de este shell. Para evitar tales casos, la siguiente función se puede usar para determinar si el proceso está completo o no:
isProcessComplete(){
PID=$1
while [ -e /proc/$PID ]
do
echo "Process: $PID is still running"
sleep 5
done
echo "Process $PID has finished"
}
Si tiene disponible bash 4.2 o una versión posterior, lo siguiente podría serle útil. Utiliza matrices asociativas para almacenar nombres de tareas y su "código", así como nombres de tareas y sus pids. También incorporé un método simple de limitación de velocidad que podría ser útil si sus tareas consumen mucha CPU o tiempo de E / S y desea limitar la cantidad de tareas concurrentes.
El script inicia todas las tareas en el primer bucle y consume los resultados en el segundo.
Esto es un poco excesivo para los casos simples pero permite cosas bastante lindas. Por ejemplo, uno puede almacenar mensajes de error para cada tarea en otra matriz asociativa e imprimirlos después de que todo se haya calmado.
#! /bin/bash
main () {
local -A pids=()
local -A tasks=([task1]="echo 1"
[task2]="echo 2"
[task3]="echo 3"
[task4]="false"
[task5]="echo 5"
[task6]="false")
local max_concurrent_tasks=2
for key in "${!tasks[@]}"; do
while [ $(jobs 2>&1 | grep -c Running) -ge "$max_concurrent_tasks" ]; do
sleep 1 # gnu sleep allows floating point here...
done
${tasks[$key]} &
pids+=(["$key"]="$!")
done
errors=0
for key in "${!tasks[@]}"; do
pid=${pids[$key]}
local cur_ret=0
if [ -z "$pid" ]; then
echo "No Job ID known for the $key process" # should never happen
cur_ret=1
else
wait $pid
cur_ret=$?
fi
if [ "$cur_ret" -ne 0 ]; then
errors=$(($errors + 1))
echo "$key (${tasks[$key]}) failed."
fi
done
return $errors
}
main
Si tienes instalado GNU Parallel puedes hacer:
# If doCalculations is a function
export -f doCalculations
seq 0 9 | parallel doCalculations {}
GNU Parallel te dará el código de salida:
0 - Todos los trabajos se ejecutaron sin error.
1-253 - Algunos de los trabajos fallaron. El estado de salida da el número de trabajos fallidos
254 - Más de 253 trabajos fallaron.
255 - Otro error.
Mire los videos de introducción para obtener más información: http://pi.dk/1
Simplemente almacene los resultados fuera del shell, por ejemplo, en un archivo.
#!/bin/bash
tmp=/tmp/results
: > $tmp #clean the file
for i in `seq 0 9`; do
(doCalculations $i; echo $i:$?>>$tmp)&
done #iterate
wait #wait until all ready
sort $tmp | grep -v '':0'' #... handle as required
Utilicé esto recientemente (gracias a Alnitak):
#!/bin/bash
# activate child monitoring
set -o monitor
# locking subprocess
(while true; do sleep 0.001; done) &
pid=$!
# count, and kill when all done
c=0
function kill_on_count() {
# you could kill on whatever criterion you wish for
# I just counted to simulate bash''s wait with no args
[ $c -eq 9 ] && kill $pid
c=$((c+1))
echo -n ''.'' # async feedback (but you don''t know which one)
}
trap "kill_on_count" CHLD
function save_status() {
local i=$1;
local rc=$2;
# do whatever, and here you know which one stopped
# but remember, you''re called from a subshell
# so vars have their values at fork time
}
# care must be taken not to spawn more than one child per loop
# e.g don''t use `seq 0 9` here!
for i in {0..9}; do
(doCalculations $i; save_status $i $?) &
done
# wait for locking subprocess to be killed
wait $pid
echo
Desde allí, se puede extrapolar fácilmente y tener un disparador (tocar un archivo, enviar una señal) y cambiar los criterios de conteo (contar los archivos tocados, o lo que sea) para responder a ese disparador. O si solo quieres "cualquier" rc cero, simplemente elimina el bloqueo de save_status.
Veo muchos buenos ejemplos enumerados aquí, también quise incluir los míos.
#! /bin/bash
items="1 2 3 4 5 6"
pids=""
for item in $items; do
sleep $item &
pids+="$! "
done
for pid in $pids; do
wait $pid
if [ $? -eq 0 ]; then
echo "SUCCESS - Job $pid exited with a status of $?"
else
echo "FAILED - Job $pid exited with a status of $?"
fi
done
Uso algo muy similar para iniciar / detener servidores / servicios en paralelo y comprobar el estado de cada salida. Funciona muy bien para mí. ¡Espero que esto ayude a alguien!
Ya hay muchas respuestas aquí, pero me sorprende que nadie parece haber sugerido el uso de matrices ... Así que esto es lo que hice: esto podría ser útil para algunos en el futuro.
n=10 # run 10 jobs
c=0
PIDS=()
while true
my_function_or_command &
PID=$!
echo "Launched job as PID=$PID"
PIDS+=($PID)
(( c+=1 ))
# required to prevent any exit due to error
# caused by additional commands run which you
# may add when modifying this example
true
do
if (( c < n ))
then
continue
else
break
fi
done
# collect launched jobs
for pid in "${PIDS[@]}"
do
wait $pid || echo "failed job PID=$pid"
done
la trampa es tu amiga Usted puede atrapar en ERR en muchos sistemas. Puede interceptar EXIT o en DEBUG para realizar un fragmento de código después de cada comando.
Esto además de todas las señales estándar.
http://jeremy.zawodny.com/blog/archives/010717.html :
#!/bin/bash
FAIL=0
echo "starting"
./sleeper 2 0 &
./sleeper 2 1 &
./sleeper 3 0 &
./sleeper 2 0 &
for job in `jobs -p`
do
echo $job
wait $job || let "FAIL+=1"
done
echo $FAIL
if [ "$FAIL" == "0" ];
then
echo "YAY!"
else
echo "FAIL! ($FAIL)"
fi
wait
también (opcionalmente) toma el PID del proceso a esperar, y con $! obtienes el PID del último comando lanzado en segundo plano. Modifique el bucle para almacenar el PID de cada subproceso generado en una matriz, y luego haga un bucle nuevamente esperando cada PID.
# run processes and store pids in array
for i in $n_procs; do
./procs[${i}] &
pids[${i}]=$!
done
# wait for all pids
for pid in ${pids[*]}; do
wait $pid
done
#!/bin/bash
set -m
for i in `seq 0 9`; do
doCalculations $i &
done
while fg; do true; done
-
set -m
te permite usar fg & bg en un script -
fg
, además de poner el último proceso en primer plano, tiene el mismo estado de salida que el proceso en primer plano -
while fg
se detendrá cuando cualquierfg
salga con un estado de salida distinto de cero
desafortunadamente esto no manejará el caso cuando un proceso en segundo plano sale con un estado de salida distinto de cero. (el bucle no terminará de inmediato. esperará a que se completen los procesos anteriores).
set -e
fail () {
touch .failure
}
expect () {
wait
if [ -f .failure ]; then
rm -f .failure
exit 1
fi
}
sleep 2 || fail &
sleep 2 && false || fail &
sleep 2 || fail
expect
El set -e
en la parte superior hace que su script se detenga en caso de error.
expect
devolverá 1
si falla alguna sub-tarea.