Introduzione ai sistemi operativi con UNIX
Prec 5. I processi Succ

5.1 fork

Adesso descriveremo meglio la primitiva UNIX per creare un nuovo processo, la fork(2).

pid_t
     fork(void);

Il seguente è lo scheletro essenziale (ma eseguibile!) di un programma C che forka un processo demone, distaccandosi dal terminale di controllo:

/* daemon.c */

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

int main(int argc, char *argv[])
{ pid_t pid;

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

    case 0: /* we are the child */
      /* detach from the controlling terminal */
      if (setsid()==-1) perror(*argv);

      /* loop forever performing tasks */
      while(1)
      { /* do something interesting here */
      }
    break;

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

pid_t corrisponde ad int in FreeBSD, ma per ragioni di portabilità è meglio usare pid_t.

La fork(2) non crea un nuovo processo, bensì crea una nuova copia del processo esistente che la invoca. Entrambi i processi continueranno l'esecuzione in (pseudo)parallelismo. Nel processo figlio la fork(2) ritorna sempre zero, mentre nel processo padre ritorna il pid del processo figlio (che è sempre diverso da zero!).

È buona norma comunque, come è stato fatto nell'esempio precedente, gestire anche la condizione di errore: può capitare che la tabella dei processi sia piena o che comunque il sistema impone un limite al numero di processi che un certo utente può avere in esecuzione contemporaneamente o che sia esaurito lo spazio di swap per il processo, ecc. In tali casi la fork(2) non crea alcun processo figlio, setta errno e ritorna -1 al processo padre.

Per tabella dei processi si intende la tabella dei descrittori dei processi. Poichè il kernel UNIX prealloca un certo numero di descrittori, ossia un array di descrittori di dimensione prestabilita dalla configurazione del kernel, la dimensione di questo array limita il massimo numero di processi che si possono avere, in quanto ogni processo deve avere se non l'intero descrittore almeno una porzione del descrittore sempre residente in memoria principale (RAM).

Occorre precisare che i due processi padre e figlio continuano l'esecuzione indipendentemente e difatti la loro esecuzione può prendere strade diverse se viene testato il valore di ritorno della fork(2), che abbiamo visto è differente. I processi padre e figlio avranno differenti pid, perchè il pid di un processo è unico, ma condivideranno lo stesso segmento di codice, ossia eseguiranno lo stesso programma. Infatti abbiamo già detto che UNIX non duplica lo spazio codice, in quanto non permette ad un programma di modificare a runtime il suo codice (programma mutante). Questa funzionalità non è utile e sarebbe solo insicura. Meccanismi hardware consentono la protezione da scrittura del segmento codice in modalità utente. Quindi duplicare lo spazio codice che è a sola lettura sarebbe un inutile spreco. Il segmento dati invece è a lettura/scrittura perchè ovviamente un programma che gira in modo utente ha bisogno di modificare i suoi dati in memoria (variabili globali e memoria dinamica) e non viene condiviso. Questo significa che la fork produce una copia del segmento dati e che processo padre e processo figlio hanno due copie separate del segmento dati. Anche se appena tornati dalla fork(2) i segmenti dati hanno lo stesso contenuto essendo stati appena copiati, poi evolvono in modo generalmente diverso (potranno persino avere dimensioni diverse in quanto la dimensione viene aggiustata dinamicamente). Similmente anche il segmento stack (per le variabili automatiche) viene duplicato e non condiviso. Il ppid sarà ovviamente differente tra i processi padre e figlio (padre e figlio non possono avere lo stesso padre!). Invece tutti i file aperti sono condivisi tra processo padre e figlio.

Il processo figlio quindi avrà aperti tutti i file che aveva aperto il processo padre prima di forkare, compresi lo standard input ed output. Naturalmente, ad esempio, l'input viene letto una sola volta: se il processo figlio legge una parte dell'input, il processo padre potrà leggerne solo una parte successiva.

Notate che alla terminazione del processo padre daemon, il processo figlio daemon, rimasto ancora attivo ed orfano, viene adottato da init(8), quindi il suo ppid sarà 1.

Ho usato uno switch come struttura di controllo per testare il valore di ritorno della fork(2), ma potete usare anche una serie di if ... else annidati, se preferite.

Ecco la situazione quando daemon è lanciato dall'utente ant:

$ ./daemon
$ ps -x
  PID  TT  STAT      TIME COMMAND
  ...  ..  ....      .... .......
  450  ??  Rs     0:05.02 ./daemon
  ...  ..  ....      .... .......
$ ps -xj
USER   PID  PPID  PGID   SESS JOBC STAT  TT       TIME COMMAND
....   ...  ....  ....   .... .... ....  ..       .... .......
ant    450     1   450 c1145bc0    0 Rs    ??    0:23.13 ./daemon
....   ...  ....  ....   .... .... ....  ..       .... .......
$ kill 450

Il corpo del ciclo while è vuoto per semplicità; questo implica che, se lo fate eseguire, il processo daemon continuerà a girare e ad occupare una buona frazione del tempo di CPU senza che faccia qualcosa di utile (come si dice in gergo monopolizza la CPU, e in inglese si usa il verbo to hog per descrivere la situazione); in un caso reale occorre che all'interno del while venga fatta qualche elaborazione utile e tipicamente accadrà anche che ad un certo punto il processo si blocchi in attesa di qualche evento, ma siccome il nostro esempio è solo illustrativo ho preferito non complicarlo. Potete comunque inserire nel while una semplice istruzione tipo sleep(numero_di_secondi) se volete che il processo non occupi inutilmente troppo tempo di CPU. Provate ad inserire la sleep(3) e vedrete che l'indicazione TIME di ps(1) per il processo ./daemon ora rimane molto vicina allo zero. Date anche un'occhiata al comando daemon(8). Usate kill(1) per terminare il processo.


Prec Indice Succ
I processi Livello superiore execl, execve