Le pipe sono un meccanismo di comunicazione tra processi che permettono a uno o più processi di condividere dati tramite un canale unidirezionale. In sostanza, una pipe crea un collegamento tra due processi: uno scrive dati nella pipe e l’altro legge quei dati.

1 - Caratteristiche principali

  • Unidirezionali: i dati fluiscono in una sola direzione (dal processo scrivente a quello leggente).
  • Anonime: sono tipicamente usate tra processi che hanno una relazione gerarchica (padre-figlio). In genere, il processo padre crea una pipe e poi genera il processo figlio che la usa.
  • Comunicazione in memoria: la pipe si comporta come un buffer circolare tra i due processi, memorizzando temporaneamente i dati fino a quando non vengono letti.

2 - Funzionamento delle pipe

Una pipe in UNIX o Linux può essere creata con la chiamata di sistema pipe(). Questa funzione genera due file descriptor:

  • Il file descriptor per la lettura (fd[0]);
  • Il file descriptor per la scrittura (fd[1]).

Un processo può scrivere dati nel file descriptor fd[1] e un altro processo può leggerli dal file descriptor fd[0].

2.1 - Esempio di codice in linguaggio C

Ecco un esempio di uso delle pipe in linguaggio C, in cui un processo padre invia un messaggio al processo figlio usando una pipe.

#include <stdio.h>
#include <unistd.h>
#include <string.h>
 
int main() {
    int fd[2]; // fd[0] per lettura, fd[1] per scrittura
    pid_t pid;
    char buffer[100];
    
    // Creazione della pipe
    if (pipe(fd) == -1) {
        perror("pipe");
        return 1;
    }
 
    // Creazione del processo figlio
    pid = fork();
 
    if (pid < 0) {
        perror("fork");
        return 1;
    }
 
    if (pid > 0) {
        // Processo padre
        close(fd[0]); // Chiudiamo l'estremità di lettura nel padre
 
        // Scriviamo un messaggio nella pipe
        char msg[] = "Ciao dal processo padre!";
        write(fd[1], msg, strlen(msg) + 1);
 
        close(fd[1]); // Chiudiamo l'estremità di scrittura dopo aver scritto
    } else {
        // Processo figlio
        close(fd[1]); // Chiudiamo l'estremità di scrittura nel figlio
 
        // Leggiamo il messaggio dalla pipe
        read(fd[0], buffer, sizeof(buffer));
        printf("Messaggio ricevuto dal figlio: %s\n", buffer);
 
        close(fd[0]); // Chiudiamo l'estremità di lettura dopo aver letto
    }
 
    return 0;
}

Spiegazione del codice:

  • pipe(fd) crea una pipe. fd[0] è usato per la lettura e fd[1] per la scrittura.
  • fork() crea un nuovo processo figlio.
    • Nel processo padre, chiudiamo l’estremità di lettura (fd[0]) e scriviamo il messaggio nella pipe tramite write().
    • Nel processo figlio, chiudiamo l’estremità di scrittura (fd[1]) e leggiamo il messaggio tramite read().
  • Dopo la comunicazione, ogni processo chiude l’estremità della pipe che non è più necessaria.

3 - Vantaggi e svantaggi delle pipe

I principali vantaggi delle pipe sono:

  • Semplicità: le pipe sono facili da usare e implementare, soprattutto per la comunicazione tra processi con una relazione gerarchica (ad esempio, un processo padre e un processo figlio). Sono supportate direttamente a livello del sistema operativo, con chiamate di sistema semplici come pipe() per creare una pipe e read()/write() per leggere e scrivere dati.
  • Comunicazione unidirezionale: le pipe forniscono un canale di comunicazione unidirezionale, utile in situazioni in cui i dati devono fluire solo in una direzione (da un processo produttore a un processo consumatore).
  • Buffering automatico: le pipe utilizzano un buffer interno gestito dal sistema operativo, che rende la gestione dei dati tra i processi più semplice, senza necessità di gestire manualmente il buffering dei dati.
  • Sincronizzazione implicita: le pipe assicurano una forma di sincronizzazione implicita, in quanto se un processo tenta di leggere da una pipe vuota, verrà bloccato fino a quando non ci saranno dati disponibili. Allo stesso modo, se la pipe è piena, il processo che scrive attenderà fino a quando c’è spazio libero.
  • Indipendenza dalla rete: le pipe funzionano a livello locale tra processi sullo stesso sistema, senza necessità di connessioni di rete, il che le rende efficienti in termini di prestazioni per la comunicazione tra processi locali.

I principali svantaggi delle pipe sono:

  • Unidirezionalità: i dati nelle pipe possono fluire in una sola direzione. Se è necessaria la comunicazione bidirezionale (andata e ritorno), bisogna creare due pipe, aumentando la complessità della gestione dei dati.
  • Uso limitato tra processi correlati: le pipe, dette anche pipe anonime o pipe convenzionali, funzionano solo tra processi che hanno una relazione gerarchica (ad esempio, un processo padre che comunica con il figlio). Per comunicare tra processi non correlati, è necessario usare le named pipe, che sono più complesse da gestire rispetto alle pipe anonime.
  • Capacità limitata: le pipe hanno una capacità limitata (tipicamente qualche kilobyte). Se il buffer si riempie, il processo scrivente viene bloccato finché il buffer non viene svuotato. Allo stesso modo, se non ci sono dati, il processo leggente viene bloccato.
  • Accesso sequenziale: le pipe operano in modo sequenziale secondo la politica FIFO (First In, First Out). Questo è ideale per flussi semplici di dati, ma in scenari complessi, in cui è richiesta una gestione avanzata dei messaggi, le pipe possono risultare limitate.
  • No persistenza dei dati: i dati nelle pipe sono transienti, cioè una volta che i dati vengono letti, vengono rimossi dalla pipe. Se un processo legge i dati ma non li elabora correttamente, non è possibile rileggere il messaggio. Inoltre, se un processo termina senza leggere i dati, questi vengono persi.
  • Sincronizzazione manuale nei processi complessi: sebbene ci sia sincronizzazione implicita, in scenari complessi potrebbe essere necessario gestire manualmente la sincronizzazione tra processi per evitare race condition o blocchi inutili.
  • Non adatte per grandi quantità di dati: le pipe sono ideali per il passaggio di piccoli blocchi di dati. Quando è necessario trasferire grandi quantità di dati o file complessi, l’uso delle pipe diventa inefficiente rispetto ad altri metodi di comunicazione tra processi come la memoria condivisa.

4 - Named pipe (FIFO)

Le named pipe (anche chiamate FIFO per il loro comportamento) sono una versione più avanzata delle cosiddette pipe anonime o pipe convenzionali, cioè le pipe ordinarie: a differenza di quest’ultime, infatti, le named pipe possono essere utilizzate anche tra processi non legati da una relazione padre-figlio e sono bidirezionali.

4.1 - Creazione di una named pipe tramite mkfifo()

La funzione mkfifo() è una chiamata di sistema che permette di creare una named pipe.

Il suo prototipo è il seguente:

#include <sys/types.h>
#include <sys/stat.h>
 
int mkfifo(const char *pathname, mode_t mode);

dove:

  • pathname: percorso del file FIFO da creare.
  • mode: permessi del file FIFO (analoghi ai permessi dei file normali, influenzati dall’umask del processo).
  • valore di ritorno int: può assumere i seguenti valori:
    • 0: la creazione della named pipe ha avuto successo.
    • -1: c’è stato un errore e errno viene impostato.

4.2 - Lettura e scrittura in un FIFO

int unlink(const char *pathname);
  • Parametro: pathname \to il percorso della FIFO da eliminare.
  • Restituisce:
    • 0 in caso di successo.
    • -1 in caso di errore, con errno impostato per specificare il tipo di errore.

Importante: unlink() rimuove solo il riferimento al file dal filesystem. Se un processo ha ancora aperto la FIFO, questa continuerà a esistere fino alla chiusura di tutti i descrittori di file associati.

4.4 - Comportamento particolare

  1. Se un processo ha aperto la FIFO e un altro la elimina con unlink(), la FIFO sparisce dal filesystem, ma continua a esistere per i processi che la stanno ancora usando.
  2. Se nessun processo sta usando la FIFO, unlink() la elimina immediatamente.
  3. Se un processo tenta di aprire la FIFO dopo la sua eliminazione, otterrà un errore ENOENT (“No such file or directory”).

4.5 - Named pipe nella shell

Ecco un esempio in bash di named pipe:

mkfifo mypipe

Dopo aver creato una named pipe, due processi distinti possono usarla per scambiarsi dati:

  • Un processo può scrivere nella pipe con echo "Hello" > mypipe;
  • Un altro processo può leggere da essa con cat < mypipe.

Fonti

  • Abraham Silberschatz, Peter Baer Galvin, Greg Gagne - Sistemi Operativi (10ᵃ Edizione) - Pearson, 2019 - ISBN: 9788891904560.
  • 🏫 Lezioni e slide del Prof. Aldinucci Marco del corso di Sistemi Operativi (canale B), Corso di Laurea in Informatica presso l’Università di Torino, A.A. 2024-25: