2010-05-15 12 views
12

Tengo un script de automatización paralelizado que necesita llamar a muchos otros scripts, algunos de los cuales cuelgan porque (incorrectamente) esperan la entrada estándar o esperan por otras cosas que no van a suceder. Eso no es un gran problema porque los atrapo con alarm. El truco es cerrar esos procesos de nieta colgados cuando el niño se apaga. Pensé que varios conjuros de SIGCHLD, espera y grupos de procesos podrían hacer el truco, pero todos bloquean y los nietos no son cosechados.¿Cómo debo limpiar los procesos de nieta colgados cuando se dispara una alarma en Perl?

Mi solución, que funciona, simplemente no parece que sea la solución correcta. No estoy especialmente interesado en la solución de Windows todavía, pero eventualmente necesitaré eso también. El mío solo funciona para Unix, lo cual está bien por ahora.

escribí un pequeño script que lleva el número de niños paralelas simultáneas para funcionar y el número total de las horquillas:

$ fork_bomb <parallel jobs> <number of forks> 

$ fork_bomb 8 500 

Esto probablemente llegarán al límite de procesos por usuario dentro de un par de minutos. Muchas soluciones que he encontrado solo te dicen que aumentes el límite del proceso por usuario, pero necesito que esto se ejecute unas 300,000 veces, así que eso no va a funcionar. Del mismo modo, las sugerencias para volver a ejecutar, etc. para borrar la tabla de procesos no son lo que necesito. Me gustaría solucionar el problema en lugar de pegarle una cinta adhesiva.

Arrastro la tabla de proceso buscando los procesos secundarios y cierro los procesos colgados individualmente en el controlador SIGALRM, que debe morir porque el resto del código real no tiene ninguna esperanza de éxito después de eso. El rastreo kludgey a través de la tabla de procesos no me molesta desde una perspectiva de rendimiento, pero no le importaría no hacerlo:

use Parallel::ForkManager; 
use Proc::ProcessTable; 

my $pm = Parallel::ForkManager->new($ARGV[0]); 

my $alarm_sub = sub { 
     kill 9, 
      map { $_->{pid} } 
      grep { $_->{ppid} == $$ } 
      @{ Proc::ProcessTable->new->table }; 

     die "Alarm rang for $$!\n"; 
     }; 

foreach (0 .. $ARGV[1]) 
    { 
    print "."; 
    print "\n" unless $count++ % 50; 

    my $pid = $pm->start and next; 

    local $SIG{ALRM} = $alarm_sub; 

    eval { 
     alarm(2); 
     system "$^X -le '<STDIN>'"; # this will hang 
     alarm(0); 
     }; 

    $pm->finish; 
    } 

Si desea ejecutar fuera de los procesos, sacar la kill.

pensé que el establecimiento de un grupo de procesos que trabajaría para que pudiera matar a todo juntos, pero que bloquea:

my $alarm_sub = sub { 
     kill 9, -$$; # blocks here 
     die "Alarm rang for $$!\n"; 
     }; 

foreach (0 .. $ARGV[1]) 
    { 
    print "."; 
    print "\n" unless $count++ % 50; 

    my $pid = $pm->start and next; 
    setpgrp(0, 0); 

    local $SIG{ALRM} = $alarm_sub; 

    eval { 
     alarm(2); 
     system "$^X -le '<STDIN>'"; # this will hang 
     alarm(0); 
     }; 

    $pm->finish; 
    } 

Lo mismo con POSIX 's setsid no funcionó bien, y creo que en realidad rompió las cosas de una manera diferente ya que no estoy realmente demonizando esto.

Curiosamente, Parallel::ForkManager 's pasa demasiado tarde para el mismo código de limpieza: los nietos aparentemente ya están desasociados de los procesos hijo en ese punto.

+0

¿Hay algún motivo por el que no cierre STDIN en los niños? – jrockway

+0

Eso también podría ser una buena idea. Tendré que pensar en eso, aunque estaba considerando manejar algunos de los casos malos con algo como Esperar para que hagan lo que tienen que hacer. –

+0

Consulte la respuesta larga a continuación. Pero, básicamente, creo que necesitas más control sobre la situación que Parallel :: ForkManager te permite, lo que significa que tienes que hacer tu propia. – jrockway

Respuesta

1

Brian - es un poco crudo y no idiomática, pero uno de los enfoques que he visto tomado es la siguiente: en cualquier momento que tenedor, que:

  1. Dale el proceso hijo una primera prueba "-id" parámetro para el programa, con un valor algo único (por PID): un buen candidato podría ser una indicación de tiempo de hasta milisegundos + PID de los padres.

  2. El padre registra el PID secundario y un valor de -id en un registro (idealmente persistente) junto con el tiempo de espera/tiempo de eliminación deseado.

Entonces tener un proceso observador (ya sea el abuelo última, o un proceso separado con el mismo UID) simplemente ciclo a través del registro periódicamente, y comprobar que los procesos que necesitan ser matado (según a matar-tiempo) todavía están dando vueltas (al hacer coincidir el valor del parámetro PID y "-id" en el registro con los PID y la línea de comando en la tabla de proceso); y envíe la señal 9 a dicho proceso (o sea amable y trate de matar gentilmente primero tratando de enviar la señal 2).

El parámetro "-id" exclusivo está destinado obviamente a evitar la muerte de algún proceso inocente que simplemente reutilizó el PID de un proceso anterior por coincidencia, lo que probablemente se deba a la escala que mencionó.

La idea de un registro ayuda con el problema de los nietos "ya desasociados" ya que ya no depende del sistema para mantener la asociación padre/hijo por usted.

Esto es algo así como la fuerza bruta, pero como nadie respondió aún, pensé que iba a pensar en mi idea de 3 centavos a tu manera.

+0

Esto es una curita, no una solución. Sé sobre las cosas de la fuerza bruta que puedo hacer, pero en realidad estoy tratando de resolver el problema real sin crear todo tipo de acoplamiento extraño dentro del programa. –

8

He leído la pregunta unas cuantas veces, y creo que de alguna manera consigo lo que está tratando de hacer. Usted tiene un script de control. Este script genera hijos para hacer algunas cosas, y estos hijos engendran a los nietos para realmente hacer el trabajo. El problema es que los nietos pueden ser demasiado lento (esperando STDIN, o lo que sea), y desea matarlos. Además, si hay un nieto lento, quiere que muera todo el hijo (matando a los otros nietos, si es posible).

Por lo tanto, traté de implementar esto de dos maneras. La primera era hacer que el padre engendrara un hijo en una nueva sesión de UNIX, establecer un temporizador durante unos pocos segundos y matar toda la sesión secundaria cuando se disparaba el temporizador. Esto hizo que el padre se responsabilizara tanto del hijo como de los nietos . Tampoco funcionó bien.

La siguiente estrategia fue hacer que el padre engendre el hijo, y luego hacer que el niño sea responsable de administrar a los nietos. Sería establecer un temporizador para cada nieto, y matarlo si el proceso no tenía salido por el tiempo de caducidad. Esto funciona muy bien, así que aquí está el código.

Usaremos EV para administrar los elementos secundarios y temporizadores, y AnyEvent para la API . (Puede probar otro bucle de evento AnyEvent, como Evento o POE. Pero sé que EV maneja correctamente la condición en la que un niño sale de antes de decirle al bucle que lo monitorice, lo que elimina las molestas condiciones de raza que otros bucles son vulnerables a .)

#!/usr/bin/env perl 

use strict; 
use warnings; 
use feature ':5.10'; 

use AnyEvent; 
use EV; # you need EV for the best child-handling abilities 

necesitamos hacer un seguimiento de los observadores de los niños:

# active child watchers 
my %children; 

entonces tenemos que escribir una función para iniciar a los niños. Las cosas que generan los padres se llaman hijos, y las cosas que los hijos engendran se llaman trabajos.

sub start_child([email protected]) { 
    my ($on_success, $on_error, @jobs) = @_; 

Los argumentos son una devolución de llamada a ser llamado cuando el niño completa éxito (es decir, su empleo también fueron un éxito), una devolución de llamada cuando el niño no se ha completado satisfactoriamente, y luego una lista de coderef empleos correr.

En esta función, tenemos que bifurcar. En el padre, que configurar un observador niño de monitorizar los niños:

if(my $pid = fork){ # parent 
     # monitor the child process, inform our callback of error or success 
     say "$$: Starting child process $pid"; 
     $children{$pid} = AnyEvent->child(pid => $pid, cb => sub { 
      my ($pid, $status) = @_; 
      delete $children{$pid}; 

      say "$$: Child $pid exited with status $status"; 
      if($status == 0){ 
       $on_success->($pid); 
      } 
      else { 
       $on_error->($pid); 
      } 
     }); 
    } 

en el niño, que en realidad ejecutar los trabajos. Esto implica un poco de configuración , sin embargo.

En primer lugar, nos olvidamos de los vigilantes de los hijos de los padres, ya que no hace que tenga sentido para que el niño se entere de que sus hermanos están saliendo. (Tenedor es divertido, porque se hereda todos estado de los padres, aun cuando ese no tiene ningún sentido en absoluto.)

else { # child 
     # kill the inherited child watchers 
     %children =(); 
     my %timers; 

También tenemos que saber cuando se hacen todos los puestos de trabajo, y si o no todos fueron un éxito. Utilizamos una variable condicional de recuento en para determinar cuándo ha salido todo. Incrementamos al inicio, y decremento a la salida, y cuando el conteo es 0, sabemos que todo está hecho.

También tengo un booleano para indicar el estado de error. Si un proceso sale con un estado distinto de cero, el error va a 1. De lo contrario, se queda 0. Es posible que desee mantener más el estado de esta :)

 # then start the kids 
     my $done = AnyEvent->condvar; 
     my $error = 0; 

     $done->begin; 

(También iniciar la cuenta en 1 de modo que si hay 0 trabajos, nuestro proceso todavía sale.)

Ahora tenemos que bifurcar para cada trabajo y ejecutar el trabajo. En el padre, nosotros hacemos algunas cosas. Incrementamos el condvar. Configuramos un temporizador para matar al niño si es demasiado lento. Y configuramos un vigilante de niños, para que podamos informarnos del estado de salida del trabajo al .

for my $job (@jobs) { 
      if(my $pid = fork){ 
       say "[c] $$: starting job $job in $pid"; 
       $done->begin; 

       # this is the timer that will kill the slow children 
       $timers{$pid} = AnyEvent->timer(after => 3, interval => 0, cb => sub { 
        delete $timers{$pid}; 

        say "[c] $$: Killing $pid: too slow"; 
        kill 9, $pid; 
       }); 

       # this monitors the children and cancels the timer if 
       # it exits soon enough 
       $children{$pid} = AnyEvent->child(pid => $pid, cb => sub { 
        my ($pid, $status) = @_; 
        delete $timers{$pid}; 
        delete $children{$pid}; 

        say "[c] [j] $$: job $pid exited with status $status"; 
        $error ||= ($status != 0); 
        $done->end; 
       }); 
      } 

Uso del temporizador es un poco más fácil que la alarma, ya que lleva estado con ella. Cada temporizador sabe qué proceso matar, y es fácil cancelar el temporizador cuando el proceso finaliza correctamente; simplemente lo eliminamos del hash.

Ese es el padre (del niño). El niño (del niño, o el trabajo ) es muy simple:

  else { 
       # run kid 
       $job->(); 
       exit 0; # just in case 
      } 

Usted podría también cerca de la entrada estándar aquí, si quería.

Ahora, después de que todos los procesos se hayan engendrado, los esperamos a todos salen esperando al condvar.El ciclo de eventos se monior los niños y contadores de tiempo, y hacer lo correcto para nosotros:

 } # this is the end of the for @jobs loop 
     $done->end; 

     # block until all children have exited 
     $done->recv; 

Entonces, cuando todos los niños han salido, podemos hacer cualquier limpieza trabajo queremos, como:

 if($error){ 
      say "[c] $$: One of your children died."; 
      exit 1; 
     } 
     else { 
      say "[c] $$: All jobs completed successfully."; 
      exit 0; 
     } 
    } # end of "else { # child" 
} # end of start_child 

OK, entonces ese es el trabajo de niño y nieto /. Ahora solo tenemos que escribir el padre, que es mucho más fácil.

Al igual que el niño, vamos a utilizar un condvar contar para esperar nuestros niños.

# main program 
my $all_done = AnyEvent->condvar; 

Necesitamos algunos trabajos para hacer. Aquí hay una que siempre tiene éxito, y que será exitosa si se pulsa el retorno, pero fallará si acaba de dejarlo ser matado por el temporizador:

my $good_grandchild = sub { 
    exit 0; 
}; 

my $bad_grandchild = sub { 
    my $line = <STDIN>; 
    exit 0; 
}; 

Así que sólo tenemos que empezar el niño trabajos. Si recuerda el modo en la parte superior de start_child, se requieren dos devoluciones de llamada, un error de devolución de llamada, y una devolución de llamada exitosa. Los configuraremos; el error de devolución de llamada imprimirá "no está bien" y disminuirá la condición, y la devolución de llamada satisfactoria imprimirá "ok" y hará lo mismo. Muy simple.

my $ok = sub { $all_done->end; say "$$: $_[0] ok" }; 
my $nok = sub { $all_done->end; say "$$: $_[0] not ok" }; 

Entonces podemos empezar un grupo de niños con aún más nietos empleos:

say "starting..."; 

$all_done->begin for 1..4; 
start_child $ok, $nok, ($good_grandchild, $good_grandchild, $good_grandchild); 
start_child $ok, $nok, ($good_grandchild, $good_grandchild, $bad_grandchild); 
start_child $ok, $nok, ($bad_grandchild, $bad_grandchild, $bad_grandchild); 
start_child $ok, $nok, ($good_grandchild, $good_grandchild, $good_grandchild, $good_grandchild); 

Dos de ellos será el tiempo de espera, y dos tendremos éxito. Sin embargo, si presiona enter mientras se están ejecutando, todos podrían tener éxito.

De todos modos, una vez que los han comenzado, sólo tenemos que esperar a que se acabado:

$all_done->recv; 

say "...done"; 

exit 0; 

Y ese es el programa.

Una cosa que no estamos haciendo que paralelo :: ForkManager hace es "Limitación de velocidad" nuestros tenedores de manera que sólo n los niños se están ejecutando en un momento . Esto es bastante fácil de implementar de forma manual, sin embargo:

use Coro; 
use AnyEvent::Subprocess; # better abstraction than manually 
          # forking and making watchers 
use Coro::Semaphore; 

my $job = AnyEvent::Subprocess->new(
    on_completion => sub {}, # replace later 
    code   => sub { the child process }; 
) 

my $rate_limit = Coro::Semaphore->new(3); # 3 procs at a time 

my @coros = map { async { 
    my $guard = $rate_limit->guard; 
    $job->clone(on_completion => Coro::rouse_cb)->run($_); 
    Coro::rouse_wait; 
}} ({ args => 'for first job' }, { args => 'for second job' }, ...); 

# this waits for all jobs to complete 
my @results = map { $_->join } @coros; 

La ventaja aquí es que usted puede hacer otras cosas mientras sus hijos se están ejecutando - acaba de generar más hilos con async antes de hacer el bloqueo unirse. También tiene mucho más control sobre los hijos con AnyEvent :: Subproceso - puede ejecutar el hijo en una Pty y alimentar stdin (como con Esperar), y puede capturar su stdin y stdout y stderr, o puedes ignorar esas cosas, o lo que sea. Llegas a decide, no un autor de módulo que está tratando de hacer las cosas "simples".

De todos modos, espero que esto ayude.

+0

Además, puede cortar y pegar el código en un script y ejecutarlo. Solo elimina el texto. – jrockway

+1

+1 para una respuesta similar a un libro! – Kyle

0

Tengo que resolver este mismo problema en un módulo I've been working on.No estoy completamente satisfecho con todo mi solución (s) o bien, pero lo que funciona generalmente en UNIX es

  1. cambio de grupo de proceso de un niño
  2. nietos de regeneración según sea necesario
  3. el cambio de grupo de procesos del niño de nuevo (por ejemplo, de nuevo a su valor original)
  4. señal de grupo de proceso de los nietos para matar a los nietos

algo como:

use Time::HiRes qw(sleep); 

sub be_sleepy { sleep 2 ** (5 * rand()) } 
$SIGINT = 2; 

for (0 .. $ARGV[1]) { 
    print "."; 
    print "\n" unless ++$count % 50; 
    if (fork() == 0) { 
     # a child process 
     # $ORIGINAL_PGRP and $NEW_PGRP should be global or package or object level vars 
     $ORIGINAL_PGRP = getpgrp(0); 
     setpgrp(0, $$); 
     $NEW_PGRP = getpgrp(0); 

     local $SIG{ALRM} = sub { 
      kill_grandchildren(); 
      die "$$ timed out\n"; 
     }; 

     eval { 
      alarm 2; 
      while (rand() < 0.5) { 
       if (fork() == 0) { 
        be_sleepy(); 
       } 
      } 
      be_sleepy(); 
      alarm 0; 
      kill_grandchildren(); 
     }; 

     exit 0; 
    } 
} 

sub kill_grandchildren { 
    setpgrp(0, $ORIGINAL_PGRP); 
    kill -$SIGINT, $NEW_PGRP; # or kill $SIGINT, -$NEW_PGRP 
} 

Esto no es completamente infalible. Los nietos pueden cambiar sus grupos de procesos o atrapar señales.

Nada de esto funcionará en Windows, por supuesto, pero digamos que TASKKILL /F /T es su amigo.


Actualización: Esta solución no maneja (para mí, al menos) el caso cuando el proceso hijo invoca system "perl -le '<STDIN>'". Para mí, esto suspende inmediatamente el proceso e impide que el SIGALRM se active y se ejecute el controlador SIGALRM. ¿Está cerrando STDIN la única solución?

+0

Esto tampoco funciona para mi caso particular. Tengo que manejar ese caso , que es la razón más común para un proceso bloqueado en mi aplicación. Mi pensamiento actual sobre eso es abrir una tubería bidireccional en su lugar y luego cerrar la entrada (de niño a nieto) inmediatamente. –

Cuestiones relacionadas