Espera a que termine un proceso

147

¿Hay alguna función integrada en Bash para esperar a que finalice un proceso?

El waitcomando solo permite esperar a que finalicen los procesos secundarios. Me gustaría saber si hay alguna forma de esperar a que termine algún proceso antes de continuar con cualquier script.

Una forma mecánica de hacer esto es la siguiente, pero me gustaría saber si hay alguna característica integrada en Bash.

while ps -p `cat $PID_FILE` > /dev/null; do sleep 1; done
Biswajyoti Das
fuente
44
Permítanme darles dos precauciones : 1. Como se señala a continuación por mp3foley , "kill -0" no siempre funciona para POSIX. 2. Probablemente también quieras asegurarte de que el proceso no sea un zombie, que es prácticamente un proceso terminado. Vea el comentario de mp3foley y el mío para más detalles.
teika kazura
2
Otra precaución ( originalmente señalada por ks1322 a continuación): el uso de PID que no sea un proceso secundario no es sólido. Si desea una forma segura, utilice, por ejemplo, IPC.
teika kazura

Respuestas:

138

Esperar a que termine cualquier proceso

Linux:

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

Darwin (requiere que $pidtenga archivos abiertos):

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

Con tiempo de espera (segundos)

Linux:

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

Darwin (requiere que $pidtenga archivos abiertos):

lsof -p $pid +r 1m%s -t | grep -qm1 $(date -v+${timeout}S +%s 2>/dev/null || echo INF)
Rauno Palosaari
fuente
42
Quién hubiera sabido que tailharía esto.
ctrl-alt-delor
8
tailfunciona bajo el capó sondeando kill(pid, SIG_0)a un proceso (descubierto usando strace).
Att Righ
2
Tenga en cuenta que lsof usa el sondeo, que +r 1es el tiempo de espera, personalmente estoy buscando una solución para MacOS que no use el sondeo.
Alexander Mills
1
Este truco falla para los zombies. Está bien para procesos que no puedes matar; tailtiene la linea kill (pid, 0) != 0 && errno != EPERM.
teika kazura
2
@AlexanderMills, si puede tolerar que su sistema macOS no se vaya a dormir mientras se ejecuta el comando, caffeinate -w $pidhará el truco.
zneak
83

No hay incorporado. Use kill -0en un bucle para una solución viable:

anywait(){

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

O como una línea más simple para un uso fácil y único:

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

Como señalaron varios comentaristas, si desea esperar procesos a los que no tiene el privilegio de enviar señales, debe encontrar otra forma de detectar si el proceso se está ejecutando para reemplazar la kill -0 $pidllamada. En Linux, test -d "/proc/$pid"funciona, en otros sistemas puede que tenga que usar pgrep(si está disponible) o algo así ps | grep "^$pid ".

Osito de peluche
fuente
2
Precaución : Esto no siempre funciona, como se señala a continuación por mp3foley . Vea ese comentario y el mío para los detalles.
teika kazura
2
Precaución 2 (sobre zombies): el comentario de seguimiento de Teddy anterior aún no es suficiente, ya que pueden ser zombies. Vea mi respuesta a continuación para obtener una solución de Linux.
teika kazura
44
¿Esta solución no arriesga una condición de carrera? Mientras duerme sleep 0.5, el proceso con $pidpuede morir y se puede crear otro proceso con el mismo $pid. Y terminaremos esperando 2 procesos diferentes (o incluso más) con el mismo $pid.
ks1322
2
@ ks1322 Sí, este código tiene una condición de carrera.
Teddy
44
¿No se suelen generar PID secuencialmente? ¿Cuál es la probabilidad de que el conteo termine en un segundo?
esmiralha
53

Encontré que "kill -0" no funciona si el proceso es propiedad de root (u otro), así que usé pgrep y se me ocurrió:

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

Esto tendría la desventaja de que probablemente coincida con los procesos de zombies.

mp3foley
fuente
2
Buena observación. En POSIX, la llamada al sistema kill(pid, sig=0)falla si el proceso de la persona que llama no tiene el privilegio de matar. Por lo tanto, / bin / kill -0 y "kill -0" (bash incorporado) también fallan en la misma condición.
teika kazura
31

Este bucle de script bash finaliza si el proceso no existe o si es un zombie.

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

EDITAR : El guión anterior fue dado a continuación por Rockallite . ¡Gracias!

Mi respuesta original a continuación funciona para Linux, confiando en procfsie /proc/. No sé su portabilidad:

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

No se limita al shell, pero los sistemas operativos en sí no tienen llamadas al sistema para ver la finalización del proceso que no es secundario.

teika kazura
fuente
1
Buena esa. Aunque tuve que rodearlo grep /proc/$PID/statuscon comillas dobles ( bash: test: argument expected)
Griddo
Hum ... solo lo intenté de nuevo y funcionó. Supongo que hice algo mal la última vez.
Griddo
77
O bienwhile s=`ps -p $PID -o s=` && [[ "$s" && "$s" != 'Z' ]]; do sleep 1; done
Rockallite
1
Por desgracia, esto no funciona en BusyBox - en su psni -po s=se soportan
ZimbiX
14

FreeBSD y Solaris tienen esta práctica pwait(1)utilidad, que hace exactamente lo que quieres.

Creo que otros sistemas operativos modernos también tienen las llamadas necesarias al sistema (MacOS, por ejemplo, implementa BSD kqueue), pero no todos lo hacen disponible desde la línea de comandos.

Mikhail T.
fuente
2
> BSD and Solaris: Inspeccionar los tres grandes BSD que vienen a la mente; ni OpenBSD ni NetBSD tienen esta función (en sus páginas de manual), solo FreeBSD la tiene, como puede comprobar fácilmente en man.openbsd.org .
benaryorg
Parece que tienes razón. Mea culpa ... Todos ellos implementan kqueue, por lo que compilar FreeBSD pwait(1)sería trivial, sin embargo. ¿Por qué no los demás BSD importar la función me escapa ...
Mikhail T.
1
plink me@oracle box -pw redacted "pwait 6998";email -b -s "It's done" etcsolo me permitió irme a casa ahora en lugar de dentro de horas.
zzxyz
11

Desde la página de manual de 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
fuente
56
Eso es cierto, pero solo puede esperar al hijo del shell actual. No puedes esperar por ningún proceso.
gumik
@gumik: "Si no se da n, se esperan todos los procesos secundarios actualmente activos" . Esto funciona perfectamente ... waitsin argumentos bloqueará el proceso hasta que finalice cualquier proceso hijo . Honestamente, no veo ningún punto para esperar ningún proceso, ya que siempre están sucediendo procesos del sistema.
coderofsalvation
1
@coderofsalvation (dormir 10 y dormir 3 y esperar) tarda 10 segundos en regresar: esperar sin argumentos se bloqueará hasta que TODOS los procesos secundarios finalicen. OP quiere recibir una notificación cuando finalice el primer proceso secundario (o nominado).
android.weasel
Tampoco funciona si el proceso no está en segundo plano o en segundo plano (en Solaris, Linux o Cygwin). ex. sleep 1000 ctrl-z wait [sleep pid]regresa inmediatamente
zzxyz
6

Todas estas soluciones se prueban en Ubuntu 14.04:

Solución 1 (usando el comando ps): solo para agregar a la respuesta de Pierz, sugeriría:

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

En este caso, grep -vw grepasegura que grep solo coincida con nombre_proceso y no grep en sí mismo. Tiene la ventaja de admitir los casos en que process_name no está al final de una línea en ps axg.

Solución 2 (usando el comando superior y el nombre del proceso):

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

Reemplace process_namecon el nombre del proceso que aparece en top -n 1 -b. Por favor, mantenga las comillas.

Para ver la lista de procesos que espera a que finalicen, puede ejecutar:

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

Solución 3 (usando el comando superior y la ID del proceso):

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

Reemplace process_idcon el ID de proceso de su programa.

Saeid BK
fuente
44
Voto a favor: la larga grep -v greptubería es un antipatrón masivo, y esto presupone que no tiene procesos no relacionados con el mismo nombre. Si conoce los PID en su lugar, esto podría adaptarse a una solución que funcione correctamente.
tripleee
Gracias tripleee por el comentario. Agregué la bandera -wpara evitar el problema grep -v grep hasta cierto punto . También agregué dos soluciones más basadas en tu comentario.
Saeid BK
5

De acuerdo, parece que la respuesta es: no, no hay una herramienta integrada.

Después de ajustar /proc/sys/kernel/yama/ptrace_scopea 0, es posible utilizar el straceprograma. Se pueden usar más interruptores para silenciarlo, de modo que realmente espere pasivamente:

strace -qqe '' -p <PID>
emú
fuente
1
¡Buena esa! Sin embargo, parece que no es posible adjuntar a un PID dado desde dos lugares diferentes (obtengo Operation not permittedpara la segunda instancia de strace); puedes confirmar eso?
eudoxos
@eudoxos Sí, la página de manual de ptrace dice: (...)"tracee" always means "(one) thread"(y confirmo el error que mencionas). Para permitir que más procesos esperen de esta manera, tendrías que hacer una cadena.
emu
2

Solución de bloqueo

Use el waiten un bucle para esperar a que finalicen todos los procesos:

function anywait()
{

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

Esta función se cerrará de inmediato, cuando todos los procesos se terminaron. Esta es la solución más eficiente.

Solución no bloqueante

Utilice el kill -0en un bucle, para esperar la finalización de todos los procesos + hacer cualquier cosa entre comprobaciones:

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'
}

El tiempo de reacción disminuyó a sleeptiempo, ya que hay que evitar el uso elevado de CPU.

Un uso realista:

En espera de terminar todos los procesos + informar al usuario sobre todos los PID en ejecución.

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'
}

Notas

Estas funciones obtienen PID a través de argumentos $@como BASH array.

andras.tim
fuente
2

Tuve el mismo problema, resolví el problema eliminando el proceso y luego esperando que cada proceso terminara de usar el sistema de archivos PROC:

while [ -e /proc/${pid} ]; do sleep 0.1; done
littlebridge
fuente
la votación es muy mala, podrías recibir una visita de la policía :)
Alexander Mills
2

No hay una función integrada que espere a que finalice cualquier proceso.

Podrías enviar kill -0a cualquier PID encontrado, para que no te desconcierten los zombis y las cosas que aún serán visibles ps(mientras recuperas la lista PID usando ps).

TheBonsai
fuente
1

Use inotifywait para monitorear algunos archivos que se cierran cuando finaliza su proceso. Ejemplo (en Linux):

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

-e especifica el evento a esperar, -q significa salida mínima solo en la terminación. En este caso será:

logfile.log CLOSE_WRITE,CLOSE

Se puede usar un solo comando de espera para esperar múltiples procesos:

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

La cadena de salida de inotifywait le dirá qué proceso finalizó. Esto solo funciona con archivos 'reales', no con algo en / proc /

Burghard Hoffmann
fuente
0

En un sistema como OSX, es posible que no tenga pgrep, por lo que puede probar esta aproximación cuando busque procesos por nombre:

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

El $símbolo al final del nombre del proceso garantiza que grep solo coincida con nombre_proceso al final de la línea en la salida ps y no a sí mismo.

Pierz
fuente
Horrible: puede haber múltiples procesos con ese nombre en algún lugar de la línea de comando , incluido su propio grep. En lugar de redirigir a /dev/null, -qdebe usarse con grep. Otra instancia del proceso puede haber comenzado mientras su ciclo estaba durmiendo y nunca lo sabrá ...
Mikhail T.
No estoy seguro de por qué seleccionó esta respuesta como 'Horrible', ya que otros han sugerido un enfoque similar. Si bien la -qsugerencia es válida, como mencioné en mi respuesta específicamente, la terminación $significa que grep no coincidirá con el nombre "en algún lugar de la línea de comando" ni con el mismo. ¿Realmente lo has probado en OSX?
Pierz
0

La solución de Rauno Palosaari Timeout in Seconds Darwines una excelente solución para un sistema operativo tipo UNIX que no tiene GNU tail(no es específico para Darwin). Pero, dependiendo de la antigüedad del sistema operativo tipo UNIX, la línea de comandos que se ofrece es más compleja de lo necesario y puede fallar:

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

En al menos un UNIX antiguo, el lsofargumento +r 1m%sfalla (incluso para un superusuario):

lsof: can't read kernel name list.

El m%ses una especificación de formato de salida. Un postprocesador más simple no lo requiere. Por ejemplo, el siguiente comando espera en PID 5959 hasta cinco segundos:

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

En este ejemplo, si el PID 5959 sale por sí solo antes de que transcurran los cinco segundos, ${?}es 0. Si no ${?}regresa1 después de cinco segundos.

Vale la pena señalar expresamente que en +r 1, 1es el intervalo de sondeo (en segundos), por lo que puede cambiarse para adaptarse a la situación.

kbulgrien
fuente