Comunicación entre procesos mediante tuberías en Linux

Tuberías Anónimas

En los sistemas Linux, las tuberías (pipes) representan uno de los mecanismos más fundamentales para la comunicación entre procesos (IPC). Su funcionamiento se asemeja a un conducto físico: un extremo escribe datos y el otro los lee, estableciendo una comunicación semidúplex. Las tuberías son, en esencia, un búfer en memoria creado por el kernel que actúa como archivo temporal.

La tubería anónima (pipe) es la variante básica y está limitada a procesos que comparten un vínculo de parentesco, típicamente un proceso padre y su hijo. Su ciclo de vida está ligado al proceso que la creó.

Perspectiva del Kernel

Cuando un proceso invoca la función pipe(), el kernel asigna un área de memoria circular. Esta operación se traduce en la creación de dos descriptores de archivo: uno para lectura (fd[0]) y otro para escritura (fd[1]). Internamente, el kernel gestiona un único objeto struct pipe_inode_info asociado a ambos descriptores.

La comunicación efectiva requiere que dos procesos compartan este mismo recurso. La secuencia típica es:

  1. El proceso padre crea la tubería, obteniendo ambos descriptores.
  2. El proceso padre genera un proceso hijo mediante fork(). El hijo hereda copias de los descriptores de archivo, por lo que ambos procesos apuntan a la misma tubería en el kernel.
  3. Para establecer un flujo de comunicación unidireccional (del padre al hijo, por ejemplo), cada proceso cierra el descriptor que no utilizará (el padre cierra el de escritura, el hijo cierra el de lectura, o viceversa). Esto es crucial para evitar condiciones de carrera y garantizar la integridad de la comunicación.

Implementación con Código

La siguiente demostración muestra un intercambio de mensajes simple. El proceso hijo actúa como escritor y el proceso padre como lector. Observen los cambios en la estructrua del código y los nombres de variables respecto a una implementación convencional.

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/wait.h>

#define BUFFER_SIZE 256

void escritor(int descriptor_escritura) {
    const char *mensajes[] = {
        "Primer paquete de datos.",
        "Segunda transmisión.",
        "Mensaje final del escritor."
    };
    int contador = 0;
    
    while (contador < 3) {
        ssize_t bytes_escritos = write(descriptor_escritura, mensajes[contador], strlen(mensajes[contador]));
        if (bytes_escritos == -1) {
            perror("write");
            break;
        }
        printf("[Escritor PID %d] Envió: \"%s\" (%zd bytes)\n", getpid(), mensajes[contador], bytes_escritos);
        contador++;
        usleep(500000); // Retraso para observar el flujo
    }
}

void lector(int descriptor_lectura) {
    char buffer_recepcion[BUFFER_SIZE];
    ssize_t bytes_leidos;
    
    while ((bytes_leidos = read(descriptor_lectura, buffer_recepcion, BUFFER_SIZE - 1)) > 0) {
        buffer_recepcion[bytes_leidos] = '\0'; // Asegurar terminación nula
        printf("[Lector PID %d] Recibió: \"%s\"\n", getpid(), buffer_recepcion);
    }
    
    if (bytes_leidos == 0) {
        printf("[Lector] El escritor cerró el extremo. Fin de la lectura.\n");
    } else if (bytes_leidos == -1) {
        perror("read");
    }
}

int main(void) {
    int canal_fds[2]; // [0]=lectura, [1]=escritura
    pid_t pid_hijo;

    if (pipe(canal_fds) == -1) {
        perror("pipe");
        exit(EXIT_FAILURE);
    }

    pid_hijo = fork();
    if (pid_hijo == -1) {
        perror("fork");
        exit(EXIT_FAILURE);
    }

    if (pid_hijo == 0) { // Proceso hijo
        close(canal_fds[0]); // Cerrar descriptor de lectura (no lo usa)
        escritor(canal_fds[1]);
        close(canal_fds[1]); // Cerrar descriptor de escritura al terminar
        exit(EXIT_SUCCESS);
    } else { // Proceso padre
        close(canal_fds[1]); // Cerrar descriptor de escritura (no lo usa)
        lector(canal_fds[0]);
        close(canal_fds[0]); // Cerrar descriptor de lectura al terminar
        wait(NULL); // Esperar la finalización del hijo
    }
    return 0;
}

Comportamientos de Bloqueo y Atomicidad

El comportamianto de bloqueo de read y write depende del estado de la tubería:

  • Lectura bloqueante: Una llamada a read() se bloqueará si la tubería está vacía y hay al menos un descriptor de escritura abierto.
  • Escritura bloqueante: Una llamada a write() se bloqueará si la tubería está llena (su capacidad es típicamente 64KB en Linux).
  • Fin de archivo (EOF): Si todos los descriptores de escritura están cerrados, una lectura retornará 0 (EOF).
  • Señal SIGPIPE: Si se intenta escribir en una tubería cuyo extremo de lectura está cerrado, el proceso escritor recibirá la señal SIGPIPE, que por defecto lo termina.

Respecto a la atomicidad, Linux garantiza que una operación de write() cuyo tamaño sea igual o inferior al límite PIPE_BUF (definido en limits.h, generalmente 4096 bytes) será atómica. Esto significa que los datos de esa escritura no se mezclarán con los de otras escrituras concurrentes en la tubería.

Tuberías con Nombre (FIFO)

Las tuberías con nombre, o FIFO (First In, First Out), superan la limitación de parentesco de las tuberías anónimas. Son archivos especiales en el sistema de archivos que sirven como punto de encuentro para procesos no relacionados. Un proceso puede escribir en el archivo FIFO y otro, independiente, puede leer de él.

Aunque su representación en el sistema de archivos ocupa una entrada (con permisos y metadatos), el contenido real de los datos reside en la memoria del kernel, por lo que el tamaño del archivo FIFO siempre se reporta como 0.

Creación y Uso

Se pueden crear FIFOs desde la línea de comandos con el programa mkfifo o mediante la llamada al sistema mkfifo() en código. La comunicación requiere que ambos extremos (escritor y lector) abran el archivo FIFO. Si solo uno lo abre, el proceso se bloqueará hasta que el otro extremo realice su apertura.

Ejemplo de Código

El siguiente código simula una comunicación cliente-servidor simple a través de una FIFO. El servidor (receptor) crea el archivo y lee datos. El cliente (emisor) se conecta y envía mensajes.

Código del Emisor (Cliente):

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>
#include <string.h>

#define RUTA_FIFO "/tmp/canal_comunicacion"

int main(void) {
    int fd_fifo;
    char buffer_envio[512];

    // Abrir la FIFO existente para escritura (bloqueará hasta que el lector la abra)
    printf("[Cliente] Conectando al servidor...\n");
    fd_fifo = open(RUTA_FIFO, O_WRONLY);
    if (fd_fifo == -1) {
        perror("open");
        exit(EXIT_FAILURE);
    }

    printf("[Cliente] Conexión establecida. Escriba mensajes (Ctrl+D para salir):\n");
    while (fgets(buffer_envio, sizeof(buffer_envio), stdin) != NULL) {
        // Escribir el mensaje (incluyendo el salto de línea)
        ssize_t escritos = write(fd_fifo, buffer_envio, strlen(buffer_envio));
        if (escritos == -1) {
            perror("write");
            break;
        }
    }

    close(fd_fifo);
    printf("[Cliente] Conexión cerrada.\n");
    return 0;
}

Código del Receptor (Servidor):

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>

#define RUTA_FIFO "/tmp/canal_comunicacion"

int main(void) {
    int fd_fifo;
    char buffer_recepcion[512];
    ssize_t bytes_leidos;

    // Crear la FIFO si no existe
    if (mkfifo(RUTA_FIFO, 0666) == -1) {
        if (errno != EEXIST) { // Ignorar si ya existe
            perror("mkfifo");
            exit(EXIT_FAILURE);
        }
    }

    printf("[Servidor] FIFO creada. Esperando conexión del cliente...\n");
    // Abrir la FIFO para lectura (bloqueará hasta que el escritor la abra)
    fd_fifo = open(RUTA_FIFO, O_RDONLY);
    if (fd_fifo == -1) {
        perror("open");
        exit(EXIT_FAILURE);
    }
    printf("[Servidor] Cliente conectado. Recibiendo datos:\n");

    // Leer datos hasta EOF (cuando el escritor cierre su descriptor)
    while ((bytes_leidos = read(fd_fifo, buffer_recepcion, sizeof(buffer_recepcion) - 1)) > 0) {
        buffer_recepcion[bytes_leidos] = '\0';
        printf("[Servidor] Recibido: %s", buffer_recepcion);
    }

    if (bytes_leidos == 0) {
        printf("[Servidor] El cliente cerró la conexión.\n");
    } else {
        perror("read");
    }

    close(fd_fifo);
    unlink(RUTA_FIFO); // Eliminar el archivo FIFO del sistema
    printf("[Servidor] FIFO eliminada. Apagando.\n");
    return 0;
}

La diferencia clave en esta implementación es que el emisor intenta abrir un archivo existente creado por el receptor, y el receptor se encarga de la limpieza final (unlink) del archivo especial.

Etiquetas: tubería anónima tubería con nombre IPC descriptores de archivo fork

Publicado el 7-5 05:57