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!)
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).
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.
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:
Quindi se si fa fork
e subito exec
nessuna pagina viene effettivamente copiata.
La exec
ha diverse varianti che si differenziano in base al
argv[]
terminato da NULL
execl("/bin/ls", arg0, arg1, ..., NULL); execlp("ls", arg0, arg1, ..., NULL); execv("/bin/ls", argv); execvp("ls", argv);
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
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
}
Errore della exec
execlp("ls2","ls2","-l",NULL);
Cosa accade?
(provare a utilizzare perror
per stampare l’errore)
Errore nel programma eseguito
execlp("ls","ls","-z",NULL);
Cosa accade? Va in errore la exec
?
#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 )
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:
exit
: termina il processo (già usata negli esempi per i casi di errore);
wait
: attende la terminazione di un figlio (se uno dei figli è uno zombie ritorna subito senza bloccarsi).
exit(int stato)
: termina il processo ritornando lo stato
al genitore; Si usano le costanti EXIT_FAILURE
e EXIT_SUCCESS
che normalmente sono uguali ad 1 e 0 rispettivamente;
pid = wait(int &stato)
: ritorna il pid
e lo stato
del figlio che ha terminato. Si invoca wait(NULL)
se non interessa lo stato. Se non ci sono figli ritorna -1.
Lo stato ritornato da wait
va gestito con opportune macro:
WIFEXITED(status)==true
se figlio uscito con una exit
WEXITSTATUS(status)
ritorna 1 byte passato dalla exit
if (WIFEXITED(status))
printf("OK: status = %d\n",WEXITSTATUS(status));
WIFSIGNALED(status)==true
se figlio terminato in maniera anomala.
WTERMSIG(status)
ritorna il “segnale” che ha causato la terminazione.
if (WIFSIGNALED(status))
printf("ANOMALO: status = %d\n",WTERMSIG(status));
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));
}
}
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.