La creazione di un processo richiede alcune operazioni da parte del Sistema Operativo:
- Creazione nuovo ID (PID – Process Identifier),
- Allocazione Memoria (Codice, Dati),
- Allocazione altre risorse (stdin, stdout, dispositivi I/O in genere),
- Gestione informazioni sul nuovo processo (es. priorità),
- Creazione PCB (Process Control Block) contenente le informazioni precedenti.
Processi in Unix
Un processo è sempre creato da un altro processo tramite un’opportuna chiamata a sistema. Fa eccezione init (pid = 1) che viene creato al momento del boot.
Il processo creante è detto parent (genitore), mentre il processo creato child (figlio). Si genera una struttura di “parentela” ad albero.
Relazioni Dinamiche
Cosa accade dopo la creazione?
- Il processo genitore attende il processo figlio.
Esempio: Esecuzione di un programma da shell$ sleep 5 ( ... 5 secondi di attesa ... )
La shell è genitore del nuovo processo
sleep
NOTA: il terminale è associato al nuovo programma:ctrl-c, ad esempio, terminasleep. - Il processo genitore continua.
Esempio: Esecuzione in background di un programma da shell:$ sleep 5 & [1] 17589 ps PID TTY TIME CMD 14695 pts/1 00:00:00 bash 17589 pts/1 00:00:00 sleep 17590 pts/1 00:00:00 ps $
In questo caso i due processi procedono in modo concorrente. Tramite
pspossiamo osservare i processi in esecuzione.NOTA:
psdi default mostra solo i processi associati al terminale da cui viene lanciato. In questo caso abbiamo la shellbash, il programmasleepe lo stessops.Anche in esecuzione rimane un legame genitore-figlio. Ad esempio, quando
sleeptermina la shell viene notificata:$ [1]+ Done sleep 5 $
Può essere utile che un processo si dissoci dal processo genitore. Ad esempio i Daemon sono processi che restano attivi fino allo shutdown del sistema: devono, ad esempio, dissociarsi dalla shell per non essere terminati alla chiusura del terminale stesso. Quando si dissociano diventano figli del processo
init(vedremo come questo sia possibile).
Per visualizzare il PID del genitore basta passare gli opportuni parametri al programma ps (PPID significa Parent process ID):
$ sleep 5 &
[1] 23
$ ps -o pid,ppid,comm
PID PPID COMMAND
11 10 bash
23 11 sleep
24 11 ps
$
Si osservi che sleep e ps sono entrambi figli di bash.
Relazioni di contenuto
Due possibilità:
- Il figlio è un duplicato del genitore (ad esempio in UNIX)
- Il figlio esegue un programma differente (ad esempio nei sistemi Windows)
Questo è il comportamento standard ma ovviamente è possibile anche l’altra modalità in entrambi i sistemi.
System Call “fork”
La chiamata a sistema fork permette di creare un processo duplicato del processo genitore.
NOTA fork, come la maggior parte delle chiamate a sistema che discuteremo, appartiene allo standard POSIX (Portable Operating System Interface) di IEEE (Institute of Electrical and Electronics Engineers). È utilizzabile in qualsiasi sistema che supporti POSIX.
La fork crea un nuovo processo che
- condivide l’area codice del processo genitore
- utilizza una copia dell’area dati del processo genitore
Come fanno i processi a differenziarsi? Si utilizza il valore di ritorno della fork:
- =0 Processo figlio
- >0 Processo genitore: il valore di ritorno è il PID del figlio.
Schema di base di utilizzo della fork:
pid = fork();
if ( pid < 0 )
perror("fork error"); // stampa la descrizione dell'errore
else if (pid == 0) {
// codice figlio
} else {
// codice genitore, (pid > 0)
}
// codice del genitore e del figlio: da usare con cautela!
NOTA: pid_t è un signed integer, cioè int in molti sistemi ma potrebbe essere di dimensioni differenti, es. long, ed è quindi bene usare sempre il tipo pid_t
Un esempio concreto:
// Esempio di utilizzo della fork.
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
int main() {
pid_t pid;
printf("Prima della fork. pid = %d, pid del genitore = %d\n",getpid(), getppid());
pid = fork();
if ( pid < 0 )
perror("fork error"); // stampa la descrizione dell'errore
else if (pid == 0) {
// figlio
printf("[Figlio] pid = %d, pid del genitore = %d\n",getpid(), getppid());
} else {
// genitore
printf("[Genitore] pid = %d, pid del mio genitore = %d\n",getpid(), getppid());
printf("[Genitore] Mio figlio ha pid = %d\n",pid);
sleep(1); // attende 1 secondo
}
// entrambi i processi
printf("PID %d termina.\n", getpid());
}
che dà il seguente output:
$ ./fork
Prima della fork. pid = 25267, pid del genitore = 329
[Genitore] pid = 25267, pid del mio genitore = 329
[Genitore] Mio figlio ha pid = 25268
[Figlio] pid = 25268, pid del genitore = 25267
PID 25268 termina.
PID 25267 termina.
$
Esempio Come possiamo visualizzare l’alternanza nell’esecuzione dei processi genitore e figlio? Un modo è mettere le printf dentro un while(1):
// visualizza l'esecuzione concorrente di genitore e figlio
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
int main() {
pid_t pid;
int i;
pid = fork();
if ( pid < 0 )
perror("fork error"); // stampa la descrizione dell'errore
else if (pid == 0) {
while(1) {
for (i=0;i<10000;i++) {} // riduce il numero di printf
printf("Figlio: pid = %d, pid del genitore = %d\n",getpid(), getppid());
}
} else {
while(1) {
for (i=0;i<10000;i++) {} // riduce il numero di printf
printf("genitore: pid = %d, pid di mio genitore = %d\n",getpid(), getppid());
}
}
}
In output avremo un’alternanza (non stretta a causa dell’output bufferizzato!) degli output dei due processi. Il ciclo for “vuoto” prima della printf riduce il numero di stampe e permette di visualizzare meglio l’alternanza (ed evita di “saturare” il terminale di output). Variare, se necessario, il limite.
NOTA Su computer a più core i processi vengono eseguiti in parallelo su core differenti. In tal caso non è necessario rallentarli con il ciclo for per vedere l’alternanza nell’output. Per vedere il carico sui vari core si può lanciare ‘top’ da un altro terminale. Il Linux, premendo 1, viene visualizzato il carico delle differenti CPU. Provare per esercizio.
Fallimento della fork
Quando fallisce una fork? Quando non è possibile creare un processo e tipicamente questo accade quando non c’è memoria per il processo o per il kernel. Ecco un piccolo test.
NOTA BENE. Il test potrebbe bloccare tutto il sistema perché i processi generati vanno ad occupare tutte le risorse e il sistema non può più prendere il controllo. È necessario limitare in numero di processi utenti tramite il comando ulimit -u num (al massimo num processi sono ammessi per l’utente sul presente terminale). Attenzione che il limite è per terminale quindi il test va eseguito dalla stessa finestra su cui avete impostato il limite. Nei sistemi Linux moderni il limite è preimpostato ed è solo possible abbassarlo. Verificare con ulimit -u che ci sia un valore massimo (che non sia unlimited) prima di eseguire il test. Nei kernel linux moderni cgroups limita comunque il numero massimo di risorse evitando che la “fork bomb” blocchi il sistema.
int main() {
while(1)
if (fork() < 0)
perror ("errore fork");
}
Esercizio. Quanti processi eseguono la fork al loop i-esimo? (pensare a quanti processi ci sono alla prima fork, quanti alla seconda, e cosi’ via).
Processi orfani e processi zombie
Se metto una sleep subito prima della printf nel figlio lo rendo orfano perché termina il genitore prima di lui: viene adottato da init (genitore di tutti i processi di sistema) o da upstart nei moderni sistemi Linux:
// figlio
sleep(5);
printf("[Figlio] pid = %d, pid del genitore = %d\n",getpid(), getppid());
ottengo:
$ ./fork
Prima della fork. pid = 396, pid del genitore = 354
[Genitore] pid = 396, pid del mio genitore = 354
[Genitore] Mio figlio ha pid = 397
PID 396 termina.
<===== 5 secondi
$ [Figlio] pid = 397, pid del genitore = 1 <===== processo adottato!
PID 397 termina.
dove si nota, appunto, l’adozione da parte di init (1).
NOTA 1: se utilizzate testbed in docker, 1 non è init bensì un processo sh in quanto docker utilizza una virtualizzazione a container.
NOTA 2: Un processo orfano non viene più terminato da ctrl-c (provare). Se invece il genitore è ancora in esecuzione tutti i processi figli vengono terminati quando il genitore viene terminato.
Gli zombie sono processi terminati ma in attesa che il genitore rilevi il loro stato di terminazione (vedremo come fare). Per osservare la generazione di un processo zombie ci basta porre la sleep prima della printf del processo genitore:
// genitore
sleep(5);
printf("[Genitore] pid = %d, pid di mio genitore = %d\n",getpid(),getppid());
che dà il seguente output:
$ ./fork & <== notare l'esecuzione in background
[1] 8270
Prima della fork. pid = 408, pid del genitore = 354
[Figlio] pid = 409, pid del genitore = 408
PID 409 termina. <== termina figlio e diventa zombie
$ ps <== andiamo ad osservare i processi
PID TTY TIME CMD
354 pts/2 00:00:00 bash
408 pts/1 00:00:00 ./fork <== genitore in esecuzione
409 pts/1 00:00:00 ./fork <defunct> <== zombie
410 pts/1 00:00:00 ps
$ <== 5 secondi
[Genitore] pid = 408, pid del mio genitore = 354
[Genitore] Mio figlio ha pid = 409
PID 408 termina.
[1]+ Done ./fork

