Linguaggio C - librerie standard per la gestione dei processi

Fra le funzioni disponibili nelle librerie del C, ce ne sono alcune che consentono la gestione dei processi, permettendo:
o la nascita di un nuovo processo (fork())
o l'attesa di un processo figlio (wait())
o la trasformazione di un processo corrente (funzioni del gruppo exec())
o l'invio di segnali fra processi (libreria signal.h)
o la condivisione di file a scopo di comunicazione o sincronizzazione fra processi (pipe)

Generalita' sui processi (Unix)

Con il termine processo si intende il programma in esecuzione con l'impegno di piu' risorse (CPU, memoria, file system, ...) che deve condividere con gli altri processi presenti sul medesimo calcolatore.
L'esecuzione di un programma puo' generare piu' di un processo (vedi
fork()).

Ogni processo Unix e' contraddistinto da un identificatore di processo (process ID) indicato con pid e da un identificatore del processo padre che viene indicato con ppid (parent process ID).

Il processo padre (parent process) e' il processo che genera un altro processo, detto processo figlio (child process).
Piu' propriamente il processo padre dovrebbe essere chiamato processo genitore dall'inglese (parent=genitore), ma il termine padre si adatta ugualmente bene allo scopo.

Il pid e' un numero univoco in un determinato istante, fornito dal sistema operativo e necessario ad identificare un processo in "corso" sul sistema.

Il ppid e' l'identificativo del padre del processo che si sta' esaminando. In altri termini il ppid e' il pid del processo che ha generato il processo considerato.

Un processo si puo' trovare in uno dei seguenti stati:
o Ready E' pronto per essere eseguito.
o Running E' in esecuzione su una CPU del sistema.
o Sleeping Attende un evento.
o Swapped Parte del processo e' stato trasferito su disco, per liberare la memoria per altri processi.
o Terminated Il processo e' terminato. Invio del segnale SIGCHLD al parent.
o Zombie Il processo ha terminato la sua esecuzione, ma il parent non ha raccolto il segnale SIGCHLD. Il processo mantiene ancora allocate delle risorse.

Fork di un processo

Un processo, durante la sua esecuzione, puo' creare un nuovo processo tramite la system call fork().
Il processo corrente e' detto processo padre o parent process, mentre il nuovo processo e' detto processo figlio o child process. Entrambi i processi condividono lo stesso codice programma ma vengono eseguiti in concorrenza fra loro assieme al resto dei processi elaborati sul sistema in oggetto. Normalmente il padre e il figlio eseguono delle istruzioni differenti.
Il child process eredita i file aperti dal parent process. Questi possono costituire un mezzo di interazione fra i 2 processi.
Diversamente i dati (le variabili del programma), lo stack e l'ambiente (environment) del parent process vengono duplicati per il nuovo processo e posti in un'area di memoria a lui riservata e non visibile dagli altri processi, parent compreso.

La fork() restituisce al parent o un valore negativo in caso di errore (il child process non viene generato), o un valore positivo corrispondente al pid del child.
La fork() ritorna al child un valore sempre nullo.

A titolo di esempio riporto un breve spezzone di codice:

esempio di fork()

In caso che venga rilevato un errore durante la creazione del nuovo processo, il codice deve essere in grado di gestirlo correttamente.
Nella figura sottostante, ho indicato in blu le istruzioni eseguite dal programma in caso di errore ritornato dalla system call fork().

fork() in errore

Normalmente il processo padre esegue le istruzioni del programma, fino ad incontrare la chiamata alla fork() (istruzioni in blu). A questo punto il processo padre genera un nuovo processo. Da questo momento i 2 processi hanno vita indipendente e il risultato tornato dalla fork() e' differente per ciascun processo.
E' normale che i 2 processi eseguano percorsi differenti all'interno dello stesso programma (istruzioni in blu).

esecuzione della fork()

Attesa di un processo

Per i processi generati con una fork() si possono creare uno dei seguenti casi:
o Il processo padre termina prima del processo figlio.
In questa situazione il child process rimane orfano del padre e viene adottato dal processo init che per definizione e' il padre di tutti i processi.
o Il processo figlio termina, ma il padre non rileva il suo termine.
Quando cio' si verifica, il processo figlio e' definito defunto oppure zombie e rimane in tale stato finche' o il padre non ha rilevato la sua terminazione, oppure fino a quando anche il padre termina; al termine del padre il processo figlio viene ereditato dal processo init che ne rileva la sua terminazione.
Un processo zombie mantiene allocate le risorse fino quando non sia stato rilevato il suo stato di terminazione o dal processo padre o dal processo init.

Nella situazione di normalita', il padre deve quindi rilevare la terminazione del figlio tramite la system call wait(). Il figlio puo' cosi' rilasciare ogni risorsa impegnata.

Funzioni fork() e wait()

Legenda della numerazione posta a fianco del flusso di processo:

  1. E' in esecuzione il solo processo padre.
  2. Il processo padre invoca la funzione fork(). Viene generato il processo figlio.
  3. Il processo padre e' eseguito in concorrenza con il processo figlio (oltre agli altri processi running sul sistema).
    Se il padre invoca la funzione wait(), il padre attende la terminazione del figlio.
  4. Il processo figlio e' eseguito in concorrenza con il processo padre (oltre agli altri processi running sul sistema).
  5. Il figlio ha eseguito la terminazione del programma invocando la funzione _exit() o exit().
    Al padre e' inviato il segnale SIGCHLD.
  6. Il padre ha rilevato la terminazione del figlio tramite la funzione wait() e le risorse impegnate dal figlio vengono liberate.
    Il processo padre riprende l'esecuzione fino alla sua terminazione.

Funzioni del gruppo exec

Le funzioni del gruppo exec sono in grado di sostituire il processo corrente con un altro processo. Il pid e il ppid rimangono invariati. Praticamente si ha una trasformazione del processo.

Quando una funzione del gruppo exec viene eseguita con successo, il processo carica il programma o lo script indicato fra gli argomenti della funzione chiamata e lo manda in esecuzione in sostituzione del processo attuale. Non e' previsto nessun tipo di ritorno al vecchio processo se non nel caso che non sia possibile avviare il nuovo processo.

Comunicazione fra processi: la pipe

Le pipe (condotte) sono dei canali unidirezionali per la comunicazione fra processi.
Ogni processo puo' trattare le pipe come se fossero dei file standard, con le dovute eccezioni:

Le pipe costituiscono un meccanismo di sincronizzazione fra processo produttore e processo consumatore.

Funzionamento della pipe

Un processo padre:

In alternativa un processo padre puo' anche:

Un processo consumatore (lettore della pipe):

Un processo produttore (scrittore della pipe):


Le funzioni per la gestione dei processi non sono definite da una libreria specifica, ma fanno riferimento a piu' librerie standard; pertanto per il loro utilizzo e' necessario includere gli headers appropriati come descritto nella sinopsi di ciascuna funzione. Es.:

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

Funzioni della libreria stdlib.h

o atexit()
o exit()
o on_exit()
o system()

Funzioni della libreria stdio.h

o popen(), pclose()

Funzioni della libreria unistd.h

o _exit()
o alarm()
o execl(), execv(), execle(), execlp(), execvp()
o execve()
o fork()
o pause()
o pipe()
o sleep()

Funzioni della libreria sys/wait.h

o wait(), waitpid()

Funzioni della libreria signal.h

o kill()
o killpg()
o signal()


Indice-C Indice linguaggio C
Indice librerie Indice librerie C
At Home Umberto Zappi Home Page