J'ai une commande appelée CMD à partir de mon script principal Bourne Shell qui prend une éternité.
Je veux modifier le script comme suit:
Quelqu'un peut-il me donner des indications pour accomplir cela?
1: dans bash, $!
contient le PID du dernier processus d'arrière-plan exécuté. Cela vous indiquera quel processus surveiller, de toute façon.
4: wait <n>
attend que le processus avec ID soit terminé (il sera bloqué jusqu'à ce que le processus soit terminé, de sorte que vous ne souhaitez peut-être pas l'appeler tant que vous n'êtes pas sûr que le processus est terminé). Après le retour de wait
, le code de sortie du processus est renvoyé dans la variable $?
2, 3: ps
ou ps | grep " $! "
peut vous dire si le processus est toujours en cours d'exécution. Il vous appartient de comprendre le résultat et de décider s'il est proche de la fin. (ps | grep
n'est pas idiot. Si vous avez le temps, vous pouvez trouver un moyen plus robuste de dire si le processus est toujours en cours d'exécution).
Voici un script squelette:
# 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
my_status=$?
echo The exit status of the process was $my_status
Voici comment je l'ai résolu quand j'avais un besoin similaire:
# 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 finnish, will take max 14s
for p in $pids; do
if wait $p; then
echo "Process $p success"
else
echo "Process $p fail"
fi
done
#/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 $?
Comme je le vois presque toutes les réponses utilisent des utilitaires externes (principalement ps
) pour interroger l’état du processus en arrière-plan. Il existe une solution plus unixesh, capturant le signal SIGCHLD. Dans le gestionnaire de signaux, il faut vérifier quel processus enfant a été arrêté. Cela peut être fait par kill -0 <PID>
intégré (universel) ou en vérifiant l'existence du répertoire /proc/<PID>
(spécifique à Linux) ou en utilisant le jobs
intégré ( bash spécifique. jobs -l
indique également le pid. Dans ce cas, le 3ème champ de la sortie peut être Stopped | Running | Done | Exit.).
Voici mon exemple.
Le processus lancé s'appelle loop.sh
. Il accepte -x
ou un nombre en tant qu'argument. Pour -x
, quitte avec le code de sortie 1. Pour un numéro, attend num * 5 secondes. Toutes les 5 secondes, il affiche son PID.
Le processus de lancement s'appelle 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
Pour plus d'explications, voir: Le démarrage d'un processus à partir d'un script bash a échoué
Le pid d'un processus enfant avec arrière-plan est stocké dans $! . Vous pouvez stocker les pids de tous les processus enfants dans un tableau, par exemple. PIDS [] .
wait [-n] [jobspec or pid …]
Attendez que le processus enfant spécifié par chaque ID de processus pid ou jobpec de spécification de travail se ferme et renvoie l'état de sortie de la dernière commande attendue. Si une spécification de travail est donnée, tous les processus du travail sont attendus. Si aucun argument n'est fourni, tous les processus enfants actuellement actifs sont attendus et le statut de retour est zéro. Si l'option -n est fournie, wait attend que tout travail se termine et renvoie son statut de sortie. Si ni jobspec ni pid ne spécifient un processus enfant actif du Shell, le statut de retour est 127.
Utilisez wait command vous permet d’attendre la fin de tous les processus enfants. Vous pouvez également obtenir le statut de sortie de chaque processus enfant via $? et enregistre le statut dans STATUS [] . Ensuite, vous pouvez faire quelque chose en fonction de votre statut.
J'ai essayé les 2 solutions suivantes et elles fonctionnent bien. solution01 est plus concis, alors que solution02 est un peu compliqué.
#!/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
#!/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
Je changerais légèrement votre approche. Plutôt que de vérifier toutes les quelques secondes si la commande est toujours active et de signaler un message, utilisez un autre processus qui indique toutes les quelques secondes que la commande est toujours en cours d'exécution, puis supprimez ce processus à la fin de la commande. Par exemple:
#!/bin/sh cmd () {sommeil 5; sortie 24; } cmd & # Exécuter le processus de longue durée pid = $! # Enregistrez le pid # Créez un processus qui indique de manière continue que la commande est toujours en cours d'exécution While echo "$ (date): $ pid est toujours en cours d'exécution"; dormez 1; done & echoer = $! # Définir un piège pour tuer le rapporteur à la fin du processus trap 'kill $ echoer' 0 # Attendre la fin du processus if wait $ pid; puis echo "cmd successed" else echo "cmd FAILED !! (return $?)" fi
Notre équipe avait le même besoin avec un script exécuté à distance par SSH qui expirait après 25 minutes d’inactivité. Voici une solution avec la boucle de surveillance vérifiant le processus d'arrière-plan toutes les secondes, mais n'imprimant que toutes les 10 minutes pour supprimer un délai d'inactivité.
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}
Un exemple simple, semblable aux solutions ci-dessus. Cela ne nécessite aucune surveillance de la sortie du processus. L'exemple suivant utilise tail pour suivre la sortie.
$ 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
Utilisez tail pour suivre la sortie du processus et quitter à la fin du processus.
$ 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
Une autre solution consiste à surveiller les processus via le système de fichiers proc (plus sûr que le combo ps/grep); lorsque vous démarrez un processus, il a un dossier correspondant dans/proc/$ pid, la solution peut donc être
#!/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
....
Vous pouvez maintenant utiliser la variable $ exit_status comme bon vous semble.
Avec cette méthode, votre script n’a pas à attendre le processus en arrière-plan, il vous suffira de surveiller un fichier temporaire pour connaître le statut de sortie.
FUNCmyCmd() { sleep 3;return 6; };
export retFile=$(mktemp);
FUNCexecAndWait() { FUNCmyCmd;echo $? >$retFile; };
FUNCexecAndWait&
maintenant, votre script peut faire autre chose pendant que vous devez simplement surveiller le contenu de retFile (il peut également contenir toute autre information que vous souhaitez, comme l'heure de sortie).
PS: au fait, j'ai codé en pensant à bash
Cela peut aller au-delà de votre question, mais si vous êtes préoccupé par la durée d'exécution des processus, vous pouvez être intéressé par la vérification du statut des processus en arrière-plan en cours d'exécution après un intervalle de temps. Il est assez facile de vérifier quels PID enfants sont toujours en cours d'utilisation à l'aide de pgrep -P $$
. Cependant, j'ai proposé la solution suivante pour vérifier le statut de sortie de ces PID qui ont déjà expiré:
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
qui produit:
interval-2: pid-28083:running; pid-28084:running;
interval-7: pid-28083:exited(24); pid-28084:running;
interval-11: pid-28084:exited(0);
Remarque: Vous pouvez remplacer $pids
par une variable chaîne plutôt que par un tableau afin de simplifier les choses si vous le souhaitez.