8. Piping
Il piping connette l’output (stdout e stderr) di un comando all’input (stdin) di un altro comando, consentendo dunque la comunicazione tra i due. Esempio:
ls . | sort -R #stout -> stdin #Questo comando elenca i file nella directory corrente e li ordina in modo casuale. | instrada l'output standard del comando di sinistra al comando a destra.
ls nonExistingDir |& wc #stdout e stderr -> stdin #Tenta di elencare i contenuti di una directory inesistente e instrada sia l’output standard che l’errore standard al comando 'wc', che conta il numero di righe, parole e caratteri
cat /etc/passwd | wc | less #out -> in, out-> in #Visualizza il contenuto del file `/etc/passwd`, lo instrada al comando `wc` per contare il numero di righe, parole e caratteri e quindi instrada l’output al comando `less` per la paginazione
#I 3 comandi sono indipendenti l'uno dagli altri
I processi sono eseguiti in concorrenza utilizzando un buffer, il processo scrittore (left) scrive dati nel buffer e il processo lettore legge i dati dal buffer:
- Se il buffer è pieno, il processo scrittore si sospende fino a quando non c’è spazio libero per scrivere altri dati
- Se il buffer è vuoto, il processo lettore si sospende fino a quando non ci sono dati da leggere
Esempio 1.
Dato il seguente script in C chiamato "output.c"
// output.out
#include <stdio.h>
#include <unistd.h>
int main()
{
for (int i = 0; i < 3; i++)
{
sleep(2);
fprintf(stdout,"Written in buffer"); //Ogni 2 secondi scrive la stringa "Written in buffer" nello standard output
fflush(stdout);
};
};
e il seguente script in C chiamato "input.c"
// input.out
#include <stdio.h>
#include <unistd.h>
int main()
{
char msg[50]; int n=3;
while((n--)>0)
{
int c = read(0,msg,49); //Legge 49 caratteri dallo standard input
if (c>0)
{
msg[c]=0;
fprintf(stdout,"Read: '%s' (%d)\n",msg,c); //Scrive i caratteri, letti dallo standard input, nello stream di output standard
};
};
};
eseguiamoli nel seguente ordine:
$./output.out | ./input.out$ #L'output di output.out viene reindirizzato all'input di input.out, quindi la stringa "Written in buffer" verrà letta da input.out e stampata sul suo output standard insieme al numero di caratteri letti
L'output è il seguente:
Read: 'Written in bufferWritten in bufferWritten in buff' (49)
Read: 'er' (2)
Ricordiamo che la funzione fflush(stdout);
serve per svuotare il buffer dello standard output e scrivere effettivamente la stringa nello standard output (istantaneamente, senza aspettare la fine dell'esecuzione del programma).
Pipe anonime
Le pipe anonime, come quelle usate su shell, ‘uniscono’ due processi aventi un antenato comune (oppure tra padre-figlio). Il collegamento è unidirezionale ed avviene utilizzando un buffer di dimensione finita.
Per interagire con il buffer (la pipe) si usano due file descriptors: uno per il lato in scrittura ed uno per il lato in lettura. Quando un processo scrive dati nel file descriptor di scrittura di una pipe anonima, il sistema operativo prende questi dati e li scrive nel buffer della pipe. Quando un processo vuole leggere dati dal file descriptor di lettura di una pipe anonima, richiede al sistema operativo di leggere un certo numero di byte dal file descriptor di lettura e il sistema operativo preleva questi dati dal buffer della pipe e li restituisce al processo. Il processo non interagisce direttamente con il buffer della pipe, ma utilizza i file descriptor per comunicare con il sistema operativo e richiedere la lettura o la scrittura di dati.
Visto che i processi figli ereditano i file descriptors, questo consente la comunicazione tra i processi.
Creazione pipe
La funzione pipe()
viene utilizzata per creare una coppia di file descriptor, che costituiscono un canale di comunicazione unidirezionale tra due processi. Un file descriptor rappresenta un canale di comunicazione o un file aperto.
La firma della funzione pipe() è la seguente:
int pipe(int pipefd[2]);
La funzione prende come argomento un array di due interi pipefd[2]
, che viene utilizzato per restituire i file descriptor associati alla pipe. pipefd[0]
rappresenta l'estremità di lettura della pipe, mentre pipefd[1]
rappresenta l'estremità di scrittura della pipe.
Dopo aver chiamato la funzione pipe()
, si ottiene una pipe con un lato per la lettura e un lato per la scrittura. I dati scritti in un lato della pipe possono essere letti dall'altro lato.
La pipe è unidirezionale, il che significa che i dati possono fluire solo in una direzione. Se si desidera una comunicazione bidirezionale tra due processi, è necessario creare due pipe.
Ecco un esempio di utilizzo della funzione:
#include <stdio.h> //pipe.c
#include <unistd.h>
#include <fcntl.h> //Senza questa libreria non compila
int main()
{
int fd[2], cnt = 0;
while(pipe(fd) == 0) //Crea una serie di pipe anonime usando 2 file descriptors. In caso di successo la funzione restituisce 0, fd[0] viene impostato al file descriptor di lettura della pipe e fd[1] viene impostato al file descriptor di scrittura della pipe
{
cnt++; //Tiene traccia del numero di pipe create con successo
printf("%d,%d,",fd[0],fd[1]); //Stampa i valori del file descriptor di lettura e scrittura della pipe appena creata
}
printf("\n Opened %d pipes, then error\n",cnt); //Appena la funzione pipe() restituisce un valore diverso da 0, il che indica che si è verificato un errore nella creazione della pipe, il programma stampa il numero totale di pipe create con successo
int op = open("/tmp/tmp.txt",O_CREAT|O_RDWR,S_IRUSR|S_IWUSR); //Apre il file /tmp/tmp.txt, il secondo argomento sono dei flag che specificano come il file deve essere aperto, il terzo argomento specifica i permessi da utilizzare nel caso in cui il file debba essere creato. O_CREAT indica che deve essere creato se non esiste, mentre O_RDWR indica che il file deve essere aperto in lettura/scrittura. S_IRUSR e S_IWUSR indicano che il proprietario del file ha il permesso di leggere e scrivere il file
printf("File opened with fd %d\n",op); //Stampa il file descriptor restituito dalla funzione open
}
L'output è simile al seguente:
3,4,5,6,7,8,9,...,10238,
Opened 5118 pipes, then error
File opened with fd 10239
Questo codice crea una serie di pipe anonime utilizzando la funzione pipe()
e stampa i valori dei file descriptor di lettura e scrittura delle pipe appena create, finchè non si raggiunge il limite massimo di file descriptors (10238 in questo caso). Inizialmente, il programma esegue un ciclo while che continua a creare pipe finché la funzione pipe()
restituisce 0, indicando che la creazione della pipe è avvenuta con successo. Ad ogni iterazione del ciclo, il programma incrementa il contatore cnt
per tenere traccia del numero di pipe create con successo e stampa i valori dei file descriptor di lettura (fd[0]
) e scrittura (fd[1]
) della pipe appena creata.
Lettura pipe: int read(int fd[0],char * data, int num)
La lettura della pipe tramite il comando read restituisce valori differenti a seconda della situazione:
- In caso di successo, read() restituisce il numero di bytes effettivamente letti
- Se il lato di scrittura è stato chiuso (da ogni processo), con il buffer vuoto restituisce 0, altrimenti restituisce il numero di bytes letti
- Se il buffer è vuoto ma il lato di scrittura è ancora aperto (in qualche processo) il processo si sospende fino alla disponibilità dei dati o alla chiusura
- Se si provano a leggere più bytes (num) di quelli disponibili, vengono recuperati solo quelli presenti
Esempio
#include <stdio.h> //readPipe.c
#include <unistd.h>
int main(void)
{
int fd[2]; char buf[50];
int esito = pipe(fd); //Crea una pipe anonima e quindi scrive e legge da essa. Se la funzione ha successo, restituisce 0, fd[0] viene impostato al file descriptor di lettura della pipe e fd[1] viene impostato al file descriptor di scrittura della pipe
if(esito == 0)
{
write(fd[1],"writing",8); //Scrive la stringa "writing" nel file descriptor di scrittura della pipe
int r = read(fd[0],&buf,50); //Legge fino a 50 byte dal file descriptor di lettura della pipe e li memorizza in un buffer buf
printf("Last read %d. Received: '%s'\n",r,buf); //Il numero di byte effettivamente letti viene restituito dalla funzione read() e viene stampato insieme al contenuto del buffer
//close(fd[1]); //Questa funzione dovrebbe chiudere il file descriptor di scrittura della pipe, ma è commentata. Quindi il file descriptor di scrittura della pipe rimane aperto e quindi la seconda chiamata alla funzione read() si bloccherà indefinitamente
r = read(fd[0],&buf,50); //Tenta di leggere nuovamente dal file descriptor di lettura della pipe, ma poiché non ci sono più dati disponibili nella pipe, questa chiamata alla funzione read() si bloccherà fino a quando non verranno scritti nuovi dati nella pipe o fino a quando il file descriptor di scrittura della pipe non verrà chiuso
printf("Last read %d. Received: '%s'\n",r,buf);
}
}
Scrittura: int write(int fd[1],char * data, int num)
La scrittura della pipe tramite il comando write restituisce il numero di bytes scritti. Tuttavia, se il lato in lettura è stato chiuso viene inviato un segnale SIGPIPE allo scrittore (default handler quit) e la chiamata restituisce -1.
In caso di scrittura, se vengono scritti meno bytes di quelli che ci possono stare (PIPE_BUF) la scrittura è “atomica” e quindi tutti i dati vengono scritti in una sola volta e non possono essere interrotti da altre operazioni di scrittura. In caso contrario non c’è garanzia di atomicità e potrebbero verificarsi problemi di concorrenza o di sovrascrittura dei dati e quindi la scrittura sarà bloccata (in attesa che il buffer venga svuotato) o fallirà se il flag O_NONBLOCK viene usato.
Per modificare le proprietà di una pipe, possiamo usare la system call fnctl, la quale manipola i file descriptors. In questo caso specifico viene utilizzata per impostare il flag O_NONBLOCK sul file descriptor fd, il che significa che le operazioni di lettura e scrittura sulla pipe associata a quel file descriptor non bloccheranno il processo se i dati non sono immediatamente disponibili o se il buffer è pieno. Invece, le operazioni di lettura e scrittura restituiranno un errore e il processo potrà continuare ad eseguire altre operazioni.
La firma della funzione fcntl() è la seguente:
int fcntl(int fd, F_SETFL, O_NONBLOCK);
Esempio
Di seguito un esempio di tentativo di scrittura nella pipe mentre il lato in lettura è chiuso.
Dato il seguente script in C chiamato "writePipe.c"
#include <unistd.h>
#include <stdio.h>
#include <signal.h>
#include <errno.h>
#include <stdlib.h> //writePipe.c
void handler(int signo) //Funzione chiamata quando si tenta di scrivere sul lato di scrittura della pipe
{
printf("SIGPIPE received\n");
perror("Error in handler");
}
int main(void)
{
signal(SIGPIPE,handler);
int fd[2]; char buf[50];
int esito = pipe(fd); //Crea una pipe senza nome
close(fd[0]); //Chiude il lato di lettura della pipe
printf("Attempting write\n");
int numOfWritten = write(fd[1],"writing",8); //Tenta di scrivere sul lato di scrittura della pipe
printf("I've written %d bytes\n",numOfWritten); //Dato che il lato di lettura è chiuso, viene generato un segnale SIGPIPE che viene catturato dalla funzione handler. Il programma stampa il numero di byte scritti, che in questo caso è -1 poiché la scrittura non è riuscita
perror("Error after write");
}
L'output è il seguente:
Attempting write
SIGPIPE received
Error in handler: Undefined error: 0
I've written -1 bytes
Error after write: Broken pipe
Esempio 2. (comunicazione unidirezionale)
Un tipico esempio di comunicazione unidirezionale tra un processo scrittore P1 ed un processo lettore P2 è il seguente:
- P1 crea una pipe()
- P1 esegue un fork() e crea P2
- P1 chiude il lato lettura: close(
fd[0]
) - P2 chiude il lato scrittura: close(
fd[1]
) - P1 e P2 chiudono l’altro fd appena finiscono di comunicare.
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h> //uni.c
int main()
{
int fd[2]; char buf[50];
pipe(fd); //Create unnamed pipe
int p2 = !fork();
if(p2)
{
close(fd[1]);
int r = read(fd[0],&buf,50); //Read from pipe
close(fd[0]);
printf("Buf: %s\n",buf);
}
else {
close(fd[0]);
write(fd[1],"writing",8); // Write to pipe
close(fd[1]);
}
while(wait(NULL) > 0);
}
Esempio 3. (comunicazione bidirezionale)
- P1 crea due pipe(), pipe1 e pipe2
- P1 esegue un fork() e crea P2
- P1 chiude il lato lettura di pipe1 ed il lato scrittura di pipe2
- P2 chiude il lato scrittura di pipe1 ed il lato lettura di pipe2
- P1 e P2 chiudono gli altri fd appena finiscono di comunicare
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#define READ 0
#define WRITE 1 //bi.c
int main()
{
int pipe1[2], pipe2[2]; char buf[50];
pipe(pipe1); pipe(pipe2); //Crea due pipe senza nome
int p2 = !fork(); //p2 vale 1 nel processo figlio e 0 nel processo padre
if(p2)
{
close(pipe1[WRITE]); close(pipe2[READ]); //Il processo figlio P2 chiude il lato di scrittura di pipe1 e il lato lettura di pipe2
int r = read(pipe1[READ],&buf,50); //Legge un messaggio da pipe1
close(pipe1[READ]); printf("P2 received: '%s'\n",buf); //Stampa il messaggio ricevuto
write(pipe2[WRITE],"Msg from p2",12); //Scrive un messaggio su pipe2
close(pipe2[WRITE]);
}else
{
close(pipe1[READ]); close(pipe2[WRITE]); //Il processo padre P1 chiude il lato di lettura di pipe1 e il lato scrittura di pipe2
write(pipe1[WRITE],"Msg from p1",12); //Scrive un messaggio su pipe1
close(pipe1[WRITE]);
int r = read(pipe2[READ],&buf,50); //Legge un messaggio da pipe2
close(pipe2[READ]); printf("P1 received: '%s'\n",buf); //Stampa il messaggio ricevuto
}
while(wait(NULL)>0);
}
Gestire la comunicazione
Per gestire comunicazioni complesse c’è bisogno di definire un “protocollo”. Esempio:
- Messaggi di lunghezza fissa (magari inviata prima del messaggio)
- Marcatore di fine messaggio (per esempio con carattere NULL o newline)
Più in generale occorre definire la sequenza di messaggi attesi.
Esempio: redirige lo stdout di cmd1 sullo stdin di cmd2
#include <stdio.h>
#include <unistd.h>
#define READ 0
#define WRITE 1 //redirect.c
int main (int argc, char *argv[])
{
int fd[2];
pipe(fd); //Crea una pipe senza nome
if (fork() != 0) //Crea un processo figlio, ed il processo padre (colui che scrive) esegue l'if
{
close(fd[READ]); //Chiude il lato di lettura
dup2(fd[WRITE], 1); //Chiude il descrittore di file 1 (che corrisponde a stdout) e lo sostituisce con una copia del descrittore di file fd[WRITE]. Qualsiasi output inviato a stdout dal programma eseguito successivamente con la funzione execlp() verrà in realtà scritto sulla pipe
close(fd[WRITE]); //Chiude il lato di scrittura originale
execlp(argv[1], argv[1], NULL); //Viene eseguito il programma specificato come primo argomento sulla riga di comando. Questo programma scriverà sulla pipe tramite stdout.
perror("error"); // Should never execute
} else //Parte eseguita dal figlio (colui che legge)
{
close(fd[WRITE]); //Chiude il lato di scrittura
dup2(fd[READ], 0); //Chiude il descrittore di file 0 (che corrisponde a stdin) e lo sostituisce con una copia del descrittore di file fd[READ]. Qualsiasi input letto da stdin dal programma eseguito successivamente con la funzione execlp() verrà in realtà letto dalla pipe
close(fd[READ]); //Chiude il lato di lettura originale
execlp(argv[2], argv[2], NULL); //Viene eseguito il programma specificato come secondo argomento sulla riga di comando utilizzando la funzione execlp(). Questo programma leggerà dalla pipe tramite stdin
perror("error"); // Should never execute
}
}
Per eseguire il programma bisognerà passare come argomenti i nomi dei programmi da eseguire nel processo padre e nel processo figlio, come ad esempio:
./redirect ls wc
In questo caso il processo padre eseguirà il comando ls (primo argomento sulla linea di comando) ed il suo output verrà inviato al processo figlio tramite pipe.
Il processo figlio esegue il comando wc (secondo argomento sulla linea di comando), il quale riceverà l'output del comando ls come input e ne stamperà il conteggio delle righe, delle parole e dei caratteri.
Pipe con nome/FIFO
FIFO
Le pipe con nome, o FIFO, sono delle pipe che corrispondono a dei file speciali nel filesystem grazie ai quali i processi, senza vincoli di gerarchia (indipendentemente dalla loro posizione nella gerarchia del sistema operativo), possono comunicare. Un processo può accedere ad una di queste pipe se ha i permessi sul file corrispondente ed è vincolato, ovviamente, dall’esistenza del file stesso (se il file che rappresenta la pipe con nome viene eliminato o non esiste, il processo non può utilizzare quella specifica pipe per comunicare).
Le FIFO sono interpretate come dei file, perciò si possono usare le funzioni di scrittura/lettura dei file viste nelle scorse lezioni. Restano però delle pipe, con i loro vincoli e le loro capacità. Dato che non sono dei file effettivi, alcune operazioni che possono essere eseguite sui file normale, come lseek, non funzionano sulle FIFO. Inoltre il loro contenuto è sempre vuoto e non vi ci si può scrivere con un editor!
Quando un processo apre una FIFO per la comunicazione, normalmente viene bloccato fino a quando anche l’altro lato della FIFO viene aperto da un altro processo. Questo significa che entrambi i processi devono aprire la FIFO prima che possano iniziare a comunicare. Le differenze tra le pipe anonime e le FIFO risiedono principalmente nella loro creazione e gestione. Le pipe anonime vengono create e gestite automaticamente dal sistema operativo quando due processi si scambiano dati, mentre le FIFO devono essere create esplicitamente dai processi e gestite come file nel filesystem.
Creare una FIFO
Crea una FIFO (un file speciale), solo se non esiste già, con il nome pathname (es /tmp/myfifo). Il parametro mode può definire i privilegi del file nella solita maniera (es S_IRUSR, S_IWUSR, S_IRGRP, S_IWGRP, S_IROTH e S_IWOTH).
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h> //fifo.c
int main(void)
{
const char * fifoName = "/tmp/fifo1";
mkfifo(fifoName,S_IRUSR|S_IWUSR); //Crea una pipe con nome (FIFO) se non esiste
perror("Created?");
}
Comunicazione bloccata
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h> //fifo.c
int main(void)
{
const char * fifoName = "/tmp/fifo1";
mkfifo(fifoName,S_IRUSR|S_IWUSR); //Crea una pipe con nome (FIFO) se non esiste
perror("Created?"); //Stampa un messaggio di errore se la creazione della FIFO non è stata eseguita correttamente
if (fork() == 0) //Parte eseguita da Child
{
open(fifoName,O_RDONLY); //Apre il lato di lettura della FIFO, ma rimane bloccato finché il lato di scrittura non viene aperto dal processo padre. Una volta che il processo padre ha aperto il lato di scrittura, il processo figlio può completare l’apertura del lato di lettura e stampare il messaggio “Open read”.
printf("Open read\n");
}else
{
sleep(3);
open(fifoName,O_WRONLY); //Apre il lato di scrittura della FIFO. L’apertura del lato di scrittura della FIFO non si blocca e può essere eseguita anche se il lato di lettura non è ancora stato aperto (il lato di lettura invece deve attendere quello di scrittura)
printf("Open write\n");
}
}
Esempio comunicazione: writer (1/2)
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h> //fifoWriter.c
int main (int argc, char *argv[])
{
int fd;
const char * fifoName = "/tmp/fifo1";
char str1[80];
const char * str2 = "I'm a writer";
mkfifo(fifoName,S_IRUSR|S_IWUSR); //Crea una pipe con nome (FIFO) se non esiste
fd = open(fifoName, O_WRONLY); //Apre la FIFO in modalità di sola scrittura
write(fd, str2, strlen(str2)+1); //La stringa puntata da str2 viene scritta nella FIFO
close(fd); //La FIFO viene chiusa
fd = open(fifoName, O_RDONLY); //Apre la FIFO in modalità di sola lettura (si può fare perchè è sufficiente aver aperto prima il lato di scrittura, anche se è stato chiuso prima di aver aperto quello di lettura)
read(fd, str1, sizeof(str1)); //I dati vengono letti dalla FIFO e memorizzati nell'array di caratteri str1
printf("Reader is writing: %s\n", str1); //Il contenuto dell'array str1 viene stampato
close(fd); //La FIFO viene chiusa
}
Esempio comunicazione: reader (2/2)
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
//fifoReader.c
int main (int argc, char *argv[])
{
int fd;
const char * fifoName = "/tmp/fifo1";
mkfifo(fifoName,S_IRUSR|S_IWUSR); //Crea una pipe con nome (FIFO) se non esiste
char str1[80];
const char * str2 = "I'm a reader";
fd = open(fifoName , O_RDONLY); //Apre la FIFO in modalità di sola lettura
read(fd, str1, 80); //Legge fino ad 80 caratteri dalla FIFO e li memorizza in str1
close(fd); //La FIFO viene chiusa
printf("Writer is writing: %s\n", str1); //Stampa il contenuto letto dalla FIFO
fd = open(fifoName , O_WRONLY); //Apre la FIFO in modalità di sola scrittura
write(fd, str2, strlen(str2)+1); //Scrive una stringa nella FIFO
close(fd); //La FIFO viene chiusa
}
Per provare la comunicazione attraverso le pipe FIFO sarà necessario compilare entrambi gli eseguibili ed eseguirli in due terminali separati. Oppure far eseguire il primo programma in background
./fifoWriter &
e successivamente avviare il secondo programma
./fifoReader
Per avere entrambi gli output nella stessa finestra del terminale
Comunicazione sul terminale
È possibile usare le FIFO da terminale, leggendo e scrivendo dati tramite gli operatori di redirezione.
echo “message for pipe” > /path/nameOfPipe #scrive la stringa “message for pipe” nella pipe denominata situata in /path/nameOfPipe
cat /path/nameOfPipe #legge i dati dalla pipe denominata situata in /path/nameOfPipe e li visualizza nel terminale
NB: non si possono scrivere dati usando editor di testo! Una volta consumati i dati, questi non sono più presenti sulla FIFO.
Pipe anonime vs FIFO
pipe | FIFO | |
Rappresentazione | Buffer | Buffer |
Riferimento | anonimo | File |
Accesso | 2 File descriptors | 1 File descriptors |
Persistenza | Eliminata alla terminazione di tutti i processi | Esiste finché esiste il file |
Vincoli accesso | Antenato comune | Permessi sul file |
Creazione | pipe() | mkfifo() |
Max bytes per atomicità | PIPE_BUF = 4096 on Linux, minimo 512 Bytes POSIX |
Conclusioni
PIPE e FIFO (“named pipes”) sono sistemi di comunicazione tra processi (“parenti”, tipicamente padre-figlio, nel primo caso e in generale nel secondo caso) che consentono scambi di informazioni (messaggi) e sincronizzazione (grazie al fatto di poter essere “bloccanti”).