Pipe

Cosa sono le pipe?

Le pipe sono la forma più “antica” di comunicazione tra processi UNIX.

Si possono inviare dati da un lato della pipe e riceverli dal lato opposto.

Tecnicamente, la pipe è una porta di comunicazione con send asincrona e receive sincrona.

Esistono pipe senza nome e con nome:

Pipe senza nome

Le pipe senza nome sono utilizzate per combinare comandi Unix direttamente dalla shell tramite il simbolo “|” (pipe).

Provate i semplici esempi illustrati in aula la lezione scorsa.

Per creare una pipe si utilizza la systemcall pipe(int filedes[2]) che restituisce in filedes due descrittori:

Notiamo quindi che le pipe sono half-duplex (monodirezionali): esistono due distinti descrittori per leggere e scrivere.

Esempio pipe senza nome

#include <stdio.h>
#include <string.h>
#include <unistd.h>
int main() {
        int fd[2];
 
        pipe(fd); /* crea la pipe */
        if (fork() == 0) {
                char *phrase = "prova a inviare questo!";
 
                close(fd[0]);                         /* chiude in lettura */
                write(fd[1],phrase,strlen(phrase)+1); /* invia anche 0x00 */
                close(fd[1]);                         /* chiude in scrittura */
        } else {
                char message[100];
                memset(message,0,100);
                int bytesread;
 
                close(fd[1]);                         /* chiude in scrittura */
                bytesread = read(fd[0],message,99);
                printf("ho letto dalla pipe %d bytes: '%s' \n",bytesread,message);
                close(fd[0]);                         /* chiude in lettura */
        }
}

Situazioni particolari

Le pipe si comportano “quasi” come dei normali file.

Ci sono casi particolari tipici delle pipe:

ESERCIZIO: modificare il programma precedente in modo da osservare i due eventi discussi qui sopra.

Esempio: pipe di shell

#include <stdio.h>
#include <unistd.h>
int main(int argc, char * argv[])
{
  int fd[2], bytesread;
  
  pipe(fd);
  if (fork() == 0)  {
    close(fd[0]);     	/* chiude in lettura */
    dup2(fd[1],1);      /* fa si che 1 (stdout) sia una copia di fd[1] */
                        /* da qui in poi l'output va sulla pipe */
    close(fd[1]);       /* chiude il descrittore fd[1] */
    execlp(argv[1],argv[1],NULL);   /* esegue il comando */
    perror("errore esecuzione primo comando");
  } else {
    close(fd[1]);       /* chiude in scrittura */
    dup2(fd[0],0);      /* fa si che 0 (stdin) sia una copia di fd[0] */
                        /* da qui in poi l'input proviene dalla pipe */
    close(fd[0]);       /* chiude il descrittore fd[0] */
    execlp(argv[2],argv[2],NULL);   /* esegue il comando */
    perror("errore esecuzione secondo comando");
  }
}

Pipe con nome

Le pipe senza nome non possono essere utilizzate da processi che non hanno un antenato in comune.

Per ovviare a questa limitazione esistono le pipe con nome. Tali pipe possono essere create con il comando mkfifo.

$ mkfifo myPipe	    <==== (oppure mknod myPipe p)
$ ls -al
totale 36
...
prw-rw-r--    1 focardi  focardi	 0 mag 23 00:57 myPipe
...
$

La pipe esiste nel filesystem e qualsiasi processo con i diritti di accesso al file può utilizzarla.

Esercizio: lettori e scrittori

Consideriamo un processo lettore (destinatario) che accetta, su una pipe con nome, messaggi provenienti da più scrittori (mittenti).

Gli scrittori mandano 3 messaggi e poi terminano.

Quando tutti gli scrittori chiudono la pipe il lettore ottiene 0 come valore di ritorno dalla read ed esce.

Lettori e scrittori sono processi distinti lanciati indipendentemente (non necessariamente parenti).

Lettore

#include <stdio.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/stat.h>
#include <stdlib.h>
#define PNAME "/tmp/aPipe"
int main() {
  int fd;
  char leggi;

  mkfifo(PNAME,0666); // crea la pipe con nome, se esiste gia' non fa nulla
  fd = open(PNAME,O_RDONLY); // apre la pipe in lettura
  if ( fd < 0 ) { 
    perror("errore apertura pipe"); 
    exit(1);
  }
  while (read(fd,&leggi,1)) { // legge un carattere alla volta fino a EOF
    if (leggi == '\0'){
      printf("\n"); // a capo dopo ogni stringa
    } else {
      printf("%c",leggi);
    }
  }
  close(fd);      // chiude il descrittore
  unlink(PNAME);  // rimuove la pipe
  return 0;
}

Scrittore

#include <stdio.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/stat.h>
#include <string.h>
#include <stdlib.h>
#define PNAME "/tmp/aPipe"
int main() {
  int fd, i, lunghezza;
  char *message;
  mkfifo(PNAME,0666); // crea la pipe con nome, se esiste gia' non fa nulla
  // crea la stringa
  lunghezza = snprintf(NULL,0,"Saluti dal processo %d",getpid());
  message = malloc(lunghezza+1);
  snprintf(message,lunghezza+1,"Saluti dal processo %d",getpid()); 
  fd = open(PNAME,O_WRONLY); // apre la pipe in scrittura
  if ( fd < 0 ) {  
    perror("errore apertura pipe"); exit(1);
  }
  for (i=1; i<=3; i++){   // scrive tre volte la stringa
    write (fd,message,strlen(message)+1); // include terminatore
    sleep(2);
  }
  close(fd);  // chiude il descrittore
  free(message);
  return 0;
}

Atomicità e direzionalità

Le scritture sulle pipe sono atomiche se inferiori alla dimensione PIPE_BUF (limits.h), usualmente 4096 bytes.

L’opzione O_RDWR nella open permette di aprire una pipe con nome in lettura e scrittura.

Esercizi

Esercizio 1: Le pipe gestiscono stream di byte: non c’è nessuna nozione di messaggio. Non è detto, quindi, che due write vengano lette tramite due read. Sperimentare inviando due write distinte consecutive e osservando che vengono lette da un’unica read (fare attenzione al terminatore).

Esercizio 2: Anche se non è ovvio da visualizzare, si può provare a superare PIPE_BUF (4096) per vedere che le write interferiscono una con l’altra. Si suggerisce di ridirezionare l’output del lettore su file (tramite > nomefile) in modo da esaminare l’output con un editor alla ricerca di messaggi ‘mescolati’