From: Simone Piccardi Date: Tue, 18 Sep 2001 20:15:58 +0000 (+0000) Subject: Finita fork, inizio terminazione e wait X-Git-Url: https://gapil.gnulinux.it/gitweb/?a=commitdiff_plain;h=c6bb1ae340cad082718e43163b9595608ed123e1;p=gapil.git Finita fork, inizio terminazione e wait --- diff --git a/gapil.tex b/gapil.tex index 3109915..4ea34f6 100644 --- a/gapil.tex +++ b/gapil.tex @@ -1,4 +1,4 @@ -%% +%% %% GaPiL : Guida alla Programmazione in Linux %% %% S. Piccardi Feb. 2001 diff --git a/process.tex b/process.tex index 776a025..d436a4f 100644 --- a/process.tex +++ b/process.tex @@ -65,7 +65,7 @@ se si vogliono scrivere programmi portabili \subsection{Come chiudere un programma} -\label{sec:proc_termination} +\label{sec:proc_conclusion} La via normale per la quale un programma finisce è quando la funzione \func{main} ritorna, una modalità equivalente di conclusione è quella di @@ -77,7 +77,7 @@ Una forma alternativa Oltre alla conclusione ``normale'' esiste anche la possibilità di una conclusione ``anomala'' del programma a causa di segnali o della chiamata alla funzione \func{abort} (che comunque genera un segnale che termina il -programma); torneremo su questo in \secref{sec:sig_prog_error}. +programma); torneremo su questo in \secref{sec:proc_termination}. Il valore di ritorno della funzione main, o quello usato nelle chiamate ad \func{exit} e \func{\_exit}, viene chiamato \textit{exit status} e passato @@ -116,7 +116,7 @@ Infine occorre distinguere fra lo stato di uscita di un programma possibile un processo possa essere terminato (da un segnale) prima che il programma in esecuzione si sia concluso. In caso di conclusione normale del programma però lo stato di uscita diventa parte dello stato di conclusione del -processo (vedi \secref{sec:proc_xxx}). +processo (vedi \secref{sec:proc_termination}). \subsection{Le funzioni \func{exit} e \func{\_exit}} diff --git a/prochand.tex b/prochand.tex index a85b31f..69bdb16 100644 --- a/prochand.tex +++ b/prochand.tex @@ -116,7 +116,7 @@ non ritorna mai (in quanto con essa viene eseguito un altro programma). In questa sezione tratteremo le funzioni per la gestione dei processi, a partire dalle funzioni elementari che permettono di leggerne gli identificatori, alle varie funzioni di manipolazione dei processi, che -riguardano la lore creazione, terminazione, e la messa in esecuzione di altri +riguardano la loro creazione, terminazione, e la messa in esecuzione di altri programmi. @@ -151,7 +151,7 @@ Entrambe le funzioni non riportano condizioni di errore. \end{functions} Il fatto che il \acr{pid} sia un numero univoco per il sistema lo rende il -candidato ideale per generare ultieriori indicatori associati al processo di +candidato ideale per generare ulteriori indicatori associati al processo di cui diventa possibile garantire l'unicità: ad esempio la funzione \func{tmpname} (si veda \secref{sec:file_temp_file}) usa il \acr{pid} per generare un pathname univoco, che non potrà essere replicato da un'altro @@ -170,15 +170,13 @@ identificativi associati ad un processo relativi al controllo di sessione. 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 protipo +l'uso di questa funzione, che è quindi la base per il multitasking. Il prototipo della funzione è: \begin{functions} \headdecl{sys/types.h} \headdecl{unistd.h} - \funcdecl{pid\_t fork(void)} - Restituisce zero al padre e il \acr{pid} al figlio in caso di successo, ritorna -1 al padre (senza creare il figlio) in caso di errore; \texttt{errno} può assumere i valori: @@ -197,7 +195,7 @@ figlio continuano ad essere eseguiti normalmente alla istruzione seguente la 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 + 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 @@ -207,6 +205,18 @@ La differenza che si ha nei due processi ritorno della funzione fork è il \acr{pid} del processo figlio, mentre nel figlio è zero; in questo modo il programma può identificare se viene eseguito dal padre o dal figlio. +Si noti come la funzione \func{fork} ritorni \textbf{due} volte: una nel padre +e una nel figlio. La sola differenza che si ha nei due processi è il valore di +ritorno restituito dalla funzione, che nel padre è il \acr{pid} del figlio +mentre nel figlio è zero; in questo modo il programma può identificare se +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 +\acr{pid} di nessun processo. \begin{figure}[!htb] \footnotesize @@ -262,24 +272,41 @@ int main(int argc, char *argv[]) \label{fig:proc_fork_code} \end{figure} -Si noti come la funzione \func{fork} ritorni \textbf{due} volte: una nel padre -e una nel figlio. La sola differenza che si ha nei due processi è il valore di -ritorno restituito dalla funzione, che nel padre è il \acr{pid} del figlio -mentre nel figlio è zero; in questo modo il programma può identificare se -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 -\acr{pid} di nessun processo. +Normalmente la chiamata a \func{fork} può fallire solo per due ragioni, o ci +sono già troppi processi nel sistema (il che di solito è sintomo che +qualcos'altro non sta andando per il verso giusto) o si è ecceduto il limite +sul numero totale di processi permessi all'utente (il valore della costante +\macro{CHILD\_MAX} definito in \file{limits.h}, che fa riferimento ai processo +con lo stesso \textit{real user id}). + +L'uso di \func{fork} avviene secondo due modalità principali; la prima è +quella in cui all'interno di un programma si creano processi figli per +affidargli l'esecuzione di una certa sezione di codice, mentre il processo +padre ne esegue un'altra. È il caso tipico dei server di rete in cui il padre +riceve ed accetta le richieste da parte dei client, per ciascuna delle quali +pone in esecuzione un figlio che è incaricato di fornire il servizio. + +La seconda modalità è quella in cui il processo vuole eseguire un altro +programma; questo è ad esempio il caso della shell. In questo caso il processo +crea un figlio la cui unica operazione è quella fare una \func{exec} (di cui +parleremo in \secref{sec:proc_exec}) subito dopo la \func{fork}. + +Alcuni sistemi operativi (il VMS ad esempio) combinano le operazioni di questa +seconda modalità (una \func{fork} seguita da una \func{exec}) in un'unica +operazione che viene chiamata \textit{spawn}. Nei sistemi unix-like è stato +scelto di mantenere questa separazione, dato che, come visto per la prima +modalità d'uso, esistono numerosi scenari in cui si può usare una \func{fork} +senza bisogno di una \func{exec}. Inoltre anche nel caso della seconda +modalità di operazioni, avere le due funzioni separate permette al figlio di +cambiare gli attributi del processo (maschera dei segnali, redirezione +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, ottenuti tramite la funzione +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}. @@ -288,10 +315,10 @@ 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, evantualmente attendere il numero di secondi +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. +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} @@ -311,7 +338,7 @@ Spawned 3 child, pid 2040 Go to next child \end{verbatim} %$ -Esaminiamo questo risultato; una prima conclusione che si può trarre è non si +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 @@ -320,8 +347,8 @@ 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. +ciclo successivo), mentre la terza volta è stato prima eseguito il figlio +(fino alla conclusione) 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 @@ -332,10 +359,10 @@ cui il processo padre ha eseguito pi 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 -di precedenza occorrerà provvedere ad espliciti meccanismi di -sincronizzazione, pena il rischio di incorrere nelle cosiddette \textit{race - conditions}. +istruzioni del codice fra padre e figli, nè sull'ordine in cui questi potranno +essere messi in esecuzione, e se è necessaria una qualche forma di precedenza +occorrerà provvedere ad espliciti meccanismi di sincronizzazione, pena il +rischio di incorrere nelle cosiddette \textit{race conditions}. Si noti inoltre che, come accennato, essendo i segmenti di memoria utilizzati dai singoli processi completamente separati, le modifiche delle variabili nei @@ -376,22 +403,23 @@ 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). +Il comportamento delle varie funzioni di interfaccia con i file è analizzato +in gran dettaglio in \capref{cha:file_unix_interface} e in +\secref{cha:files_std_interface}. Qui basta accennare 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 +buffer veniva scaricato, e le singole righe erano stampate a video subito dopo +l'esecuzione della \func{printf}. Ma con la redirezione su file la scrittura +non avviene più alla fine di ogni riga e l'output resta nel buffer, per questo +motivo, 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 del figlio il buffer +viene scritto su disco, troveremo nel file anche tutto quello che il processo +padre aveva scritto prima della sua creazione. E alla fine del file, 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 @@ -412,38 +440,132 @@ le stesse voci della file table (per la spiegazione di questi termini si veda 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). +table, e tutti gli altri processi che condividono la file table 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 sovrapporsi a quello dei precedenti (l'output potrà risultare +mescolato, ma non ci saranno parti perdute per via di una sovrascrittura). 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. +scrivono sullo stesso file, ad esempio lo standard output (un caso tipico è la +shell). 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 +sequenza impredicibile. Le modalità con cui in genere si usano i file dopo una \func{fork} sono sostanzialmente due: -\begin{itemize} +\begin{enumerate} \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 + ciascuno dei due deve chiudere i file che non gli servono una volta che la + \func{fork} è stata eseguita, per evitare ogni forma di interferenza. +\end{enumerate} + +Oltre ai file aperti i processi figli ereditano dal padre una serie di altre +proprietà comuni; in dettaglio avremo che dopo l'esecuzione di una \func{fork} +padre e figlio avranno in comune: +\begin{itemize} +\item i file aperti (e gli eventuali flag di \textit{close-on-exec} se + settati). +\item gli identificatori per il controllo di accesso: il \textit{real user + id}, il \textit{real group id}, l'\textit{effective user id}, + l'\textit{effective group id} e i \textit{supplementary group id} (vedi + \secref{tab:proc_uid_gid}). +\item gli identificatori per il controllo di sessione: il \textit{process + group id} e il \textit{session id} e il terminale di controllo. +\item i flag \acr{suid} e \acr{suid} (vedi \secref{sec:file_suid_sgid}). +\item la directory di lavoro e la directory radice (vedi + \secref{sec:file_work_dir}). +\item la maschera dei permessi di creazione (vedi \secref{sec:file_umask}). +\item la maschera dei segnali. +\item i segmenti di memoria condivisa agganciati al processo. +\item i limiti sulle risorse +\item le variabili di ambiente (vedi \secref{sec:proc_environ}). \end{itemize} +le differenze invece sono: +\begin{itemize} +\item il valore di ritorno di \func{fork}. +\item il \textit{process id}. +\item il \textit{parent process id} (quello del figlio viene settato al + \acr{pid} del padre). +\item i valori dei tempi di esecuzione (\var{tms\_utime}, \var{tms\_stime}, + \var{tms\_cutime}, \var{tms\_uetime}) che nel figlio sono posti a zero. +\item i \textit{file lock}, che non vengono ereditati dal figlio. +\item gli allarmi pendenti, che per il figlio vengono cancellati. +\end{itemize} + + +\subsection{La funzione \func{vfork}} +\label{sec:proc_vfork} + +La funzione \func{vfork} è esattamente identica a \func{fork} ed ha la stessa +semantica e gli stessi errori; la sola differenza è che non viene creata la +tabella delle pagine né la struttura dei task per il nuovo processo. Il +processo padre è posto in attesa fintanto che il figlio non ha eseguito una +\func{execve} o non è uscito con una \func{\_exit}. Il figlio condivide la +memoria del padre (e modifiche possono avere effetti imprevedibili) e non deve +ritornare o uscire con \func{exit} ma usare esplicitamente \func{\_exit}. + +Questa funzione è un rimasuglio dei vecchi tempi in cui eseguire una +\func{fork} comportava anche la copia completa del segmento dati del processo +padre, che costituiva un inutile appesantimento in tutti quei casi in cui la +\func{fork} veniva fatto solo per poi eseguire una \func{exec}. La funzione +venne introdotta in BSD per migliorare le prestazioni. + +Dato che Linux supporta il \textit{copy on write} la perdita di prestazioni è +assolutamente trascurabile, e l'uso di questa funzione (che resta un caso +speciale della funzione \func{clone}), è deprecato, per questo eviteremo di +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). + +Ma oltre alla conclusione normale abbiamo accennato che esistono anche delle +modalità di conclusione anomala; queste sono in sostanza due: il programma può +chiamare la funzione \func{abort} per invocare una chiusura anomala, o essere +terminato da un segnale. In realtà anche la prima modalità si riconduce alla +seconda, dato che \func{abort} si limita a generare il segnale +\macro{SIGABRT}. + +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}. + \subsection{Le funzioni \texttt{wait} e \texttt{waitpid}} \label{sec:proc_wait} + \subsection{Le funzioni \texttt{exec}} \label{sec:proc_exec} @@ -464,7 +586,7 @@ funzioni per la loro manipolazione diretta. Abbiamo già accennato in \secref{sec:intro_multiuser} ad ogni utente ed gruppo sono associati due identificatori univoci, lo \acr{uid} e il \acr{gid} che li -contraddistinguono nei confonti del kernel. Questi identificatori stanno alla +contraddistinguono nei confronti del kernel. Questi identificatori stanno alla base del sistema di permessi e protezioni di un sistema unix, e vengono usati anche nella gestione dei privilegi di accesso dei processi.