Aggiunti zombie, orfani, e lupi mannari.
authorSimone Piccardi <piccardi@gnulinux.it>
Wed, 19 Sep 2001 17:10:49 +0000 (17:10 +0000)
committerSimone Piccardi <piccardi@gnulinux.it>
Wed, 19 Sep 2001 17:10:49 +0000 (17:10 +0000)
gapil.tex
prochand.tex
sources/ForkTest.c

index 4ea34f6417e1f8dc703b54ff0e1e41962b70c81d..310991538ca1befe7872932186b0d4be1e1d91fc 100644 (file)
--- a/gapil.tex
+++ b/gapil.tex
@@ -1,4 +1,4 @@
-%% 
+%%
 %% GaPiL : Guida alla Programmazione in Linux
 %%
 %% S. Piccardi Feb. 2001
index 69bdb16e4224a89cfe216fca14896f760804029b..b74cacdfa791b708e52caae0748dba8264e131ae 100644 (file)
@@ -78,9 +78,10 @@ affrontate in dettaglio in \secref{sec:proc_fork}).
 
 Se si vuole che il processo padre si fermi fino alla conclusione del processo
 figlio questo deve essere specificato subito dopo la \func{fork} chiamando la
-funzione \func{wait} o la funzione \func{waitpid}; queste funzioni
-restituiscono anche una informazione abbastanza limitata (il codice di uscita)
-sulle cause della terminazione del processo.
+funzione \func{wait} o la funzione \func{waitpid} (si veda
+\secref{sec:proc_wait}); queste funzioni restituiscono anche una informazione
+abbastanza limitata (lo stato di terminazione) sulle cause della terminazione
+del processo.
 
 Quando un processo ha concluso il suo compito o ha incontrato un errore non
 risolvibile esso può essere terminato con la funzione \func{exit} (si veda
@@ -146,9 +147,10 @@ usando le funzioni:
 \funcdecl{pid\_t getpid(void)} restituisce il pid del processo corrente.
 \funcdecl{pid\_t getppid(void)} restituisce il pid del padre del processo
     corrente.
-
 Entrambe le funzioni non riportano condizioni di errore. 
 \end{functions}
+esempi dell'uso di queste funzioni sono riportati in
+\figref{fig:proc_fork_code}, nel programma di esempio \file{ForkTest.c}.
 
 Il fatto che il \acr{pid} sia un numero univoco per il sistema lo rende il
 candidato ideale per generare ulteriori indicatori associati al processo di
@@ -168,10 +170,11 @@ identificativi associati ad un processo relativi al controllo di sessione.
 \subsection{La funzione \func{fork}}
 \label{sec:proc_fork}
 
-La funzione \func{fork} è la funzione fondamentale della gestione dei processi
-in unix; come si è detto l'unico modo di creare un nuovo processo è attraverso
-l'uso di questa funzione, che è quindi la base per il multitasking. Il prototipo
-della funzione è:
+La funzione \func{fork} è la funzione fondamentale della gestione dei
+processi: come si è detto l'unico modo di creare un nuovo processo è
+attraverso l'uso di questa funzione, essa quindi riveste un ruolo centrale
+tutte le volte che si devono scrivere programmi che usano il multitasking.  Il
+prototipo della funzione è:
 
 \begin{functions}
   \headdecl{sys/types.h} 
@@ -189,17 +192,17 @@ della funzione 
   \end{errlist}
 \end{functions}
 
-Dopo l'esecuzione di una \func{fork} sia il processo padre che il processo
-figlio continuano ad essere eseguiti normalmente alla istruzione seguente la
-\func{fork}; il processo figlio è però una copia del padre, e riceve una copia
-dei segmenti di testo, stack e dati (vedi \secref{sec:proc_mem_layout}), ed
-esegue esattamente lo stesso codice del padre, ma la memoria è copiata, non
-condivisa\footnote{In generale il segmento di testo, che è identico, è
-  condiviso e tenuto in read-only, Linux poi utilizza la tecnica del
-  \textit{copy-on-write}, per cui la memoria degli altri segmenti viene
-  copiata dal kernel per il nuovo processo solo in caso di scrittura, rendendo
-  molto più efficiente il meccanismo} pertanto padre e figlio vedono variabili
-diverse.
+Dopo il successo dell'esecuzione di una \func{fork} sia il processo padre che
+il processo figlio continuano ad essere eseguiti normalmente alla istruzione
+seguente la \func{fork}; il processo figlio è però una copia del padre, e
+riceve una copia dei segmenti di testo, stack e dati (vedi
+\secref{sec:proc_mem_layout}), ed esegue esattamente lo stesso codice del
+padre, ma la memoria è copiata, non condivisa\footnote{In generale il segmento
+  di testo, che è identico, è condiviso e tenuto in read-only, Linux poi
+  utilizza la tecnica del \textit{copy-on-write}, per cui la memoria degli
+  altri segmenti viene copiata dal kernel per il nuovo processo solo in caso
+  di scrittura, rendendo molto più efficiente il meccanismo} pertanto padre e
+figlio vedono variabili diverse.
 
 La differenza che si ha nei due processi è che nel processo padre il valore di
 ritorno della funzione fork è il \acr{pid} del processo figlio, mentre nel
@@ -214,8 +217,8 @@ viene eseguito dal padre o dal figlio.
 La scelta di questi valori non è casuale, un processo infatti può avere più
 figli, ed il valore di ritorno di \func{fork} è l'unico modo che permette di
 identificare quello appena creato; al contrario un figlio ha sempre un solo
-padre (il cui \acr{pid} può sempre essere ottenuto con \func{getppid}, vista
-in \secref{sec:proc_pid}) e si usa il valore nullo, che non può essere il
+padre (il cui \acr{pid} può sempre essere ottenuto con \func{getppid}, vedi
+\secref{sec:proc_pid}) e si usa il valore nullo, che non può essere il
 \acr{pid} di nessun processo.
 
 \begin{figure}[!htb]
@@ -237,26 +240,23 @@ int main(int argc, char *argv[])
  */
     int nchild, i;
     pid_t pid;
-    int wait_child=0;
-    int wait_parent=0;
-
+    int wait_child  = 0;
+    int wait_parent = 0;
+    int wait_end    = 0;
     ...        /* handling options */
-
-    /* There must be remaing parameters */
-    if (optind == argc) {
-        usage();
-    }
     nchild = atoi(argv[optind]);
     printf("Test for forking %d child\n", nchild);
     /* loop to fork children */
     for (i=0; i<nchild; i++) {
-        if ( (pid = fork()) < 0) {
-            printf("Error on %d child creation, %s\n", i, strerror(errno));
+        if ( (pid = fork()) < 0) { 
+            /* on error exit */ 
+            printf("Error on %d child creation, %s\n", i+1, strerror(errno));
+            exit(-1); 
         }
         if (pid == 0) {   /* child */
             printf("Child %d successfully executing\n", ++i);
             if (wait_child) sleep(wait_child);
-            printf("Child %d exiting\n", i);
+            printf("Child %d, parent %d, exiting\n", i, getppid());
             exit(0);
         } else {          /* parent */
             printf("Spawned %d child, pid %d \n", i+1, pid);
@@ -265,6 +265,7 @@ int main(int argc, char *argv[])
         }
     }
     /* normal exit */
+    if (wait_end) sleep(wait_end);
     return 0;
 }
   \end{lstlisting}
@@ -303,13 +304,13 @@ dell'output, \textit{user id}) prima della \func{exec}, rendendo molto pi
 flessibile la possibilità di modificare gli attributi del nuovo processo.
 
 In \curfig\ si è riportato il corpo del codice del programma di esempio
-\cmd{forktest}, che ci permette di illustrare l'uso della funzione
-\func{fork}. Il programma permette di creare un numero di figli specificato a
-linea di comando, e prende anche due opzioni \cmd{-p} e \cmd{-c} per indicare
-degli eventuali tempi di attesa (in secondi, eseguiti tramite la funzione
-\func{sleep}) per il padre ed il figlio; il codice completo, compresa la parte
-che gestisce le opzioni a riga di comando, è disponibile nel file
-\file{ForkTest.c}.
+\cmd{forktest}, che ci permette di illustrare molte caratteristiche dell'uso
+della funzione \func{fork}. Il programma permette di creare un numero di figli
+specificato a linea di comando, e prende anche alcune opzioni per indicare
+degli eventuali tempi di attesa in secondi (eseguiti tramite la funzione
+\func{sleep}) per il padre ed il figlio (con \cmd{forktest -h} si ottiene la
+descrizione delle opzioni); il codice completo, compresa la parte che gestisce
+le opzioni a riga di comando, è disponibile nel file \file{ForkTest.c}.
 
 Decifrato il numero di figli da creare, il ciclo principale del programma
 (\texttt{\small 28--40}) esegue in successione la creazione dei processi figli
@@ -318,23 +319,27 @@ controllando il successo della chiamata a \func{fork} (\texttt{\small
 suo numero di successione, eventualmente attendere il numero di secondi
 specificato e scrivere un messaggio prima di uscire. Il processo padre invece
 (\texttt{\small 29--31}) stampa un messaggio di creazione, eventualmente
-attende il numero di secondi specificato, e procede nell'esecuzione del ciclo.
-Se eseguiamo il comando senza specificare attese (il default è non attendere),
+attende il numero di secondi specificato, e procede nell'esecuzione del ciclo;
+alla conclusione del ciclo, prima di uscire, può essere specificato un altro
+periodo di attesa.
+
+Se eseguiamo il comando senza specificare attese (come si può notare in
+\texttt{\small 17--19} i valori di default specificano di non attendere),
 otterremo come output sul terminale:
 \begin{verbatim}
 [piccardi@selidor sources]$ ./forktest 3
 Test for forking 3 child
-Spawned 1 child, pid 2038 
+Spawned 1 child, pid 1964 
 Child 1 successfully executing
-Child 1 exiting
+Child 1, parent 1963, exiting
 Go to next child 
-Spawned 2 child, pid 2039 
+Spawned 2 child, pid 1965 
 Child 2 successfully executing
-Child 2 exiting
+Child 2, parent 1963, exiting
 Go to next child 
 Child 3 successfully executing
-Child 3 exiting
-Spawned 3 child, pid 2040 
+Child 3, parent 1963, exiting
+Spawned 3 child, pid 1966 
 Go to next child 
 \end{verbatim} %$
 
@@ -380,25 +385,25 @@ che otterremo 
 [piccardi@selidor sources]$ cat output
 Test for forking 3 child
 Child 1 successfully executing
-Child 1 exiting
+Child 1, parent 1967, exiting
 Test for forking 3 child
-Spawned 1 child, pid 836 
+Spawned 1 child, pid 1968 
 Go to next child 
 Child 2 successfully executing
-Child 2 exiting
+Child 2, parent 1967, exiting
 Test for forking 3 child
-Spawned 1 child, pid 836 
+Spawned 1 child, pid 1968 
 Go to next child 
-Spawned 2 child, pid 837 
+Spawned 2 child, pid 1969 
 Go to next child 
 Child 3 successfully executing
-Child 3 exiting
+Child 3, parent 1967, exiting
 Test for forking 3 child
-Spawned 1 child, pid 836 
+Spawned 1 child, pid 1968 
 Go to next child 
-Spawned 2 child, pid 837 
+Spawned 2 child, pid 1969 
 Go to next child 
-Spawned 3 child, pid 838 
+Spawned 3 child, pid 1970 
 Go to next child 
 \end{verbatim}
 che come si vede è completamente diverso da quanto ottenevamo sul terminale.
@@ -530,11 +535,18 @@ trattarla ulteriormente.
 \subsection{La conclusione di un processo.}
 \label{sec:proc_termination}
 
-In \secref{sec:proc_conclusion} abbiamo già affrontato le tre modalità con cui
-si conclude un programma in maniera normale: la chiamata di \func{exit} (che
-esegue le funzioni registrate e chiude gli stream), il ritorno dalla funzione
-\func{main} (equivalente alla chiamata di \func{exit}), e la chiamata ad
-\func{\_exit} (che esegue direttamente la terminazione del processo).
+In \secref{sec:proc_conclusion} abbiamo già affrontato le modalità con cui
+concludere un programma, ma dal punto di vista del programma stesso; avendo a
+che fare con un sistema multitasking occorre adesso affrontare l'argomento dal
+punto di vista generale di come il sistema gestisce la conclusione dei
+processi.
+
+Abbiamo già visto in \secref{sec:proc_conclusion} le tre modalità con cui un
+programma viene terminato in maniera normale: la chiamata di \func{exit} (che
+esegue le funzioni registrate per l'uscita e chiude gli stream), il ritorno
+dalla funzione \func{main} (equivalente alla chiamata di \func{exit}), e la
+chiamata ad \func{\_exit} (che passa direttamente alle operazioni di
+terminazione del processo da parte del kernel).
 
 Ma oltre alla conclusione normale abbiamo accennato che esistono anche delle
 modalità di conclusione anomala; queste sono in sostanza due: il programma può
@@ -545,27 +557,137 @@ seconda, dato che \func{abort} si limita a generare il segnale
 
 Qualunque sia la modalità di conclusione di un processo, il kernel esegue
 comunque una serie di operazioni: chiude tutti i file aperti, rilascia la
-memoria che stava usando, e così via. Ma per ciascuna delle varie modalità
-di chiusura al padre deve essere riportato come il figlio è terminato. 
-
-Nel caso di conclusione normale per riportare lo stato di uscita del processo
-viene usato l'\textit{exit status} specificato dal valore passato alle
-funzioni \func{exit} o \func{\_exit} (o dal valore di ritorno per
-\func{main}).  Ma se il processo viene concluso in maniera anomala è il kernel
-che deve generare un \textit{termination status} per indicare le ragioni della
-conclusione anomala. Si noti che si è distinto fra \textit{exit status} e
-\textit{termination status} in quanto anche in caso di conclusione normale, il
-kernel usa il primo per produrre il secondo.
-
-In ogni caso il valore dello stato di conclusione del processo può essere
-letto attraverso le funzioni \func{wait} o \func{waitpid}.
+memoria che stava usando, e così via; l'elenco completo delle operazioni
+eseguite alla chiusura di un processo è il seguente:
+\begin{itemize}
+\item tutti i descrittori dei file sono chiusi.
+\item viene memorizzato lo stato di terminazione del processo.
+\item ad ogni processo figlio viene assegnato un nuovo padre.
+\item viene inviato il segnale \macro{SIGCHLD} al processo padre.
+\item se il processo è un leader di sessione viene mandato un segnale di
+  \macro{SIGHUP} a tutti i processi in background e il terminale di controllo
+  viene disconnesso.
+\item se la conclusione di un processe rende orfano un \textit{process group}
+  ciascun membro del gruppo viene bloccato, e poi gli vengono inviati in
+  successione i segnali \macro{SIGHUP} e \macro{SIGCONT}.
+\end{itemize}
+ma al di la di queste operazioni è necessario poter disporre di un meccanismo
+ulteriore che consenta di sapere come questa terminazione è avvenuta; dato che
+in un sistema unix-like tutto viene gestito attraverso i processi il
+meccanismo scelto consiste nel riportare lo stato di terminazione
+(\textit{termination status}) di cui sopra al processo padre.
+
+Nel caso di conclusione normale, lo stato di uscita del processo viene
+caratterizzato tremite il valore del cosiddetto \textit{exit status}, cioè il
+valore passato alle funzioni \func{exit} o \func{\_exit} (o dal valore di
+ritorno per \func{main}).  Ma se il processo viene concluso in maniera anomala
+il programma non può specificare nessun \textit{exit status}, ed è il kernel
+che deve generare autonomamente il \textit{termination status} per indicare le
+ragioni della conclusione anomala.  
+
+Si noti la distinzione fra \textit{exit status} e \textit{termination status}:
+quello che contraddistingue lo stato di chiusura del processo e viene
+riportato attraverso le funzioni \func{wait} o \func{waitpid} (vedi
+\secref{sec:proc_wait}) è sempre quest'ultimo; in caso di conclusione normale
+il kernel usa il primo (nel codice eseguito da \func{\_exit}) per produrre il
+secondo.
+
+La scelta di riportare al padre lo stato di terminazione dei figli, pur
+essendo l'unica possibile, comporta comunque alcune complicazioni: infatti se
+alla sua creazione è scontato che ogni nuovo processo ha un padre, non è detto
+che sia così alla sua conclusione, dato che il padre protrebbe essere già
+terminato (si potrebbe avere cioè quello che si chiama un processo
+\textsl{orfano}). 
+
+Questa complicazione viene superata facendo in modo che il processo figlio
+venga \textsl{adottato} da \cmd{init}: come già accennato quando un processo
+termina il kernel controlla se è il padre di altri processi in esecuzione: in
+caso positivo allora il \acr{ppid} di tutti questi processi viene sostituito
+con il \acr{pid} di \cmd{init} (e cioè con 1); in questo modo ogni processo
+avrà sempre un padre (nel caso \textsl{adottivo}) cui riportare il suo stato
+di terminazione.  Come verifica di questo comportamento eseguiamo il comando
+\cmd{forktest -c2 3}, in questo modo ciascun figlio attenderà due secondi
+prima di uscire, il risultato è:
+\begin{verbatim}
+[piccardi@selidor sources]$ ./forktest -c2 3
+Test for forking 3 child
+Spawned 1 child, pid 1973 
+Child 1 successfully executing
+Go to next child 
+Spawned 2 child, pid 1974 
+Child 2 successfully executing
+Go to next child 
+Child 3 successfully executing
+Spawned 3 child, pid 1975 
+Go to next child 
+[piccardi@selidor sources]$ Child 3, parent 1, exiting
+Child 2, parent 1, exiting
+Child 1, parent 1, exiting
+\end{verbatim}
+come si può notare in questo caso il processo padre si conclude prima dei
+figli, tornando alla shell, che stampa il prompt sul terminale: circa due
+secondi dopo viene stampato a video anche l'output dei tre figli che
+terminano, e come si può notare in questo caso, al contrario di quanto visto
+in precedenza, essi riportano 1 come \acr{ppid}.
+
+Altrettanto rilevante è il caso in cui il figlio termina prima del padre,
+questo perché non è detto che il padre possa ricevere immediatamente lo stato
+di terminazione, quindi il kernel deve comunque conservare una certa quantità
+di informazioni riguardo ai processi che sta terminando.
+
+Questo viene fatto mantenendo attiva la voce nella tabella dei processi, e
+memorizzando alcuni dati essenziali, come il \acr{pid}, i tempi di CPU usati
+dal processo (vedi \secref{sec:intro_unix_time}) e lo stato di terminazione
+(NdA verificare esattamente cosa c'è!), mentre la memoria in uso ed i file
+aperti vengono rilasciati immediatamente. I processi che sono terminati, ma il
+cui stato di terminazione non è stato ancora ricevuto dal padre sono chiamati
+\textit{zombie}, essi restano presenti nella tabella dei processi ed in genere
+possono essere identificati dall'output di \cmd{ps} per la presenza di una
+\cmd{Z} nella colonna che ne indica lo stato. Quando il padre effettuarà la
+lettura dello stato di uscita anche questa informazione, non più necessaria,
+verrà scartata e la terminazione potrà dirsi completamente conclusa.
+
+Possiamo utilizzare il nostro programma di prova per analizzare anche questa
+condizione: lanciamo il comando \cmd{forktest -e10 3 &} in background,
+indicando al processo padre di aspettare 10 secondi prima di uscire; in questo
+caso, usando \cmd{ps} sullo stesso terminale (prima dello scadere dei 10
+secondi) otterremo:
+\begin{verbatim}
+[piccardi@selidor sources]$ ps T
+  PID TTY      STAT   TIME COMMAND
+  419 pts/0    S      0:00 bash
+  568 pts/0    S      0:00 ./forktest -e10 3
+  569 pts/0    Z      0:00 [forktest <defunct>]
+  570 pts/0    Z      0:00 [forktest <defunct>]
+  571 pts/0    Z      0:00 [forktest <defunct>]
+  572 pts/0    R      0:00 ps T
+\end{verbatim} %$
+e come si vede, dato che non si è fatto nulla per riceverne lo stato di
+terminazione, i tre processi figli sono ancora presenti pur essendosi
+conclusi, con lo stato di zombie e l'indicazione che sono stati terminati.
+
+La possibilità di avere degli zombie deve essere tenuta presente quando si
+scrive un programma che deve essere mantenuto in esecuzione a lungo e creare
+molti figli. In questo caso si deve sempre avere cura di far leggere
+l'eventuale stato di uscita di tutti i figli (in genere questo si fa
+attraverso un apposito \textit{signal handler}, che chiama la funzione
+\func{wait} vedi \secref{sec:sig_xxx} e \secref{sec:proc_wait}). Questa
+operazione è necessaria perché anche se gli \textit{zombie} non consumano
+risorse di memoria o processore, occupano comunque una voce nella tabella dei
+processi, che a lungo andare potrebbe esaurirsi.
+
+Si noti che quando un processo adottato da \cmd{init} termina esso non diviene
+uno \textit{zombie}, in quanto una delle funzioni di \cmd{init} è appunto
+quella di chiamare \func{wait} per i processi di cui fa da padre. Questo è
+quanto avviene ad esempio nel caso dell'ultimo esempio: scaduti i dieci
+secondi \cmd{forktest} esce, siccome i suoi figli vengono ereditati da
+\cmd{init} il quale provvederà.
 
 
 \subsection{Le funzioni \texttt{wait} e  \texttt{waitpid}}
 \label{sec:proc_wait}
 
 
-
 \subsection{Le funzioni \texttt{exec}}
 \label{sec:proc_exec}
 
index d57ef7a7436c469f0620ce45c3dfee50d9d31fe1..5401e319be20b79d721225d8bd340cafbe060f62 100644 (file)
@@ -26,7 +26,7 @@
  *
  * Usage: forktest -h give all info's
  *
- * $Id: ForkTest.c,v 1.4 2001/09/14 22:16:41 piccardi Exp $
+ * $Id: ForkTest.c,v 1.5 2001/09/19 17:10:49 piccardi Exp $
  *
  ****************************************************************/
 /* 
@@ -48,14 +48,15 @@ int main(int argc, char *argv[])
  */
     int nchild, i;
     pid_t pid;
-    int wait_child=0;
-    int wait_parent=0;
+    int wait_child  = 0;
+    int wait_parent = 0;
+    int wait_end    = 0;
     /*
      * Input section: decode command line parameters 
      * Use getopt function
      */
     opterr = 0;         /* don't want writing to stderr */
-    while ( (i = getopt(argc, argv, "hp:c:")) != -1) {
+    while ( (i = getopt(argc, argv, "hp:c:e:")) != -1) {
        switch (i) {
        /* 
         * Handling options 
@@ -66,10 +67,13 @@ int main(int argc, char *argv[])
            return -1;
            break;
        case 'c':   /* take wait time for childen */
-           wait_child=strtol(optarg, NULL, 10);    /* convert input */
+           wait_child = strtol(optarg, NULL, 10);    /* convert input */
            break;
        case 'p':   /* take wait time for childen */
-           wait_parent=strtol(optarg, NULL, 10);   /* convert input */
+           wait_parent = strtol(optarg, NULL, 10);   /* convert input */
+           break;
+       case 'e':   /* take wait before parent exit */
+           wait_end = strtol(optarg, NULL, 10);      /* convert input */
            break;
        case '?':   /* unrecognized options */
            printf("Unrecognized options -%c\n",optopt);
@@ -101,7 +105,7 @@ int main(int argc, char *argv[])
        if (pid == 0) {   /* child */
            printf("Child %d successfully executing\n", ++i);
            if (wait_child) sleep(wait_child);
-           printf("Child %d exiting\n", i);
+           printf("Child %d, parent %d, exiting\n", i, getppid());
            exit(0);
        } else {          /* parent */
            printf("Spawned %d child, pid %d \n", i+1, pid);
@@ -110,6 +114,7 @@ int main(int argc, char *argv[])
        }
     }
     /* normal exit */
+    if (wait_end) sleep(wait_end);
     return 0;
 }
 /*
@@ -118,7 +123,11 @@ int main(int argc, char *argv[])
 void usage(void) {
     printf("Program forktest: fork a given number of child \n");
     printf("Usage:\n");
-    printf("  forktest [-h] child to fork \n");
+    printf("  forktest [-h] [-p sec] [-c sec] [-e sec] child to fork \n");
     printf("  -h          print this help\n");
+    printf("  -p sec       wait sec seconds before next fork\n");
+    printf("  -c sec       wait sec seconds before child termination\n");
+    printf("  -e sec       wait sec seconds before parent return\n");
+    
     exit(1);
 }