Creazione di processi

Creazione di un processo

La creazione di un processo richiede alcune operazioni da parte del Sistema Operativo:

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 e detto parent (genitore), mentre il processo creato child (figlio). Si genera una struttura di “parentela” ad albero.

Relazioni Dinamiche

Cosa accade dopo la creazione?
Ci sono due possibilità:

  1. 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 5

    NOTA: il terminale e associato al nuovo programma: ctrl-c, ad esempio, termina il programma.

Relazioni dinamiche

  1. Il processo genitore continua.

    Esempio: Esecuzione in background di un programma da shell:

    $ sleep 5 &
    [1] 20
    $ ps
      PID TTY          TIME CMD
       11 pts/0    00:00:00 bash
       20 pts/0    00:00:00 sleep
       21 pts/0    00:00:00 ps
    $

    In questo caso i due processi procedono in modo concorrente. Tramite ps possiamo osservare i processi in esecuzione.

Visualizzare le relazioni

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à:

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.

La fork crea un nuovo processo che

Schema di utilizzo della “fork”

Come fanno i processi a differenziarsi? Si utilizza il valore di ritorno:

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!

Esempio

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());
}

Alternanza di esecuzione

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());
            fflush(stdout);
        }

    } else { 
        while(1) {
            for (i=0;i<10000;i++) {} // riduce il numero di printf
            printf("genitore: pid = %d, pid genitore = %d\n",getpid(), getppid());
            fflush(stdout);
        }
    }
}

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. ulimit e, in Linux, cgroups limitano il numero massimo di risorse allocabili evitando che si blocchi tutto il sistema (provare ulimit -u)!

// fork bomb, usare a proprio rischio e pericolo!
int main() {
    while(1)
        if (fork() < 0) 
            perror ("errore fork");
}

Processi orfani

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 genitore = %d\n",getpid(), getppid());

NOTA Un processo orfano non viene più terminato da ctrl-c (utilizzare ps -o pid,ppid,comm per vedere che il processo è ancora attivo, eventualmente con l’opzione -e).

Processi zombie

Gli zombie sono processi terminati ma in attesa che il genitore rilevi il loro stato di terminazione. 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 genitore = %d\n",getpid(),getppid());

Testare la presenza di zombie con ps -e  da un altro terminale oppure lanciando il programma in background tramite &:

$ fork &
... 
ps -e | grep fork
 5118 pts/3    00:00:00 fork
 5119 pts/3    00:00:00 fork <defunct>