Esecuzione e terminazione

Esercizio

Considerare il seguente programma:

#include <unistd.h>
#include <stdio.h>

int main() {
  pid_t f1,f2,f3;
  
  f1=fork();
  f2=fork();
  f3=fork();

  printf("%i%i%i ", (f1 > 0),(f2 > 0),(f3 > 0)); 
}

Domanda: che output dà? Perché?
(provare prima di guardare la soluzione!)

Soluzione

L’output è una permutazione qualunque del seguente:

000 001 100 101 010 011 110 111

cioè tutti i numeri binari di 3 cifre in qualche ordine (dipendente dallo scheduling).

System call exec

Come si fa ad eseguire un programma diverso da quello che ha effettuato la fork? Esiste una chiamata a sistema apposita: exec.

La exec sostituisce codice e dati di un processo con quelli di un programma differente.

Copy-on-write

Viene copiata solamente la page-table, e le pagine (quelle contenenti i dati, che dovrebbero essere state copiate) sono invece etichettate come read-only. Un tentativo di scrittura genera un errore che viene gestito dal kernel:

  1. copiando al volo (copy-on-write, appunto) la pagina fisica e aggiornando opportunamente la page-table in modo che punti alla nuova copia;
  2. impostando la modalità a read-write: da quel momento in poi le due copie sono indipendenti.

Quindi se si fa fork e subito exec nessuna pagina viene effettivamente copiata.

Sintassi di exec

La exec ha diverse varianti che si differenziano in base al

 

execl("/bin/ls", arg0, arg1, ..., NULL);
execlp("ls", arg0, arg1, ..., NULL);
execv("/bin/ls", argv);
execvp("ls", argv);

Argomenti

Per convenzione, il primo argomento contiene il nome del file associato al programma da eseguire.

#include <stdio.h>
int main(int argc, char * argv[]) {
	int i;
	for(i=0;i<argc;i++) {
		printf("arg %d: %s\n",i,argv[i]);
	}
}

Dà il seguente output:

$ ./argv prova 1 2 3
arg 0: ./argv
arg 1: prova
arg 2: 1
arg 3: 2
arg 4: 3

Valore di ritorno

La exec ritorna solamente in caso di errore (valore -1).

In caso di successo il vecchio codice è completamente sostituito dal nuovo e non è più possibile tornare al programma originale.

#include <stdio.h>
#include <unistd.h>
int main() {
      printf("provo a eseguire ls\n");

      execl("/bin/ls","/bin/ls","-l",NULL); 
      // oppure : execlp("ls","ls","-l",NULL); 

      printf("non scrivo questo! \n");
      // questa printf non viene eseguita, se la exec va a buon fine
}

Errori

Esercizio 1

Errore della exec

execlp("ls2","ls2","-l",NULL);

Cosa accade?
(provare a utilizzare perror per stampare l’errore)

Esercizio 2

Errore nel programma eseguito

execlp("ls","ls","-z",NULL);

Cosa accade? Va in errore la exec?

Esempio: Simulare una shell

#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
int main() {
    int esito=1,i;
    char comando[128], *argv[128], *pch;
    while(1) {    
        printf("myshell# ");
        fgets(comando, 128, stdin); 
        pch = strtok (comando," \n"); // primo argomento
        for (i=0; pch != NULL && i < 127; i++) {
            argv[i] = pch;
            pch = strtok (NULL, " \n"); // argomento successivo
        }
        argv[i] = NULL; // termina l'array argv con NULL
        if ((argv[0] != 0) && (esito=fork()) < 0)
            perror("fallimento fork");
        else if (esito == 0) {
            execvp(argv[0],argv); // esegue il comando!
            perror("Errore esecuzione");
            exit(EXIT_FAILURE);
        }
    }    // il processo genitore (shell) torna a leggere un altro comando
}

Provare a compilarlo ed eseguirlo. Cosa si nota di anomalo? (usare anche ps )

Terminazione di un processo

La terminazione di un processo rilascia le risorse allocate dal SO al momento della creazione (ad esempio la memoria e i file aperti) e “segnala” la terminazione al genitore.

Parte della informazioni contenute nella PCB vengono mantenute dopo la terminazione, finché il processo genitore non ha eventualmente letto tali informazioni.

Il sistema mantiene almeno:

  1. il PID
  2. lo stato di terminazione
  3. il tempo di CPU utilizzato dal processo

System call exit e wait

Sintassi

Valore di ritorno della wait

Lo stato ritornato da wait va gestito con opportune macro:

Esempio completo

int main() {
    int pid,status;
    pid = fork();
    if ( pid < 0 ) { perror("errore fork"); exit(EXIT_FAILURE); }
    /* figlio 1: esce normalmente inviando al genitore lo stato "42" */
    if (pid == 0) {
        printf("Sono il figlio1! pid=%d ppid=%d\n",getpid(), getppid());
        sleep(3);
        exit(42);}
    pid = fork();
    if ( pid < 0 ) { perror("errore fork"); exit(EXIT_FAILURE); }
    /* figlio 2: segfault, cerca di accedere alla locazione 0 */
    if (pid == 0) {
        int *tmp=0;
        int a;
        printf("Sono il figlio2! pid=%d ppid=%d\n",getpid(), getppid());
        sleep(5);
        a = *tmp; } // segfault
    /* solo il genitore continua e attende tutti i figli ... */
    while((pid=wait(&status)) >= 0) {
        printf("ricevuta terminazione di pid=%d\n",pid);
        if (WIFEXITED(status))
            printf("OK: status = %d\n",WEXITSTATUS(status));
        else if (WIFSIGNALED(status))
            printf("ANOMALO: status = %d\n",WTERMSIG(status));
    }
}

Esercizi

Esercizio 1. Aggiungere le opportune wait, con relativa gestione dello stato, al codice della shell visto precedentemente.
Provare anche ad eseguire, tramite tale shell, programmi che effettuano errori run-time (come divisioni per zero).

Esercizio 2. Una tecnica per creare demoni (programmi in esecuzione che non sono sotto il controllo degli utenti, come i servizi di sistema) è quella di eseguire una doppia fork e far terminare il primo figlio: in questo modo il processo “nipote” viene adottato da init o da upstart e si distacca dal processo “nonno” definitivamente.
Scrivere il codice che realizzi questa tecnica facendo attenzione a non lasciare processi zombie.