Pipes en C – Linux

| 2013-10-10 | 1 Comentario »

Tuberías

Una tubería (pipe, cauce o ‘|’) consiste en una cadena de procesos conectados de forma tal que la salida de cada elemento de la cadena es la entrada del próximo. Permiten la comunicación y sincronización entre procesos. Es común el uso de buffer de datos entre elementos consecutivos.

Una tubería es unidireccional.

La utilización de tuberías mediante el uso de la shell es “el pan nuestro de cada día”, cualquier administrador de sistemas medianamente preparado encadena comandos y comandos mediante tuberías de forma natural:

cat /etc/passwd | grep bash | wc –lines

Los comandos “cat”, “grep” y “wc” se lanzan en paralelo y el primero va “alimentando” al segundo, que posteriormente “alimenta” al tercero. Al final tenemos una salida filtrada por esas dos tuberías. Las tuberías empleadas son destruidas al terminar los procesos que las estaban utilizando.

Utilizar tuberías en C es también bastante sencillo, si bien a veces es necesario emplear lápiz y papel para no liarse con tanto descriptor de fichero. Ya vimos anteriormente que para abrir un fichero, leer y escribir en él y cerrarlo, se empleaba su descriptor de fichero. Una tubería tiene en realidad dos descriptores de fichero: uno para el extremo de escritura y otro para el extremo de lectura. Como los descriptores de fichero de UNIX son simplemente enteros, un pipe o tubería no es más que un array de dos enteros:

int tuberia[2];

Para crear la tubería se emplea la función pipe(), que abre dos descriptores de fichero y almacena su valor en los dos enteros que contiene el array de descriptores de fichero. El primer descriptor de fichero es abierto como O_RDONLY, es decir, sólo puede ser empleado para lecturas. El segundo se abre como O_WRONLY, limitando su uso a la escritura. De esta manera se asegura que el pipe sea de un solo sentido: por un extremo se escribe y por el otro se lee, pero nunca al revés. Ya hemos dicho que si se precisa una comunicación “full-duplex”, será necesario crear dos tuberías para ello.

int tuberia[2];

pipe(tuberia);

Una vez creado un pipe, se podrán hacer lecturas y escrituras de manera normal, como si se tratase de cualquier fichero. Sin embargo, no tiene demasiado sentido usar un pipe para uso propio, sino que se suelen utilizar para intercambiar datos con otros procesos. Como ya sabemos, un proceso hijo hereda todos los descriptores de ficheros abiertos de su padre, por lo que la comunicación entre el proceso padre y el proceso hijo es bastante cómoda mediante una tubería. Para asegurar la unidireccionalidad de la tubería, es necesario que tanto padre como hijo cierren los respectivos descriptores de ficheros.

El proceso padre y su hijo comparten datos mediante una tubería.

La tubería “p” se hereda al hacer el fork() que da lugar al proceso hijo, pero es necesario que el padre haga un close() de p[0] (el lado de lectura de la tubería), y el hijo haga un close() de p[1] (el lado de escritura de la tubería). Una vez hecho esto, los dos procesos pueden emplear la tubería para comunicarse (siempre unidireccionalmente), haciendo write() en p[1] y read() en p[0], respectivamente. Veamos un ejemplo de este tipo de situación:

#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>

#define SIZE 512

int main( int argc, char **argv )
{
  pid_t pid;
  int p[2], readbytes;
  char buffer[SIZE];

  pipe( p );

  if ( (pid=fork()) == 0 )
  { // hijo
    close( p[1] ); /* cerramos el lado de escritura del pipe */

    while( (readbytes=read( p[0], buffer, SIZE )) > 0)
      write( 1, buffer, readbytes );

    close( p[0] );
  }
  else
  { // padre
    close( p[0] ); /* cerramos el lado de lectura del pipe */

    strcpy( buffer, "Esto llega a traves de la tuberia\n" );
    write( p[1], buffer, strlen( buffer ) );

    close( p[1] );
  }
  waitpid( pid, NULL, 0 );
  exit( 0 );
}

Veamos ahora cómo implementar una comunicación bidireccional entre dos procesos mediante tuberías. Como ya hemos venido diciendo, será preciso crear dos tuberías diferentes (a[2] y b[2]), una para cada sentido de la comunicación. En cada proceso habrá que cerrar descriptores de ficheros diferentes. Vamos a emplear el pipe a[2] para la comunicación desde el padre al hijo, y el pipe b[2] para comunicarnos desde el hijo al padre. Por lo tanto, deberemos cerrar:

  • En el padre:
    • el lado de lectura de a[2].
    • el lado de escritura de b[2].
  • En el hijo:
    • el lado de escritura de a[2].
    • el lado de lectura de b[2].

Dos procesos se comunican bidireccionalmente con dos tuberías:

#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>

#define SIZE 512

int main( int argc, char **argv )
{
  pid_t pid;
  int a[2], b[2], readbytes;
  char buffer[SIZE];

  pipe( a );
  pipe( b );

  if ( (pid=fork()) == 0 )
  { // hijo
    close( a[1] ); /* cerramos el lado de escritura del pipe */
    close( b[0] ); /* cerramos el lado de lectura del pipe */

    while( (readbytes=read( a[0], buffer, SIZE ) ) > 0)
      write( 1, buffer, readbytes );
    close( a[0] );

    strcpy( buffer, "Soy tu hijo hablandote por
            la otra tuberia.\n" );
    write( b[1], buffer, strlen( buffer ) );
    close( b[1] );
  }
  else
  { // padre
    close( a[0] ); /* cerramos el lado de lectura del pipe */
    close( b[1] ); /* cerramos el lado de escritura del pipe */

    strcpy( buffer, "Soy tu padre hablandote
            por una tuberia.\n" );
    write( a[1], buffer, strlen( buffer ) );
    close( a[1]);

    while( (readbytes=read( b[0], buffer, SIZE )) > 0)
      write( 1, buffer, readbytes );
    close( b[0]);
  }
  waitpid( pid, NULL, 0 );
  exit( 0 );
}

Linux también soporta tuberías con nombre, denominadas habitualmente FIFOs, (First in First out) debido a que las tuberías funcionan como una cola: el primero en entrar es el primero en salir. A diferencia de las tuberías sin nombre, los FIFOs no tiene carácter temporal sino que perduran aunque dos procesos hayan dejado de usarlos. Para crear un FIFO se puede utilizar el comando “mkfifo“ o bien llamar a la función de C mkfifo():

int mkfifo(const char *pathname, mode_t mode);

Esta función recibe dos parámetros: “pathname” indica la ruta en la que queremos crear el FIFO, y “mode” indica el modo de acceso a dicho FIFO. Cualquier proceso es capaz de utilizar un FIFO siempre y cuando tengan los privilegios necesarios para ello.

Fuente: txipi

Acerca del autor: Rodrigo Paszniuk

Ingeniero Informático, amante de la tecnología, la música, el ciclismo y aprender cosas nuevas.

Posts Relacionados

  • Developers SO Sistemas Operativos preferidos por los developers
  • Instalar Tomcat 7 en CentOS 6
  • RPC (Remote Procedure Call) en C – Linux
  • Sockets en C (Parte II) – Linux



SEGUÍNOS EN FACEBOOK


GITHUB