Introduction

Il peut être parfois nécessaire d'exécuter des commandes en tâche de fond dans un script Bash. Les exemples qui suivent vous permettront de comprendre comment cela fonctionne et d'éviter les pièges les plus sournois.

Rappels

En mode interactif (à la console), pour lancer une commande en tâche de fond, il faut lui ajouter l'esperluette & en fin de ligne:

$ sleep 1 &
[1] 12432
$

Dans le script, on utilisera le même principe. Attention les canaux /dev/stdout et /dev/stderr seront automatiquement hérités aux enfants. Les sorties sur ces canaux deviendront vite illisibles si elles ne sont pas correctement redirigées. /dev/stdin, quant à lui sera /dev/null. Vos commandes lancées en tâche de fond ne pourront obtenir de données via ce canal.

Finalité

Les deux programmes ont pour but de démontrer la possibilité d'exécuter des commandes en tâche de fond. Dans ces exemples, il sera lancé quelques commandes sleep en tâche de fond, la finalité étant de ne sortir du programme qu'à la fin de l'exécution de tous les processus enfants, ceci pour contrôler leur correcte exécution.

Exemple 1 - mode bourrin

Le script fourni en exemple 1 est disponible ici:

Le PID de chaque processus enfant est ajouté à la chaîne de caractère $pids grâce à la variable spéciale $!. Cela servira, par la suite, à vérifier si le processus s'est terminé.

Le programme passe dans une boucle infinie:

while true; do 

Si la chaîne de caractère $pids n'est pas vide, c'est à dire qu'il reste des processus enfants encore en cours d'exécution, on teste alors chaque processus enfant avec un kill -0:

if [ -n "$pids" ] ; then
    # check each pid one by one
    for pid in $pids; do
        echo "Checking the $pid"
        # try to send a signal to the child, if child has exited, 
        #remove its pid from pids string
        kill -0 "$pid" 2>/dev/null || pids=$(echo $pids | sed "s/\b$pid\s*//")
    done
else

Si la commande kill -0 renvoie une erreur, dans le cas où le processus n'existe plus, alors son PID est retiré de la chaîne $pids:

pids=$(echo $pids | sed "s/\b$pid\s*//")

Si par contre la chaîne $pids est vide, cela signifie que tous les processus enfants se sont terminés:

        echo "All your process completed"
        break
    fi
done

On force alors le programme à sortir de la boucle infinie avec le break.

Pour
  • simple et efficace
Contre
  • pas de gestion des codes retour des processus enfants
  • utilisation CPU excessive dans la boucle infinie.

Exemple 2 - scrutateur

Le script fourni en exemple 2 est disponible ici:

Le script reprend le même principe de la chaîne $pid. Elle contient toujours les PID des processus enfants. Le programme fait appel à une fonction waitall() qui prend en paramètre tous les PID des processus enfants qu'il faut attendre. On retrouve la boucle infinie.

while :; do

Un parcours des PID est effectué, ainsi que le test du kill -0:

for pid in "$@"; do
    shift
    # test if still present
    if kill -0 "$pid" 2>/dev/null; then
        # still present, remove the pid from pid list
        debug "$pid is still alive."
        set -- "$@" "$pid"

Le programme entre dans le corps du if si le processus enfant est encore vivant, sinon on utilise la commande Bash wait qui permet de récupérer le code retour du processus enfant:

    elif wait "$pid"; then
        debug "$pid exited with zero exit status."
    else
        debug "$pid exited with non-zero exit status."
        ((++errors))
    fi

Si le processus enfant a renvoyé 0, le programme passe dans le elif .Si le processus s'est terminé avec un code retour différent de 0, un comptage des erreurs est gardé dans la variable $errors.

On force le programme à attendre une seconde avant de passer à l'itération suivante:

sleep ${WAITALL_DELAY:-1}

Cela permet de "soulager" le CPU. Il est possible de changer ce temps d'attente grâce à la variable $WAITALL_DELAY.

Si plus aucun processus enfant n'est en vie, alors on sort de la boucle infinie:

(("$#" > 0)) || break

Enfin, on sort de la fonction avec un code retour 0 si aucun processus enfant n'est sorti en erreur:

((errors == 0))
Pour
  • Simple à mettre en œuvre grâce à la fonction waitall()
Contre
  • Boucle infinie sans gestion de timeout
  • obligation de faire un sleep 1 à chaque itération

A venir

Dans le prochain billet, on verra comment se passer du sleep 1 en utilisant une interruption de bash (trap). On verra aussi qu'il est possible de gérer des timeout à l'exécution des processus enfants.