May 262014
 
Artículo Perl

Un script perl normalmente comienza leyendo información de una serie de fuentes: archivos del disco, bases de datos, conexiones de red, teclado… Después procesa la información, y por último entrega el resultado escribiéndolo en un archivo, guardándolo en una base de datos, enviándolo por la red, presentándolo en pantalla,….

En ocasiones, esta tarea se puede dividir en subtareas, cada una de las cuales sigue el mismo esquema de leer/procesar/escribir la información.

En este caso, se puede obtener una mejora sustancial del rendimiento del script si estas tareas se ejecutan en paralelo, porque mientras unas subtareas pueden estar esperando recibir información de la red, otras pueden estar utilizando la CPU para realizar el proceso de los datos, y otras pueden estar esperando que finalice una operación de entrada/salida al disco.

En este artículo explicamos como programar un script perl para que ejecute varios subprocesos simultáneos y aproveche así al máximo los recursos disponibles.

La función fork()

La funcionalidad que buscamos se obtiene mediante el uso de la función fork(). Esta función crea un subproceso que se ejecuta de manera independiente, en paralelo con el programa principal. El subproceso hereda todo el entorno del programa que lo ha creado, incluyendo las variables que han sido definidas y sus valores, los manejadores de ficheros, conexiones a bases de datos, etc…

Ejemplo:

#!/usr/bin/perl
#

    my $pid;
    $pid = fork;
    die "La llamada a fork ha fallado: $!" unless defined $pid;
    if ($pid) {

        # Código ejecutado por el padre
        ...

    } else {

        # Código ejecutado por el hijo
        ...

        exit; 
    }

    # Espera a que finalice el proceso hijo
    1 while (wait() != -1);

    # Continúa la ejecución del proceso padre
    ...

La llamada a fork devuelve al proceso padre un identificador del proceso hijo, y devuelve al proceso hijo el valor cero. Esto permite dividir, mediante la sentencia “if ($pid)” el código que ejecuta el padre del que ejecuta el hijo.

Este mecanismo se puede utlizar para que un mismo proceso padre cree múltiples hijos, cada uno de los cuales estará identificado por un valor distinto de $pid.

Por último, el proceso padre puede esperar a que termine la ejecución de todos sus hijos mediante la llamada a “wait()”.

Esperando a que termine el hijo

Normalmente, el proceso padre debe esperar hasta que todos los procesos hijo finalizan su ejecución, antes de finalizar él mismo. Esto se hace mediante una llamada a wait(), como se puede ver en el ejemplo anterior. wait() suspende la ejecución del proceso padre hasta que uno de los procesos hijo finaliza, y devuelve el PID del hijo que ha finalizado. Cuando ya no quedan procesos hijo en ejecución, wait() devuelve -1.

También existe una función waitpid() que permite al proceso padre quedar a la espera de que finalice un proceso hijo en concreto. La función waitpid() recibe como primer argumento el ID del proceso hijo a esperar, y como segundo argumento o bien un cero, para suspender la ejecución del proceso padre, o bien la constante WNOHANG para permitir que el padre continúe ejecutándose.

# Incluir el módulo POSIX en donde está definida la constante WNOHANG
use POSIX ":sys_wait_h";
 
# Está ejecutándose todavía el proceso hijo ?
if (waitpid($pid,WNOHANG) == 0) { # Realizar una llamada no bloqueante a waitpid
    # Si waitpid() devuelve cero, es que el hijo todavía está en ejecución
    # Realizar algún proceso en paralelo
    ...
    # Quedar a la espera de que finalice el proceso hijo
    waitpid($pid,0);
}

En lugar del PID de un proceso hijo en concreto, se puede pasar el valor -1 como primer argumento de waitpid(). En este caso, waitpid() queda a la espera de que finalice cualquiera de los procesos hijo en ejecución, y devuelve el PID del proceso hijo que haya finalizado. Es decir, que una llamada a waitpid(-1,0) es equivalente a una llamada a wait().

En la llamada a waitpid, se puede utilizar también como segundo argumento la constante WNOHANG. En este caso la llamada no suspende la ejecución del proceso padre, sino que devuelve inmediatamente un valor que puede ser:

  • cero si no hay ningún hijo que haya finalizado desde la última llamada a waitpid
  • el PID de uno de los procesos hijo que ha finalizado. Si hay más de un proceso que ha finalizado, sucesivas llamadas a waitpid van devolviendo los PID de los mismos.
  • -1 si no quedan procesos hijo en ejecución, y las sucesivas llamadas a waitpid han devuelto ya todos los PIDs de los hijos que han finalizado.

Paso de datos entre el proceso padre y el proceso hijo

fork() crea una copia del entorno del proceso padre, de manera que las variables que estaban definidas en el proceso padre también están definidas en el proceso hijo, con los mismos valores. Pero las modificaciones que realice el proceso hijo a los valores de una variable no son visibles por el proceso padre.

Para poder intercambiar datos entre el proceso padre y el proceso hijo, se puede establecer un canal de comunicación mediante el uso de “pipes”:

Cuando la función open() es llamada con el argumento “-|”, funciona igual que la función fork(), creando un subproceso. Pero además, devuelve un manejador de entrada/salida asociado a la salida estándar (STDOUT) del proceso hijo. De este modo, lo que escriba el proceso hijo por la salida estándar puede ser leído por el proceso padre.

Ejemplo 1. Comunicación en el sentido hijo -> padre:

my $mensaje;
if (open(FROM_HIJO, "-|")) {
    # Este código es ejecutado por el proceso padre
    $mensaje = <FROM_HIJO>;
}
else {
    # Este código es ejecutado por el proceso hijo
    print STDOUT "Hola, soy el proceso hijo";
    exit;
}
# El padre espera a que el hijo finalice
wait();
print "Mi hijo me ha dicho: " . $mensaje . "\n";

De la misma manera, se puede utilizar la función open() con el argumento “|-“. En este caso, se abre un canal de comunicación unidireccional en el sentido padre -> hijo.

Ejemplo 2. Comunicación en el sentido padre -> hijo:

my $mensaje;
if (open(TO_HIJO, "|-")) {
    # Este código es ejecutado por el proceso padre
    print TO_HIJO "Hola, soy el proceso padre";
}
else {
    # Este código es ejecutado por el proceso hijo
    my $mensaje = <STDIN>;
    print STDOUT "Mi padre dice: $mensaje\n";
    exit;
}
# El padre espera a que el hijo finalice
wait();

Comunicación bidireccional con pipes

Si necesitamos establecer una comunicación en ambos sentidos, podemos crear explícitamente dos canales de comunicación unidireccionales con la función pipe(), y crear el subproceso con la función fork():

use IO::Handle;

pipe(FROM_PADRE, TO_HIJO)     or die "pipe: $!";
pipe(FROM_HIJO,  TO_PADRE)    or die "pipe: $!";

# Establecer la opción "autoflush" en ambos canales, para que lo que se escriba
# En cada canal esté inmediatamente disponible para su lectura
TO_HIJO->autoflush(1);
TO_PADRE->autoflush(1);

if ($pid = fork) {
    # Este código es ejecutado por el proceso padre
    close FROM_PADRE; close TO_PADRE;
    print TO_HIJO "Hola, soy el padre con PID $$\n";
    chomp($line = <FROM_HIJO>);
    print "El proceso padre con PID $$ ha recibido un mensaje: `$line'\n";
    close FROM_HIJO; close TO_HIJO;
    waitpid($pid,0);
} else {
    # Este código es ejecutado por el proceso hijo
    die "Error ejecutando fork: $!" unless defined $pid;
    close FROM_HIJO; close TO_HIJO;
     chomp($line = <FROM_PADRE>);
     print "El proceso hijo con PID $$ ha recibido un mensaje: `$line'\n";
     print TO_PADRE "Hola, soy el hijo con PID $$\n";
     close FROM_PADRE; close TO_PADRE;
     exit;
}

 Publicado por en 2:04 pm

 Deja un comentario

(requerido)

(requerido)