From: Simone Piccardi Date: Mon, 17 Sep 2001 17:14:48 +0000 (+0000) Subject: Correzioni e condivisione file dopo la fork X-Git-Url: https://gapil.gnulinux.it/gitweb/?a=commitdiff_plain;h=fa2959bc0d6de2bf0f171f76591d437fe7b5595d;p=gapil.git Correzioni e condivisione file dopo la fork --- diff --git a/intro.tex b/intro.tex index d88d6a7..634c3cb 100644 --- a/intro.tex +++ b/intro.tex @@ -379,7 +379,7 @@ successiva a \func{strerror}; nel caso si usino i thread provvista\footnote{questa funzione è una estensione GNU, non fa parte dello standard POSIX} una versione apposita: \begin{prototype}{string.h} -{char * strerror_r(int errnum, char * buff, size\_t size)} +{char * strerror\_r(int errnum, char * buff, size\_t size)} La funzione è analoga a \func{strerror} ma ritorna il messaggio in un buffer specificato da \var{buff} di lunghezza massima (compreso il terminatore) \var{size}. @@ -388,7 +388,7 @@ che utilizza un buffer che il singolo thread deve allocare, per evitare i problemi connessi alla condivisione del buffer statico. Infine, per completare la caratterizzazione dell'errore, si può usare anche la variabile globale\footnote{anche questa è una estensione GNU} -\var{program_invocation_short_name} che riporta il nome del programma +\var{program\_invocation\_short\_name} che riporta il nome del programma attualmente in esecuzione. Una seconda funzione usata per riportare i codici di errore in maniera diff --git a/prochand.tex b/prochand.tex index 90da681..a85b31f 100644 --- a/prochand.tex +++ b/prochand.tex @@ -225,9 +225,10 @@ int main(int argc, char *argv[]) /* * Variables definition */ - int i; - int nchild; + int nchild, i; pid_t pid; + int wait_child=0; + int wait_parent=0; ... /* handling options */ @@ -243,12 +244,14 @@ int main(int argc, char *argv[]) printf("Error on %d child creation, %s\n", i, strerror(errno)); } if (pid == 0) { /* child */ - printf("Child %d successfully executing\n", i++); - sleep(2); + printf("Child %d successfully executing\n", ++i); + if (wait_child) sleep(wait_child); printf("Child %d exiting\n", i); exit(0); } else { /* parent */ - printf("Spawned %d child, pid %d \n", i, pid); + printf("Spawned %d child, pid %d \n", i+1, pid); + if (wait_parent) sleep(wait_parent); + printf("Go to next child \n"); } } /* normal exit */ @@ -265,29 +268,32 @@ ritorno restituito dalla funzione, che nel padre mentre nel figlio è zero; in questo modo il programma può identificare se viene eseguito dal padre o dal figlio. -La scelta di questi valori comunque 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 \acr{pid} di nessun processo. +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 +\acr{pid} di nessun 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 -dei tempi di attesa (in seconda) 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}. +degli eventuali tempi di attesa (in secondi, ottenuti 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}. Decifrato il numero di figli da creare, il ciclo principale del programma (\texttt{\small 28--40}) esegue in successione la creazione dei processi figli controllando il successo della chiamata a \func{fork} (\texttt{\small 29--31}); ciascun figlio (\texttt{\small 29--31}) si limita a stampare il -suo numero di successione, attendere 3 secondi e scrivere un messaggio prima -di uscire. Il processo padre invece (\texttt{\small 29--31}) stampa un -messaggio di creazione e procede nell'esecuzione del ciclo. Se eseguiamo il -comando otterremo come output sul terminale: +suo numero di successione, evantualmente 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), +otterremo come output sul terminale: \begin{verbatim} [piccardi@selidor sources]$ ./forktest 3 Test for forking 3 child @@ -305,21 +311,25 @@ Spawned 3 child, pid 2040 Go to next child \end{verbatim} %$ -Come si vede non si può dire quale processo fra il padre ed il figlio venga -eseguito per primo\footnote{anche se nel kernel 2.4.x era stato introdotto un - meccanismo che metteva in esecuzione sempre il xxx per primo (TODO - recuperare le informazioni esatte)} dopo la chiamata a \func{fork}, nel caso -mostrato sopra ad esempio si può notare come dopo la creazione il secondo ed -il quinto figlio sia stato stati eseguiti per primi, mantre per gli altri -figli è stato eseguito per primo il padre. +Esaminiamo questo risultato; una prima conclusione che si può trarre è non si +può dire quale processo fra il padre ed il figlio venga eseguito per +primo\footnote{anche se nel kernel 2.4.x era stato introdotto un meccanismo + che metteva in esecuzione sempre il xxx per primo (TODO recuperare le + informazioni esatte)} dopo la chiamata a \func{fork}; dall'esempio si può +notare infatti come nei primi due cicli sia stato eseguito per primo il padre +(con la stampa del \acr{pid} del nuovo processo) per poi passare +all'esecuzione del figlio (completata con i due avvisi di esecuzione ed +uscita), e tornare all'esecuzione del padre (con la stampa del passaggio al +ciclo successivo), mentre la terza volta è stato prima eseguito il figlio (in +maniera completa) e poi il padre. In generale l'ordine di esecuzione dipenderà, oltre che dall'algoritmo di scheduling usato dal kernel, dalla particolare situazione in si trova la macchina al momento della chiamata, risultando del tutto impredicibile. -Eseguendo più volte il programma di prova, si sono ottenute situazioni -completamente diverse, compreso caso in cui il processo padre ha eseguito più -di una \func{fork} prima che uno dei figli venisse messo in -esecuzione. +Eseguendo più volte il programma di prova e producendo un numero diverso di +figli, si sono ottenute situazioni completamente diverse, compreso il caso in +cui il processo padre ha eseguito più di una \func{fork} prima che uno dei +figli venisse messo in esecuzione. Pertanto non si può fare nessuna assunzione sulla sequenza di esecuzione delle istruzioni del codice fra padre e figli, e se è necessaria una qualche forma @@ -327,14 +337,108 @@ di precedenza occorrer sincronizzazione, pena il rischio di incorrere nelle cosiddette \textit{race conditions}. -Si ricordi inoltre che come accennato, essendo i segmenti di memoria -utilizzati dai singoli processi completamente separati, le modifiche delle -variabili nei processi figli (come l'incremento di \var{i} in \texttt{\small - 33}) saranno effettive solo per essi, e non hanno alcun effetto sul valore -che le stesse variabili hanno nel processo padre. - -L'esempio mostra anche - +Si noti inoltre che, come accennato, essendo i segmenti di memoria utilizzati +dai singoli processi completamente separati, le modifiche delle variabili nei +processi figli (come l'incremento di \var{i} in \texttt{\small 33}) sono +visibili solo al loro interno, e non hanno alcun effetto sul valore che le +stesse variabili hanno nel processo padre (ed in eventuali altri processi +figli che eseguano lo stesso codice). + +Un secondo aspetto molto importante nella creazione dei processi figli è +quello dell'interazione dei vari processi con i file; per illustrarlo meglio +proviamo a redirigere su un file l'output del nostro programma di test, quello +che otterremo è: +\begin{verbatim} +[piccardi@selidor sources]$ ./forktest 3 > output +[piccardi@selidor sources]$ cat output +Test for forking 3 child +Child 1 successfully executing +Child 1 exiting +Test for forking 3 child +Spawned 1 child, pid 836 +Go to next child +Child 2 successfully executing +Child 2 exiting +Test for forking 3 child +Spawned 1 child, pid 836 +Go to next child +Spawned 2 child, pid 837 +Go to next child +Child 3 successfully executing +Child 3 exiting +Test for forking 3 child +Spawned 1 child, pid 836 +Go to next child +Spawned 2 child, pid 837 +Go to next child +Spawned 3 child, pid 838 +Go to next child +\end{verbatim} +che come si vede è completamente diverso da quanto ottenevamo sul terminale. + +Analizzeremo in gran dettaglio in \capref{cha:file_unix_interface} e in +\secref{cha:files_std_interface} il comportamento delle varie funzioni di +interfaccia con i file. Qui basta ricordare che si sono usate le funzioni +standard della libreria del C che prevedono l'output bufferizzato; e questa +bufferizzazione varia a seconda che si tratti di un file su disco (in cui il +buffer viene scaricato su disco solo quando necessario) o di un terminale (nel +qual caso il buffer viene scaricato ad ogni a capo). + +Nel primo esempio allora avevamo che ad ogni chiamata a \func{printf} il +buffer veniva scaricato, e le singole righe erano stampate a video volta a +volta. Quando con la redirezione andiamo a scrivere su un file, questo non +avviene più, e dato che ogni figlio riceve una copia della memoria del padre, +esso riceverà anche quanto c'è nel buffer delle funzioni di I/O, comprese le +linee scritte dal padre fino allora. Così quando all'uscita di un figlio il +buffer viene scritto su disco, troveremo nel file anche tutto quello che il +processo padre aveva scritto prima della sua creazione. Alla fine, dato che +in questo caso il padre esce per ultimo, troviamo anche l'output del padre. + +Ma l'esempio ci mostra un'altro aspetto fondamentale dell'interazione con i +file, che era valido anche per l'esempio precedente, ma meno evidente; il +fatto cioè che non solo processi diversi possono scrivere in contemporanea +sullo stesso file (l'argomento della condivisione dei file in unix è trattato +in dettaglio in \secref{sec:file_sharing}), ma anche che, a differenza di +quanto avviene per le variabili, la posizione corrente sul file è condivisa +fra il padre e tutti i processi figli. + +Quello che succede è che quando lo standard output del padre viene rediretto, +lo stesso avviene anche per tutti i figli; la funzione \func{fork} infatti ha +la caratteristica di duplicare (allo stesso modo in cui lo fa la funzione +\func{dup}, trattata in \secref{sec:file_dup}) nei figli tutti i file +descriptor aperti nel padre, il che comporta che padre e figli condividono +le stesse voci della file table (per la spiegazione di questi termini si veda +\secref{sec:file_sharing} e referenza a figura da fare) e quindi anche +l'offset corrente nel file. + +In questo modo se un processo scrive sul file aggiornerà l'offset sulla file +table, e tutti gli altri vedranno il nuovo valore; in questo modo si evita, in +casi come quello appena mostrato, in cui diversi processi scrivono sullo +stesso file, che l'output successivo di un processo vada a sovrascrivere +quello dei precedenti (l'output potrà risultare mescolato, ma non ci saranno +parti perdute per via di una sovrapposizione). + +Questo tipo di comportamento è essenziale in tutti quei casi in cui il padre +crea un figlio ed attende la sua conclusione per proseguire, ed entrambi +scrivono sullo stesso file (ad esempio lo standard output). Se l'output viene +rediretto con questo comportamento avremo che il padre potrà continuare a +scrivere automaticamente in coda a quanto scritto dal figlio; se così non +fosse ottenere questo comportamento sarebbe estremamente complesso +necessitando di una qualche forma di comunicazione fra i due processi. + +In generale comunque non è buona norma far scrivere più processi sullo stesso +file senza una qualche forma di sincronizzazione in quanto, come visto con il +nostro esempio, le varie scritture risulteranno mescolate fra loro in una +sequenza impredicibile. Le modalità generali con cui si usano i file dopo una +\func{fork} sono sostanzialmente due: +\begin{itemize} +\item Il processo padre aspetta la conclusione del figlio. In questo caso non + è necessaria nessuna azione riguardo ai file, in quanto la sincronizzazione + degli offset dopo eventuali operazioni di lettura e scrittura effettuate dal + figlio è automatica. +\item L'esecuzione di padre e figlio procede indipendentemente. In questo caso + entrambi devono chiudere i file che non servono, per evitare ogni forma +\end{itemize} \subsection{Le funzioni \texttt{wait} e \texttt{waitpid}} \label{sec:proc_wait}