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

6.5 pipe, popen

UNIX mette a disposizione vari meccanismi per la comunicazione e la sincronizzazione tra i processi. I più semplici da usare ed anche i primi che sono stati introdotti sono i segnali, le pipe [paip] (dette anche pipeline) e le fifo [faifou]. Questi strumenti sono più limitati e parziali rispetto a quelli generali (semafori e primitive wait e signal del modello a memoria comune, primitive send e receive del modello a scambio di messaggi che non è stato ancora illustrato). Tuttavia sono sufficienti per risolvere i problemi più comuni e semplici di sincronizzazione e comunicazione tra i processi. Nelle versionipiù recenti di UNIX, compresi i ben noti Linux e FreeBSD, sono stati introdotti anche gli strumenti più generali (semafori e code di messaggi). Questi sono meno facili da usare rispetto a pipe, fifo e segnali e li studieremo in seguito.

Una pipe permette a due processi che devono cooperare di comunicare tramite messaggi che sono sequenze di byte: un processo invia un messaggio e un'altro lo riceve. Esiste una system call, la pipe(2), che serve per costruire una struttura di tipo pipe. Per inviare e leggere dati su una pipe si usano le system call generali per l'I/O, la read(2) e la write(2). La differenza principale tra una pipe e una fifo è che quest'ultima è permanente, mentre la pipe scompare quando il processo che l'ha creata termina. In altri termini pipe e fifo sono molto simili; si può dire che le fifo sono una variante persistente delle pipe, mentre le pipe invece persistono solo durante il tempo di vita dei processi. Studieremo prima le pipe e poi vedremo che capire le fifo non richiederà ulteriori sforzi.

int
     pipe(int *fildes);

La syscall pipe(2) ritorna 0 in caso di successo, altrimenti ritorna il valore -1 e setta errno, quindi dovreste sempre controllare il valore di ritorno. Ci sono tre casi di errore:

     [EMFILE]           Troppi descrittori attivi.

     [ENFILE]           La tabella di sistema dei file è piena.

     [EFAULT]           Il buffer fildes è fuori dell'area di
                        indiririzzamento del processo.

Un esempio d'uso:

int fd[2];

if (pipe(fd))
{ perror(NULL);
  exit(EXIT_FAILURE);
}

fildes (file descriptor) rappresenta un parametro di uscita: la pipe(2) alloca due descrittori di file e li inserisce in fildes; sono due perché da una pipe occorre sia leggere che scrivere. Infatti un descrittore va usato per scrivere e l'altro per leggere. Come fate con tutti i descrittori di file, potete utilizzarli con le syscall read(2), write(2) e close(2). Quest'ultime servono quindi anche per la comunicazione tra i processi! Si scrive e si legge dalle pipe come quando si scrive e si legge da un file ordinario. Per convenzione fildes[0], il primo descrittore, viene usato come estremità di lettura della pipe e fildes[1] come terminale di scrittura della pipe. Se fate viceversa funziona lo stesso in FreeBSD, ma meglio aderire alla convenzione. Inoltre per maggiore leggibilità al posto di 0 e 1 useremo queste due costanti simboliche:

#define READ  0
#define WRITE 1

Quindi tutto ciò che viene scritto in fildes[1], può essere letto nello stesso ordine tramite fildes[0]. Uno stesso processo può fare sia la lettura che la scrittura, ma ciò evidentemente non è molto utile. Vogliamo che siano due processi separati a fare la lettura e la scrittura. Affinché i due processi possano condividere la pipe questa deve essere creata da un comune antenato dei due processi e poi ereditata, oppure più semplicemente dei due processi uno è il processo padre e crea la pipe prima di chiamare la fork(2) e generare il processo figlio. Ricordate infatti che quando si crea un clone di un processo tramite la fork(2), vengono ereditati dal processo figlio tutti i file che il padre ha aperti, comprese le pipe. In effetti le pipe sono un meccanismo di comunicazione tra processi non del tutto generale: si può usare solo tra due processi che abbiano un comune antenato che ha creato la pipe prima di forkare o tra un processo padre che ha creato la pipe prima di forkare e uno dei suoi discendenti, che la eredita. Ricordate che con la fork(2) i descrittori vengono copiati, ma essi riferiscono gli stessi oggetti sottostanti, perciò il canale della pipe viene effettivamente condiviso dai due processi.

Tradizionalmente la pipe è un canale di comunicazione tra processi unidirezionale (o half-duplex), nel senso che un processo deve solo scrivere nella pipe (il produttore) mentre l'altro deve solo leggere dalla pipe (il consumatore). Il sistema operativo UNIX associa ad ogni pipe un buffer condiviso dai due processi, il cui elemento ha la dimensione di 1 singolo byte, e dei semafori che guardano questo buffer dagli accessi concorrenti e sincronizzano gli accessi da parte dei due processi, come abbiamo visto teoricamente. Tutto questo lavoro di buffering e sincronizzazione è fatto da UNIX, senza che il programmatore debba minimamente preoccuparsene. In particolare, come abbiamo visto discutendo il problema del produttore e del consumatore, la sincronizzazione è fatta sospendendo il processo consumatore che tenta di leggere da una pipe vuota, finché non arriva qualche dato (ossia accade che un processo produttore scrive usando il descrittore di file corrispondente al lato di scrittura della stessa pipe); dualmente viene posto nello stato di wait il processo produttore che tenta di scrivere in una pipe già piena, e ci resta finchè non interviene il consumatore a svuotarla un po'.

Quelle delle pipe è stata un'idea geniale dei progettisti di UNIX: con le pipe il sistema operativo mette a disposizione del programmatore uno strumento per consentire ai processi di comunicare tra di loro tramite scambio di messaggi (problema del produttore-consumatore), a volte senza che i processi coinvolti lo sappiano, come quando la pipe viene impostata dalla shell. I messaggi sono sequenze di byte; le pipe sono unidirezionali, cioe' i messaggi fluiscono in una sola direzione: un processo inserisce i messaggi da un lato scrittura e un altro preleva i messaggi dal lato lettura. La pipe gestisce i messaggi inseriti secondo una logica FIFO. Normalmente le pipe hanno una dimensione fissa (es. 4096 byte). Il sistema realizza nello strumento pipe il meccanismo di sincronizzazione nascondendolo ai processi. Il meccanismo di sincronizzazione e' lo stesso visto nel problema generale del produttore consumatore:

Un programmatore che utilizzi le pipe tuttavia non ha nemmeno bisogno di essere conscio dei problemi di sincronizzazione, in quanto ci pensa il sistema operativo a risolverli per lui!

Tutto quello che il programmatore deve sapere è come invocare la pipe(2), e che bisogna passargli un vettore di due interi fd che rappresentano gli identificatori dei due file che costituiscono la pipe: quello aperto in lettura fd[0] (detto anche lato di uscita della pipe) e quello aperto in scrittura fd[1] (detto lato di ingresso). Il valore di ritorno in caso di successo e' 0; se non si riesce a costruire la pipe si ha -1.

La pipe viene usata nell'ambito della gerarchia dei processi: come abbiamo visto in UNIX l'uso della fork(2) per creare un nuovo processo induce una relazione gerarchica tra tutti i processi del sistema. Non esiste un processo nel sistema che non faccia parte dell'albero dei processi. Attraverso lo strumento pipe possono comunicare processo padre e processo figlio o processi figli (che ereditano il pipe dal processo padre) e in generale le pipe non possono essere usate tra due processi qualsiasi dell'intera gerarchia.

In realtà l'implementazione di FreeBSD permette di usare la pipe in modo bidirezionale (o full-duplex), quindi ciascuno dei due descrittori può essere usato sia per leggere che per scrivere nella pipe. Per ragioni di compatibilità è meglio tuttavia limitarsi ad usare le pipe in modo unidirezionale, e meglio ancora è se seguite la usuale convenzione definita dalle costanti READ e WRITE. Se vi servono delle pipe bidirezionali potete usarne due, una la usate per scrivere e l'altra solo per leggere in un processo e viceversa nell'altro processo.

Volendo potete avere più processi che scrivono o leggono dalla stessa pipe. Quando avete produttori e consumatori multipli, se non è importante l'ordine in cui ciascun produttore produce e ciascun consumatore consuma, le pipe vanno bene come sono, altrimenti sono richiesti ulteriori vincoli di sincronizzazione che bisognerebbe imporre tramite opportuni semafori.

Nel caso di processi multipli che leggono o scrivono da una pipe (o una fifo) diventa importante considerare che lo standard POSIX stabilisce che atomicamente, senza cioè che l'operazione venga interrotta, al più 512 bytes possono essere letti o scritti da una pipe, come definito in <limits.h>:

#define _POSIX_PIPE_BUF         512

Invece in <sys/pipe.h> è definita la dimensione del buffer usato dalle pipe e sono 16 kB:

/*
 * Pipe buffer size, keep moderate in value, pipes take kva space.
 */
#ifndef PIPE_SIZE
#define PIPE_SIZE       16384
#endif

Quindi se in questo caso scrivete o leggete quantità maggiori di dati, l'operazione potrebbe essere interrotta e pezzi di 512 byte potrebbero essere scritti o letti in modo intervallato da diversi processi.

Tuttavia il caso più frequente di utilizzo delle pipe è quello in cui vi sono due soli processi, uno legge solamente e l'altro scrive solamente. Questo è anche il modo in cui le pipe vengono usate dalla shell.

Vediamo un esempio di utilizzo delle pipe. Supponete che volete scrivere un programma C che in alcune circostanze deve mandare una e-mail. Siete piuttosto pigri e non avete voglia di scrivere del codice che usi i socket e il protocollo SMTP, o imparare ad usare una libreria che abbia una funzione per mandare le e-mail. Sapete però che tra i comandi di sistema ce n'è uno che fa proprio quello che volete fare voi dal vostro programma, mail(1), che è un MUA (Mail User Agent) in modo testo. State pensando: se fosse così facile programmare come usare la shell! Se potessi usare il comando mail(1) anche dal mio programma C, nonostante non abbia i sorgenti.

La risposta è che potete. Ci sono due modi diversi di farlo. Uno dei motivi fondamentali del successo di UNIX è proprio il fatto che mette a disposizione molti strumenti, tutti dotati di una interfaccia d'uso uniforme (la riga di comando) e ciascuno specializzato in un determinato compito e permette poi di combinarli insieme, fornendo strumenti per farli comunicare (come sono appunto le pipe o la redizione dei flussi di I/O predefiniti per i programmi). Questo permette di automatizzare alcune operazioni tramite script senza bisogno di intervento manuale, e anche di riutilizzare le funzioni messe a disposizione da un certo programma all'interno di un altro programma, semplificando lo sviluppo di quest'ultimo e quindi diminuendone notevolmente i tempi di sviluppo.

Nota: uno svantaggio di questo approccio è però che spesso l'installazione di un programma richiede l'installazione di molti altri programmi come dipendenze. A volte può accadere che queste dipendenze siano tante e vi siano programmi installati come dipendenze anche abbastanza grossi e ricchi di funzioni che non vengono mai utilizzate, né dal programma che le richiede come dipendenze, né manualmente, perché non conoscete in dettaglio questi programmi e non li usate mai manualmente, oppure ne usate altri simili. Eppure perché sono delle dipendenze siete costretti ad installarli.

I linguaggi di shell sono in effetti dei "linguaggi colla" adatti per realizzare in modo semplice proprio questo tipo di operazioni.

Usando la shell, immaginando che il comando myprg produca il corpo della mail che stampa sul suo stdout, si usa questo comando per generare e inviare la mail:

   $ myprg | mail -s "insert subject here" user@domain

Nota: se volete sperimentare, ricordate che per operare il comando mail(1) abbisogna che sia avviato localmente un Mail Transport Agent, quale sendmail(8). mail(1) non è in grado di utilizzare un MTA remoto attraverso internet in quanto non supporta il protocollo SMTP. Questo è un altro esempio di dipendenza tra i programmi. Tuttavia affinché mail(1) possa operare non è necessario che sendmail(8) si metta in ascolto tramite socket TCP/IP anche per la posta in arrivo, basta che sia avviato per gestire la sola posta locale e quella in uscita verso altri host. In FreeBSD il programma interattivo di installazione e configurazione sysinstall(8) permette di settare l'avvio di sendmail tramite l'opzione Configure/Networking/Sendmail.

Cosa sta accadendo? La shell esegue i due comandi myprg e mail come processi figli concorrenti e aggiusta le cose in modo che l'output standard del comando myprg divenga l'input standard del comando mail. Nessuno dei due processi sa da dove proviene effettivamente il suo stdin o dove va a finire il suo stdout e non ha bisogno di saperlo!

Più in dettaglio ecco cosa accade: la shell, il processo padre, prima crea una pipe, poi forka due volte: la pipe viene ereditata da entrambi i nuovi processi figli creati, di cui uno si classifica come produttore e l'altro come consumatore. A questo punto tuttavia i processi figli stanno ancora eseguendo codice della shell. Quello che accade dopo è che uno dei nuovi processi shell eseguirà la exec(3) per diventare myprg, mentre l'altro diventerà mail. La shell aspetterà poi che entrambi i processi siano terminati, chiamando due volte la system call wait(2). Ma come avviene che lo stdout di myprg diventa lo stdin di mail? Il processo di shell che deve eseguire myprg fa sì che il suo stdout, ossia il descrittore 1, diventi un duplicato del descrittore WRITE della pipe ereditata, prima di eseguire la exec(3). Similmente quello che deve eseguire mail fa sì che il suo stdin, ossia il descrittore 0, diventi un duplicato del descrittore READ della pipe ereditata.

La riassegnazione di un descrittore avviene in due passi: prima lo si chiude tramite la close(2). Poi si usa la dup(2), indicando come argomento solamente il descrittore da duplicare, perchè la dup(2) ha la proprietà che il nuovo descrittore che ricrea è quello di numero più basso non utilizzato nella tabella dei descrittori per-processo, quindi il primo disponibile. Tra un po' vedremo un esempio concreto di codice che implementa questo meccanismo fondamentale alla shell e a tutti quei programmi che vogliono costruire delle pipe tra processi come fa la shell.

L'altro modo per sfruttare il comando esterno mail(2) da programma consiste nel passare attraverso il file system, creando un file temporaneo in cui scrivere il body della mail inviare e poi utilizzando la combinazione fork/exec o la più semplice e portabile funzione di libreria standard del C system(3), per invocare il comando mail(2). Poiché mail(2) non prevede una opzione per leggere in un file esterno la mail da inviare, ma si aspetta l'input da stdin, occorre che il contenuto del file temporaneo sia rediretto nello stdin di mail(2). A questo provvede la shell: specificando l'argomento <tempfile, questo viene elaborato dalla shell e non verrà passato al comando mail(2); l'effetto di <tempfile è il seguente: la shell forka per eseguire mail(2), ma prima di eseguire la exec(3) fa sì che nel processo figlio il descrittore del file per l'input standard (il descrittore numero 0) si riferisca al file tempfile anziché al terminale. Questo si ottiene semplicemente chiudendo il file 0 e poi aprendo il file tempfile in lettura tramite la open(2). Il descrittore restituito dalla open(2) sarà il numero zero perché il sistema assicura che la open(2) utilizzi sempre il descrittore di file libero di numero più basso (la open(2) effettua una ricerca sequenziale dei descrittori liberi, partendo dal descrittore numero 0 e incrementando il numero finché non ne trova uno libero).

I seguenti comandi di shell simulano ciò che abbiamo descritto. Tutte queste operazioni le potete fare allo stesso modo da un programma C, senza intervento da parte della shell:

   $ myprg >tempfile
   $ mail -s "insert subject here" user@domain <tempfile
   $ rm tempfile

Usare la pipe, per lo meno da riga di comando è più semplice, ci sono meno caratteri da digitare e non bisogna ricordarsi di rimuovere il file temporaneo alla fine. Inoltre usare la pipe(2) risulta più efficiente perché i processi myprg e mail eseguono in parallelo o pseudo-parallelismo, anziché sequenzialmente. Siccome il buffer della pipe(2) è tenuto in memoria, si evita di utilizzare il disco e anche questo aumenta l'efficienza. Questo può essere importante soprattutto quando i dati da trasferire sequenzialmente da un processo all'altro sono molti e l'azione stessa del trasferimento viene ripetuta molte volte.

Usare le pipe nella shell è veramente semplice: basta digitare un solo carattere, | come separatore dei comandi da far comunicare tramite una pipe. Ovviamente non abusate delle pipe. Se potete farne a meno meglio non usarle. Il seguente esempio:

   $ cat testo.txt | wc
        319    2032   14054
può essere riscritto più semplicemente ed efficientemente come segue:
   $ wc <testo.txt
        319    2032   14054
o, equivalentemente come prestazioni,
   $ wc testo.txt
        319    2032   14054 testo.txt

L'ultima forma è preferibile: lasciate che sia wc(1) ad aprire il file, in modo che conosca da quale file su disco provengano i dati e ne stampi il nome nel report. Il programma wc(1), che può operare efficientemente anche su file molto grandi, prevede la specifica del nome del file di input come argomento. In mancanza di questo viene utilizzato lo stdin.

Tornando al nostro programma che riutilizza mail(1), ecco come si presenta la nostra prima versione:

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

#define READ  0
#define WRITE 1

int main()
{ int p[2];

  /* ... */

  if (pipe(p)<0)
    perror(NULL); /* segnala l'errore se la creazione della pipe fallisce */
  else
  { pid_t pid=fork();

    if (pid==0)
    { /* processo figlio */
      close(p[WRITE]); /* chiude il lato scrittura della pipe */
      close(0);
      dup(p[READ]);
      close(p[READ]); /* lo stdin è ora il lato lettura della pipe */
      execl("/usr/bin/mail", "mail", "-s", "insert subject here",
        "user@domain", NULL);
      _exit(EXIT_FAILURE); /* esci con codice di errore se la execl fallisce */
    }

    /* processo padre */
    if (pid==-1)
      perror(NULL); /* segnala l'errore se la fork fallisce */
    else
    { close(p[READ]); /* chiude il lato lettura della pipe */

      /* scrive la email nella pipe
         usando il descrittore p[WRITE] con la write(2) */
      write(p[WRITE], "message in a bottle\n", 20);

      close(p[WRITE]); /* segnala a mail(1) che l'input è finito */
      /* attendi la terminazione di mail(1)
         nell'ipotesi che sia l'unico figlio */
      wait(NULL);
    }
  }

  /* ... */

  exit(EXIT_SUCCESS);
}

Osservate che affinché i test che rilevano la fine dell'input da parte di mail(1) operino correttamente è necessario che il processo figlio che esegue mail(1) chiuda prima il lato scrittura della pipe, altrimenti esso stesso è uno scrittore potenzialmente attivo e mail(1) non vedrà mai la fine dell'input. Questo perché con la fork(2) i descrittori vengono copiati, ma referenziano gli stessi oggetti che rappresentano i file all'interno del kernel. Quando tutti gli scrittori hanno chiuso il file relativo all'estremità di scrittura della pipe, il lettore dopo averla svuotata incontrerà l'eof. Se provate a commentare o cancellare l'istruzione close(p[WRITE]);, poi ricompilate ed eseguite, vedrete che il processo padre rimane indefinitamente bloccato ad aspettare che il processo figlio mail(1) finisca, ma questo non finisce mai perché non vede mai la fine del suo input! Il ps(1) continua a mostrare entrambi i processi nello stato I (idle). In gergo si dice che si è verificata una situazione di deadlock e per uscirne non resta altro da fare che uccidere i due processi, o solo il processo che esegue mail(1) per consentire al padre di continuare (ma la mail non verrà mandata).

La libreria standard del C mette a disposizione una funzione portabile e molto più semplice da usare che crea un processo e permette al processo padre di comunicare con questo nuovo processo tramite una pipe. Si tratta della popen(3) dichiarata nell'header standard <stdio.h>:

FILE *
     popen(const char *command, const char *type);

La popen(3) opera similmente a come abbiamo fatto noi prima: prima viene creata la pipe, poi si forka e si invoca la shell. Mentre noi abbiamo chiamato il comando mail(1) direttamente con la execl(3), la popen(3) invece passa la stringa C command (ossia una stringa null-terminata) alla shell /bin/sh usando il parametro -c, in questo modo:

execl("/bin/sh", "sh", "-c", "mail -s \"insert subject here\" user@domain", 0);

Questo significa che command può essere in generale una qualsiasi riga di comando della shell e provvede tutte le feature messe a disposizione dalla shell (ricerca automatica del path dell'eseguibile, ed espansione dei metacaratteri nella lista degli argomenti). In questo caso non ci servivano queste feature, ecco perché avevo chiamato mail direttamente, senza mettere di mezzo la shell.

L'argomento type pure è una stringa C che non verrà modificata da popen(3). Questa stringa deve essere o "r" se il processo padre che invoca la popen(3) desidera leggere nella pipe (ossia agire da consumatore), oppure "w" se il processo padre vuole essere il produttore, vale a dire vuole scrivere nella pipe.

Come abbiamo detto FreeBSD da tempo ha operato una estensione del concetto di pipe e permette di usarle anche in modo bidirezionale: in tal caso il parametro type deve valere "r+", che sta ad indicare che il processo padre può agire sia da produttore che da consumatore e potrà usare uno stesso estremo della pipe sia per leggere che per scrivere, ma questa feature non è portabile tra tutte le versioni di UNIX, perciò dovrebbe essere usata solo quando la portabilità non è importante. Infatti altre implementazioni di popen(3) in versioni di UNIX diverse da FreeBSD permettono solo i valori "r" e "w" per l'argomento type, e non entrambe le cose, in quanto usano pipe unidirezionali.

Il valore di ritorno di popen(3) non è un descrittore, ma un oggetto stream della libreria standard di I/O del C, che è buona parte della libreria standard del C (se invece si verifica un errore all'atto della fork(2) o della syscall pipe(2) la popen ritorna NULL). La prima è detta libreria stdio, mentre la seconda libreria libc. Si tratta di una libreria a collegamento statico o dinamico (/usr/lib/libc.a, /usr/lib/libc.so) e il compilatore C fa sì che sia collegata automaticamente al vostro programma se ne usate le funzioni.

Gli stream in C sono dei puntatori ad una struttura chiamata FILE (definita tramite typedef, sicché FILE è un nome di tipo), la cui definizione viene ottenuta includendo l'header file <stdio.h>. La struttura FILE contiene tutte le informazioni sul file aperto, tra le quali l'indirizzo di un buffer (la libreria portabile di I/O del C infatti per default bufferizza pienamente sia l'input che output per ragioni di efficienza, a meno che non si tratti di stream associati al terminale che sono bufferizzati parzialmente, vedere stdio(3)), la posizione del carattere corrente (o del prossimo) nel buffer, la modalità con cui è aperto il file, il numero del descrittore, ecc. Il sistema UNIX non fa differenza tra stream di tipo testo e di tipo binario, anche se alcune implementazioni del C su altri sistemi la fanno. In effetti tutti gli stream in UNIX sono binari.

La libreria stdio mette a disposizione più di una sessantina di funzioni. Trovate la lista completa in stdio(3). Le più importanti e maggiormente usate sono, in ordine alfabetico:

funzione descrizione dichiarazione
fclose(3) chiude uno stream int fclose(FILE *stream);
fflush(3) scarica i buffer di uno stream int fflush(FILE *stream);
fgets(3) legge una linea da uno stream char *fgets(char *str, int size, FILE *stream);
fopen(3) apre uno stream FILE *fopen(const char *path, const char *mode);
fprintf(3) converte e formatta l'output int fprintf(FILE *stream, const char *format, ...);
fputs(3) scrive una linea su uno stream int fputs(const char *str, FILE *stream);
fscanf(3) legge e converte l'input formattato int fscanf(FILE *stream, const char *format, ...);
getc(3) legge il prossimo carattere da uno stream di input int getc(FILE *stream);
putc(3) scrive un carattere in uno stream int putc(int c, FILE *stream);
ungetc(3) rimette un carattere in uno stream di input int ungetc(int c, FILE *stream);

Per i dettagli si rimanda alle rispettive pagine di manuale in linea o ad un libro sul C. Tutte queste funzioni, ad eccezione della fopen(3), operano su uno stream passato per riferimento come argomento. Alcune varianti molto usate operano sugli stream predefiniti all'avvio del programma stdin e stdout. Questi sono due costanti di tipo (FILE *) (puntatore a FILE) che vengono definite includendo <stdio.h>. Rappresentano lo standard input e lo standard output rispettivamente e sono normalmente connessi al terminale, ma possono essere rediretti su file o sulle pipe:

funzione descrizione dichiarazione
getchar(3) legge il prossimo carattere da stdin int getchar();
gets(3) legge una linea da stdin (deprecata perchè insicura) char *gets(char *str);
scanf(3) legge e converte l'input formattato da stdin int scanf(const char *format, ...);

funzione descrizione dichiarazione
printf(3) converte e formatta l'output standard int printf(const char *format, ...);
putchar(3) scrive un carattere su stdout int putchar(int c);
puts(3) scrive una linea su stdout (appende il newline) int puts(const char *str);

Lo stream ritornato da popen(3) viene connesso all'estremità richiesta della pipe. Se si scrive su questo stream si scrive in realtà sullo standard input del comando eseguito dalla popen(3); l'output standard di questo comando risulterà essere lo stesso del processo che ha chiamato la popen(3), a meno che non venga alterato dal comando stesso, cosa in genere rara. Per converso, leggendo dallo stream ritornato da popen(3), si legge in realtà dall'output standard del comando eseguito, mentre lo standard input del comando sarà lo stesso di quello del processo che ha chiamato la popen(3). Invece eventuali stream aperti da precedenti chiamate a popen(3) nel processo genitore vengono chiusi nel nuovo processo figlio.

Quello ritornato da popen(3) è un normale stream di I/O standard, e per defaul viene pienamente bufferizzato. L'unica differenza rispetto agli altri stream di I/O è che dovrebbe essere chiuso tramite pclose(3) anziché fclose(3). La pclose(3) esegue una wait(2) o una sua variante per aspettare che il processo figlio che opera sulla pipe termini e ne raccoglie lo stato come ritornato dalla wait(2) e similari:

int
     pclose(FILE *stream);
pclose(3) ritorna -1 in caso la wait(2) ritorni -1, oppure in caso il parametro stream passato non è stato ottenuto da una popen(3), oppure se lo stream è stato già chiuso.

La ragione per cui occorre eseguire la wait(2), e anche noi lo avevamo fatto nel nostro programma C di prima, è che in UNIX quando un processo termina tramite exit(3) o _exit(2) non viene completamente eliminato. La maggior parte delle risorse vengono rilasciate, ma alcune informazioni di stato riguardanti la terminazione rimangono nel sistema, in attesa che il processo che lo ha generato (il padre) le raccoglia. Il processo padre con queste informazioni sà se la terminazione del processo figlio è avvenuta o no correttamente, che tipo di problema si è verificato o quante risorse ha usato il processo figlio durante la sua esecuzione e può decidere il da farsi (ad esempio lanciare un altro processo figlio per rimpiazzarne uno terminato). Finché questo non avviene il processo terminato entra in uno stato detto zombie (o defunct o dead) e indicato con Z da ps(1). Se il processo padre termina, in modo anomalo a causa di un errore, prima di raccogliere lo stato dei processi figli oppure perché il programmatore ha dimenticato di farlo, su alcuni sistemi lo stato sarà raccolto periodicamente dal processo di inizializzazione del sistema, init(8), dopodichè ogni traccia dell'esecuzione del processo scomparirà dalla memoria. Naturalmente init(8) semplicemente ignorerà le informazioni di stato non sapendo come trattarle: semplicemente distrugge i processi defunti. Su altri sistemi invece i processi defunti vengono distrutti senza che init(8) ne diventi il genitore.

Proprio questo di liberare la memoria è il motivo per cui occorre che qualcuno raccolga quanto prima questo stato di terminazione dei processi, oppure che il processo padre dichiari al sistema operativo che non intende raccoglierlo (vedremo in seguito come fare quando tratteremo dei segnali). Il sistema potrebbe porre un limite al numero di processi figli di cui il padre non ha ancora raccolto lo stato tramite la wait(2), anche se questi sono processi che hanno terminato. Fare la wait(2) è l'unico modo per fare uscire completamente questi processi dal sistema, a meno che si sia scelto di non raccogliere gli stati di terminazione (cosa che non abbiamo ancora detto come si fa), e in tal caso non è una cattiva pratica di programmazione non fare la wait(2). Il kernel sa che deve distruggere subito completamente un processo alla terminazione, senza dover salvare alcuna informazione di stato nella tabella dei processi. Una buona pratica di codice consiste nell'evitare che si creino zombie: quindi un processo dovrebbe eseguire la wait(2) su tutti i processi figli che forka oppure dichiarare al sistema che non intende raccoglierne lo stato. Questo è particolarmente importante per quei programma che creano molti processi figli in poco tempo, ma è una buona norma da seguire anche se i processi figli sono pochi.

La nuova versione del nostro programma che usa la popen(3) e la pclose(3) è molto più semplice e portabile:

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

int main()
{ FILE *fout;

  /* ... */

  if ((fout=popen("mail -s \"insert subject here\" user@domain", "w"))==NULL)
    perror(NULL);
  else
  { /* scrive la email nella pipe
       usando lo stream fout con la fputs(3) */
    fputs("message in a bottle\n", fout);

    /* segnala a mail(1) che l'input è finito
       e attendine la terminazione */
    pclose(fout);
  }

  /* ... */

  exit(EXIT_SUCCESS);
}

Notate che la popen(3) e la pclose(3) sono una coppia di funzioni che va usata in modo similare a come si usa la coppia fopen(3) e fclose(3).


Prec Indice Succ
exit, _exit Livello superiore mkfifo