Introduzione ai sistemi operativi con UNIX
Prec 6. La comunicazione tra i processi Succ

6.6 mkfifo

Riconsiderate il codice di un generico programma demone che avevamo iniziato a scrivere nel paragrafo sulla fork(2). Supponiamo di voler implementare un modello del tipo client-server (cliente-servitore): il processo daemon è il server o gestore di una certa risorsa (ad esempio una connessione di rete, un database o qualsiasi altra cosa); altri processi, definiti client o clienti, che daemon non sa a priori quali siano e non sono suoi figli in quanto daemon non ne genera, ma sono comunque processi qualunque facenti parte dell'albero dei processi a cui appartiene anche daemon, eseguiti dalla stessa CPU o da altre CPU dello stesso sistema multiprocessore, chiedono di compiere alcune operazioni su questa risorsa. daemon accetta le richieste ed esegue le operazioni per conto dei processi clienti. In questo modello a scambio di messaggi di tipo client-server, se solo daemon ha accesso diretto alla risorsa e se vi è un unico processo servitore daemon, ossia viene soddisfatta una richiesta per volta, non possono mai verificarsi problemi di sincronizzazione, per lo meno del tipo che devono essere gestiti esplicitamente dai processi.

Semplifichiamo ancora e supponiamo che sia sufficiente una comunicazione unidirezionale tra cliente e servitore.

Possiamo utilizzare le pipe per stabilire questo tipo di comunicazione? No, perché le pipe presentano l'inconveniente di consentire la comunicazione solo tra processi in relazione di parentela. UNIX mette a disposizione una versione persistente delle pipe dette pipe con nome (named pipe) o più semplicemente fifo, nome che enfatizza la logica con cui viene gestito il buffer usato per comunicare, che è la stessa usata nelle pipe, cioè FIFO = First In First Out. Il primo byte che viene scritto nella pipe o nella fifo sarà anche il primo ad essere letto, in altre parole i byte vengono letti nell'esatto ordine in cui sono scritti, così come in una coda di persone "oneste", i clienti vengono serviti (cioè escono dalla coda) nell'ordine esatto di arrivo.

Mentre le pipe non sono persistenti, vengono distrutte quando i processi che le hanno usate terminano, le fifo esistono sotto forma di file di dispositivo speciali all'interno del filesystem di UNIX e ciascun processo può utilizzarle indirizzandole tramite percorso (path) e nome, e in effetti si usa proprio la open(2) per aprire in lettura o in scrittura una fifo. Naturalmente questo pone dei problemi di sicurezza che vengono risolti in UNIX associando alla fifo una serie di permessi come a qualsiasi altro file. Quindi la fifo avrà un utente e gruppo possessore e permessi di lettura, scrittura ed esecuzione per l'utente, il gruppo e tutti gli altri, proprio come ciascun altro file. Quando un processo invoca la system call mkfifo(2) per creare una fifo deve specificarne i permessi d'accesso desiderati.

Le fifo sono usate molto meno della pipe. Esistono come vedremo altri meccanismi più potenti (non limitati ad un funzionamento unidirezionale) per la comunicazione cliente-servitore, quali le code di messaggi e i socket BSD, oppure la memoria condivisa e i semafori per sincronizzarvi l'accesso.

Potete creare ed usare le fifo anche dagli script di shell o tramite comandi di shell dati a mano, tramite l'utility mkfifo(1). In questo caso simuliamo una pipeline usando le fifo, ma questo esempio non rende ragione del fatto che le fifo permettono a processi qualsiasi di comunicare:

$ ls /dev | pr >dev-list

$ umask
0022
$ mkfifo fifo
$ ls -l fifo 
prw-r--r--  1 ant  ant  0 Jun 23 17:27 fifo
$ ls -F fifo
fifo|
$ ls /dev >fifo &
[1] 356
$ jobs -l
[1]+   356 Running                 ls /dev >fifo &
$ pr <fifo >dev-list
[1]+  Done                    ls /dev >fifo
$ ls -l fifo 
prw-r--r--  1 ant  ant  0 Jun 23 17:28 fifo

Notate che ls(1) usa l'indicatore p (che sta per pipe) per indicare che il file è di tipo fifo. Se usate l'opzione -F postpone al nome del file un simbolo di pipe (|). Per simulare una pipeline più lunga, occorrono tante fifo differenti quanti sono i simboli | usati nella pipeline.

Modifichiamo daemon.c in modo che crei una fifo utilizzando la system call mkfifo(2):

int
     mkfifo(const char *path, mode_t mode);
/* daemon.h */

#define FIFO_FILE "/tmp/daemon.fifo"
#define MSG_SIZE 256


/* daemon.c */

#include <sys/types.h>
#include <sys/stat.h>
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
#include "daemon.h"

int main(int argc, char *argv[])
{ pid_t pid;
  mode_t mode;
  FILE *p;
  char msg[MSG_SIZE];

  switch (pid=fork())
  { case -1:
      perror(*argv);
    exit(EXIT_FAILURE);

    case 0: /* we are the child */
      /* create the fifo if it doesn't exist */
      mode=umask(0); /* set unrestrictive umask
                        and save previous umask value */
      if (mkfifo(FIFO_FILE, 0666)==-1) perror(*argv);
      umask(mode); /* reset umask value */

      /* loop forever performing tasks */
      while(1)
      { p=fopen(FIFO_FILE, "r");
        fgets(msg, MSG_SIZE, p);
        puts("Received message:");
        do
        { printf("%s", msg);
        } while (fgets(msg, MSG_SIZE, p));
        fclose(p);
      }
    break;

    default: /* we are the parent */
      exit(EXIT_SUCCESS);
    break;
  }
}

Notate che i permessi di accesso specificati dal secondo argomento di mkfifo(2), non saranno quelli effettivi: il kernel considera la umask del processo daemon, la inverte bit a bit e poi effettua l'and logico bit a bit con il valore di mode per determinare i veri permessi. Poiché il valore di umask viene ereditato dal processo padre, questo influenza i permessi della fifo, sottraendone alcuni, ma mai aumentandoli. Un modo per essere sicuri dei permessi finali che avrà la fifo è settare uno specifico valore di umask per il processo daemon tramite la umask(2), oppure lasciare per tutta la durata di daemon il valore ereditato e disabilitare il meccanismo dell'umask solo durante l'esecuzione della mkfifo, come è stato fatto. Questo si ottiene quando la umask è 0, perchè quando viene invertita diventa una stringa di tutti 1 e in and logico non altera i valori dei permessi specificati:

permessi reali = permessi & ~umask;

se umask==0 ho permessi reali == permessi;

Se il file FIFO_FILE esiste già, la mkfifo(2) restituisce un errore di codice EEXIST.

$ ./daemon
$ ls -lF /tmp/daemon.fifo 
prw-rw-rw-  1 ant  wheel  0 Jun 23 18:40 /tmp/daemon.fifo|

Notate che il possessore della pipe viene settato uguale all'euid (effective user ID) del processo che la crea. Il gruppo della fifo viene settato uguale a quello della directory nella quale viene creata:

$ ls -ld /tmp
drwxrwxrwt  6 root  wheel  1024 Jun 23 21:39 /tmp

Per aprire in lettura o scrittura una fifo potete usare la open(2), oppure, se volete usare le funzioni della libreria stdio trattando la fifo come uno stream, la fopen(3). Quando avete terminato chiudetela tramite close(2) oppure fclose(3). Ecco il codice di un client che attraverso la fifo manda un messaggio al server, che semplicemente ne fa l'eco sul terminale. Una volta eseguito il server si blocca in attesa che un processo client apra la fifo in scrittura (la fopen(3) fatta per aprire una fifo in lettura di default è bloccante se la fifo non è aperta in scrittura da un altro processo). Nel momento in cui il client apre la fifo, il server viene sbloccato. Se il client non ci scrive niente ma la tiene aperta, e l'esecuzione passa al server, questo si blocca con la fgets(3) tentando di leggere su una fifo vuota. Immaginate poi che il client ci scrive qualcosa e poi la chiude. Una volta che il server svuota la fifo, la fgets(3) continuerebbe a restituire NULL, anziché bloccarsi di nuovo (nell'ipotesi che non ci sia un altro processo cliente scrittore). Per evitare il busy waiting che ne deriva, la fifo viene chiusa e poi riaperta e la prossima fopen(3) eseguita sarà bloccante se non vi è alcun cliente ancora. Questo è il motivo per cui la fopen(3) non è stata inserita fuori del ciclo infinito. Similmente aprendo una fifo in scrittura, se non vi è un altro processo lettore, la fopen(3) per default è bloccante.

/* client.c */

#include <stdio.h>
#include <stdlib.h>

#include "daemon.h"

int main()
{ FILE *p;
  char msg[MSG_SIZE];

  if ((p=fopen(FIFO_FILE, "w"))==NULL)
  { perror("fopen");
    exit(EXIT_FAILURE);
  }

  while (fgets(msg, MSG_SIZE, stdin))
    fputs(msg, p);

  exit(ferror(stdin)!=0);
}

Non resta che provare: avviate il demone su un terminale virtuale o in una finestra xterm(1) (o altro emulatore di terminale). Poi in altre finestre o terminali virtuali avviate uno o più client. Usando terminali diversi si apprezza meglio il meccanismo di comunicazione. Avrei potuto anche usare per questo esempio le funzioni read(2) e write(2) per operare sulla fifo, anziché quelle della libreria stdio(3). In questo modo non vi sono di mezzo i buffer che la stdio per default mette in gioco. Ecco le versioni di daemon.c e client.c che usano le syscall di basso livello per l'I/O sulla fifo:

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

int main(int argc, char *argv[])
{ pid_t pid;
  mode_t mode;
  int p;
  ssize_t len;
  char msg[MSG_SIZE];

  switch (pid=fork())
  { case -1:
      perror(*argv);
    exit(EXIT_FAILURE);

    case 0: /* we are the child */
      /* create the fifo if it doesn't exist */
      mode=umask(0); /* set unrestrictive umask
                        and save previous umask value */
      if (mkfifo(FIFO_FILE, 0666)==-1) perror(*argv);
      umask(mode); /* reset umask value */

      /* loop forever performing tasks */
      while(1)
      { p=open(FIFO_FILE, O_RDONLY);
        while ((len=read(p, msg, MSG_SIZE-1)) && len!=-1)
        { msg[len]='\0';
          printf("Received message:\n%s", msg);
        }
        close(p);
      }
    break;

    default: /* we are the parent */
      exit(EXIT_SUCCESS);
    break;
  }
}


/* client.c */

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>

#include "daemon.h"

int main()
{ int p;
  char msg[MSG_SIZE];

  if ((p=open(FIFO_FILE, O_WRONLY))==-1)
  { perror("open");
    exit(EXIT_FAILURE);
  }

  while (fgets(msg, MSG_SIZE, stdin))
    write(p, msg, strlen(msg));

  exit(ferror(stdin)!=0);
}

Anche qui in daemon.c la read(2) quando non ci sono più scrittori, dopo aver svuotato la fifo, invece di bloccarsi restituisce sempre 0 e non conviene quindi fare un ciclo infinito che abbia dentro una read(2), in quanto il server userebbe il busy waiting per aspettare che qualcuno apra e scriva nella fifo. Ecco perché conviene chiudere la fifo e poi riaprirla in attesa che si crei qualche processo che la scriva, quindi facendo fare alla open(2) l'eventuale bloccaggio.

I limiti delle fifo sono i seguenti: nel caso di più lettori, non c'è modo per dire a priori quale lettore riceverà un certo messaggio; lo scrittore cioè non ha modo di indirizzare un certo messaggio ad un determinato lettore. In pratica si usano solo quando c'è un solo lettore (un solo processo daemon). Invece più scrittori possono identificarsi al lettore, basta stabilire un formato per tutti i messaggi che vengono mandati: un messaggio potrebbe includere all'inizio informazioni circa il processo mittente e la lunghezza del messaggio stesso, cioè un header che precede il contenuto informativo del messaggio. Rimane comunque un meccanismo unidirezionale. Vedremo altri meccanismi che superano questi limiti.


Prec Indice Succ
pipe, popen Livello superiore kill, signal