Attendez qu'un processus se termine

147

Existe-t-il une fonctionnalité intégrée dans Bash pour attendre la fin d'un processus?

La waitcommande permet seulement d'attendre la fin des processus enfants. Je voudrais savoir s'il existe un moyen d'attendre la fin d'un processus avant de procéder à un script.

Une façon mécanique de le faire est la suivante, mais j'aimerais savoir s'il existe une fonctionnalité intégrée dans Bash.

while ps -p `cat $PID_FILE` > /dev/null; do sleep 1; done
Biswajyoti Das
la source
4
Permettez-moi de donner deux avertissements : 1. Comme indiqué ci-dessous par mp3foley , "kill -0" ne fonctionne pas toujours pour POSIX. 2. Vous voulez probablement aussi vous assurer que le processus n'est pas un zombie, ce qui est pratiquement un processus terminé. Voir le commentaire de mp3foley et le mien pour les détails.
teika kazura
2
Autre mise en garde ( signalée à l'origine par ks1322 ci-dessous): l'utilisation d'un PID autre qu'un processus enfant n'est pas robuste. Si vous voulez un moyen sécurisé, utilisez par exemple IPC.
teika kazura

Réponses:

138

Attendre la fin de tout processus

Linux:

tail --pid=$pid -f /dev/null

Darwin (nécessite d' $pidavoir des fichiers ouverts):

lsof -p $pid +r 1 &>/dev/null

Avec timeout (secondes)

Linux:

timeout $timeout tail --pid=$pid -f /dev/null

Darwin (nécessite d' $pidavoir des fichiers ouverts):

lsof -p $pid +r 1m%s -t | grep -qm1 $(date -v+${timeout}S +%s 2>/dev/null || echo INF)
Rauno Palosaari
la source
42
Qui aurait su que tailferait cela.
ctrl-alt-delor
8
tailfonctionne sous le capot en interrogeant kill(pid, SIG_0)un processus (découvert en utilisant strace).
Att Righ
2
Notez que lsof utilise l'interrogation, c'est-à- +r 1dire le timeout, je recherche personnellement une solution pour MacOS qui n'utilise pas d'interrogation.
Alexander Mills
1
Cette astuce échoue pour les zombies. C'est correct pour les processus que vous ne pouvez pas tuer; taila la ligne kill (pid, 0) != 0 && errno != EPERM.
teika kazura
2
@AlexanderMills, si vous pouvez tolérer que votre système macOS ne se mette pas en veille pendant l'exécution de la commande, caffeinate -w $pidfera l'affaire.
zneak
83

Il n'y a pas de builtin. À utiliser kill -0en boucle pour une solution viable:

anywait(){

    for pid in "$@"; do
        while kill -0 "$pid"; do
            sleep 0.5
        done
    done
}

Ou comme oneliner plus simple pour une utilisation simple et unique:

while kill -0 PIDS 2> /dev/null; do sleep 1; done;

Comme l'ont noté plusieurs commentateurs, si vous souhaitez attendre des processus auxquels vous n'avez pas le privilège d'envoyer des signaux, vous avez trouvé un autre moyen de détecter si le processus est en cours d'exécution pour remplacer l' kill -0 $pidappel. Sous Linux, test -d "/proc/$pid"fonctionne, sur d'autres systèmes, vous devrez peut-être utiliser pgrep(si disponible) ou quelque chose du genre ps | grep "^$pid ".

Teddy
la source
2
Attention : cela ne fonctionne pas toujours, comme indiqué ci-dessous par mp3foley . Voir ce commentaire et le mien pour les détails.
teika kazura
2
Attention 2 (sur les zombies): Le commentaire de suivi de Teddy ci-dessus n'est pas encore suffisant, car il peut s'agir de zombies. Voir ma réponse ci-dessous pour une solution Linux.
teika kazura
4
Cette solution ne risque-t-elle pas une condition de concurrence? Pendant le sommeil sleep 0.5, le processus avec $pidpeut mourir et un autre processus peut être créé avec le même $pid. Et nous finirons par attendre 2 processus différents (voire plus) avec le même $pid.
ks1322 du
2
@ ks1322 Oui, ce code a en effet une condition de concurrence.
Teddy
4
Les PID ne sont-ils pas généralement générés séquentiellement? Quelle est la probabilité que le décompte s'enroule en une seconde?
esmiralha
53

J'ai trouvé que "kill -0" ne fonctionne pas si le processus appartient à root (ou autre), j'ai donc utilisé pgrep et j'ai trouvé:

while pgrep -u root process_name > /dev/null; do sleep 1; done

Cela aurait l'inconvénient de correspondre probablement aux processus zombies.

mp3foley
la source
2
Bonne observation. Dans POSIX, l'appel système kill(pid, sig=0)échoue si le processus appelant n'a pas le privilège de tuer. Ainsi / bin / kill -0 et "kill -0" (bash intégré) échouent également dans les mêmes conditions.
teika kazura
31

Cette boucle de script bash se termine si le processus n'existe pas ou si c'est un zombie.

PID=<pid to watch>
while s=`ps -p $PID -o s=` && [[ "$s" && "$s" != 'Z' ]]; do
    sleep 1
done

EDIT : Le script ci-dessus a été donné ci-dessous par Rockallite . Merci!

Ma réponse originale ci-dessous fonctionne pour Linux, en s'appuyant sur procfsie /proc/. Je ne connais pas sa portabilité:

while [[ ( -d /proc/$PID ) && ( -z `grep zombie /proc/$PID/status` ) ]]; do
    sleep 1
done

Ce n'est pas limité au shell, mais les OS eux-mêmes n'ont pas d'appels système pour surveiller l'arrêt des processus non enfants.

teika kazura
la source
1
Joli. Bien que je devais entourer grep /proc/$PID/statusde guillemets doubles ( bash: test: argument expected)
Griddo
Hum ... je l'ai essayé à nouveau et ça a marché. Je suppose que j'ai fait quelque chose de mal la dernière fois.
Griddo
7
Ouwhile s=`ps -p $PID -o s=` && [[ "$s" && "$s" != 'Z' ]]; do sleep 1; done
Rockallite
1
Malheureusement, cela ne fonctionne pas dans BusyBox - dans son ps, ni l'un -pni l' autre ou s=sont pris en charge
ZimbiX
14

FreeBSD et Solaris ont cet pwait(1)utilitaire pratique , qui fait exactement ce que vous voulez.

Je crois que d'autres systèmes d'exploitation modernes ont également les appels système nécessaires (MacOS, par exemple, implémente BSD kqueue), mais tous ne le rendent pas disponible à partir de la ligne de commande.

Mikhail T.
la source
2
> BSD and Solaris: Inspecter les trois grands BSD qui me viennent à l'esprit; ni OpenBSD ni NetBSD n'ont cette fonction (dans leurs pages de manuel), seul FreeBSD le fait, comme vous pouvez facilement le vérifier sur man.openbsd.org .
benaryorg
On dirait que vous avez raison. Mea culpa ... Ils sont tous implémentés kqueue, donc compiler FreeBSD pwait(1)serait trivial, cependant. Pourquoi les autres BSD n'importent-ils pas la fonctionnalité qui m'échappe ...
Mikhail T.
1
plink me@oracle box -pw redacted "pwait 6998";email -b -s "It's done" etcm'a juste permis de rentrer à la maison maintenant au lieu de quelques heures.
zzxyz
11

Depuis la page de manuel bash

   wait [n ...]
          Wait for each specified process and return its termination  status
          Each  n  may be a process ID or a job specification; if a
          job spec is given, all processes  in  that  job's  pipeline  are
          waited  for.  If n is not given, all currently active child processes
          are waited for, and the return  status  is  zero.   If  n
          specifies  a  non-existent  process or job, the return status is
          127.  Otherwise, the return status is the  exit  status  of  the
          last process or job waited for.
Charlweed
la source
56
C'est vrai mais il ne peut attendre que l'enfant du shell actuel. Vous ne pouvez attendre aucun processus.
gumik
@gumik: "Si n n'est pas donné, tous les processus enfants actuellement actifs sont attendus" . Cela fonctionne parfaitement .. waitsans argument bloquera le processus jusqu'à ce que tous les processus enfants se terminent. Honnêtement, je ne vois aucun intérêt à attendre un processus car il y a toujours des processus système en cours.
coderofsalvation
1
@coderofsalvation (sleep 10 & sleep 3 & wait) prend 10 secondes pour revenir: wait without args bloquera jusqu'à ce que TOUS les processus enfants se terminent. OP souhaite être notifié lorsque le premier processus enfant (ou nominé) se termine.
android.weasel
Ne fonctionne pas non plus si le processus n'est pas en arrière-plan ou au premier plan (sous Solaris, Linux ou Cygwin). ex. sleep 1000 ctrl-z wait [sleep pid]retourne immédiatement
zzxyz
6

Toutes ces solutions sont testées dans Ubuntu 14.04:

Solution 1 (en utilisant la commande ps): Juste pour ajouter à la réponse de Pierz, je suggérerais:

while ps axg | grep -vw grep | grep -w process_name > /dev/null; do sleep 1; done

Dans ce cas, grep -vw grepgarantit que grep correspond uniquement à nom_processus et non à grep lui-même. Il a l'avantage de prendre en charge les cas où le process_name n'est pas à la fin d'une ligne à ps axg.

Solution 2 (en utilisant la commande supérieure et le nom du processus):

while [[ $(awk '$12=="process_name" {print $0}' <(top -n 1 -b)) ]]; do sleep 1; done

Remplacer process_name par le nom du processus qui apparaît dans top -n 1 -b. Veuillez conserver les guillemets.

Pour voir la liste des processus que vous attendez qu'ils soient terminés, vous pouvez exécuter:

while : ; do p=$(awk '$12=="process_name" {print $0}' <(top -n 1 -b)); [[ $b ]] || break; echo $p; sleep 1; done

Solution 3 (en utilisant la commande supérieure et l'ID de processus):

while [[ $(awk '$1=="process_id" {print $0}' <(top -n 1 -b)) ]]; do sleep 1; done

Remplacez-le process_idpar l'ID de processus de votre programme.

Saeid BK
la source
4
Downvote: le long grep -v greppipeline est un anti-modèle massif, et cela présuppose que vous n'avez pas de processus sans rapport avec le même nom. Si vous connaissez plutôt les PID, cela pourrait être adapté à une solution fonctionnant correctement.
tripleee
Merci tripleee pour le commentaire. J'ai ajouté le drapeau -wpour éviter le problème grep -v grep dans une certaine mesure . J'ai également ajouté deux autres solutions basées sur votre commentaire.
Saeid BK le
5

D'accord, il semble donc que la réponse soit - non, il n'y a pas d'outil intégré.

Après avoir réglé /proc/sys/kernel/yama/ptrace_scopesur 0, il est possible d'utiliser le straceprogramme. D'autres commutateurs peuvent être utilisés pour le rendre silencieux, afin qu'il attende vraiment passivement:

strace -qqe '' -p <PID>
émeu
la source
1
Joli! Il semble cependant qu'il n'est pas possible de s'attacher à un PID donné à partir de deux endroits différents (j'obtiens Operation not permittedpour la seconde instance de strace); pouvez-vous le confirmer?
eudoxos
@eudoxos Oui, la page de manuel de ptrace dit: (...)"tracee" always means "(one) thread"(et je confirme l'erreur que vous mentionnez). Pour laisser plus de processus attendre de cette façon, vous devez créer une chaîne.
emu
2

Solution de blocage

Utilisez le waitdans une boucle, pour attendre la fin de tous les processus:

function anywait()
{

    for pid in "$@"
    do
        wait $pid
        echo "Process $pid terminated"
    done
    echo 'All processes terminated'
}

Cette fonction se terminera immédiatement, lorsque tous les processus seront terminés. C'est la solution la plus efficace.

Solution non bloquante

Utilisez le kill -0dans une boucle, pour attendre la fin de tous les processus + faire quoi que ce soit entre les vérifications:

function anywait_w_status()
{
    for pid in "$@"
    do
        while kill -0 "$pid"
        do
            echo "Process $pid still running..."
            sleep 1
        done
    done
    echo 'All processes terminated'
}

Le temps de réaction a diminué avec le sleeptemps, car il faut éviter une utilisation élevée du processeur.

Un usage réaliste:

En attente de la fin de tous les processus + informer l'utilisateur de tous les PID en cours d'exécution.

function anywait_w_status2()
{
    while true
    do
        alive_pids=()
        for pid in "$@"
        do
            kill -0 "$pid" 2>/dev/null \
                && alive_pids+="$pid "
        done

        if [ ${#alive_pids[@]} -eq 0 ]
        then
            break
        fi

        echo "Process(es) still running... ${alive_pids[@]}"
        sleep 1
    done
    echo 'All processes terminated'
}

Remarques

Ces fonctions obtiennent des PID via des arguments en $@tant que tableau BASH.

andras.tim
la source
2

J'ai eu le même problème, j'ai résolu le problème en tuant le processus, puis en attendant que chaque processus se termine en utilisant le système de fichiers PROC:

while [ -e /proc/${pid} ]; do sleep 0.1; done
Littlebridge
la source
le sondage est très mauvais, vous pourriez obtenir la visite de la police :)
Alexander Mills
2

Il n'y a pas de fonction intégrée pour attendre la fin d'un processus.

Vous pouvez envoyer kill -0à n'importe quel PID trouvé, afin de ne pas être déconcerté par les zombies et les éléments qui seront toujours visibles dans ps(tout en récupérant la liste PID en utilisant ps).

TheBonsai
la source
1

Utilisez inotifywait pour surveiller certains fichiers qui se ferment lorsque votre processus se termine. Exemple (sous Linux):

yourproc >logfile.log & disown
inotifywait -q -e close logfile.log

-e spécifie l'événement à attendre, -q signifie une sortie minimale uniquement à la fin. Dans ce cas, ce sera:

logfile.log CLOSE_WRITE,CLOSE

Une seule commande wait peut être utilisée pour attendre plusieurs processus:

yourproc1 >logfile1.log & disown
yourproc2 >logfile2.log & disown
yourproc3 >logfile3.log & disown
inotifywait -q -e close logfile1.log logfile2.log logfile3.log

La chaîne de sortie de inotifywait vous indiquera quel processus s'est terminé. Cela ne fonctionne qu'avec des fichiers 'réels', pas avec quelque chose dans / proc /

Burghard Hoffmann
la source
0

Sur un système comme OSX, vous n'avez peut-être pas pgrep, vous pouvez donc essayer cette approche, lorsque vous recherchez des processus par nom:

while ps axg | grep process_name$ > /dev/null; do sleep 1; done

Le $symbole à la fin du nom du processus garantit que grep correspond uniquement à nom_processus à la fin de la ligne dans la sortie ps et non à lui-même.

Pierz
la source
Horrible: Il peut y avoir plusieurs processus portant ce nom quelque part dans la ligne de commande , votre propre grep inclus. Au lieu de rediriger vers /dev/null, -qdoit être utilisé avec grep. Une autre instance du processus a peut-être commencé pendant que votre boucle dormait et vous ne saurez jamais ...
Mikhail T.
Je ne sais pas pourquoi vous avez choisi cette réponse comme «horrible» - comme une approche similaire a été suggérée par d'autres? Bien que la -qsuggestion soit valide, comme je l'ai mentionné dans ma réponse spécifiquement, la terminaison $signifie que grep ne correspondra pas au nom «quelque part dans la ligne de commande» ni ne correspondra à lui-même. Avez-vous réellement essayé sur OSX?
Pierz
0

La solution de Rauno Palosaari pour Timeout in Seconds Darwin, est une excellente solution de contournement pour un système d'exploitation de type UNIX qui n'a pas GNU tail(ce n'est pas spécifique à Darwin). Mais, selon l'âge du système d'exploitation de type UNIX, la ligne de commande proposée est plus complexe que nécessaire et peut échouer:

lsof -p $pid +r 1m%s -t | grep -qm1 $(date -v+${timeout}S +%s 2>/dev/null || echo INF)

Sur au moins un ancien UNIX, l' lsofargument +r 1m%séchoue (même pour un super-utilisateur):

lsof: can't read kernel name list.

Le m%sest une spécification de format de sortie. Un post-processeur plus simple n'en a pas besoin. Par exemple, la commande suivante attend le PID 5959 pendant cinq secondes maximum:

lsof -p 5959 +r 1 | awk '/^=/ { if (T++ >= 5) { exit 1 } }'

Dans cet exemple, si le PID 5959 sort de lui-même avant la fin des cinq secondes, ${?}est 0. Sinon ${?}revient 1après cinq secondes.

Il peut être intéressant de noter expressément que dans +r 1 , le 1est l'intervalle d'interrogation (en secondes), il peut donc être modifié en fonction de la situation.

kbulgrien
la source