Post on 06-Aug-2020
transcript
I processi e le funzioni di LINUX
per la gestione dei processi
Prof. Mauro Negri
Prof. Giuseppe Pelagatti
Dipartimento di Elettronica e Informazione (D.E.I.)
25 Novembre 2008
PROCESSI E FUNZIONI LINUX
Proff. Mauro Negri, Giuseppe Pelagatti 1
1 La necessità del parallelismo Un programma eseguibile, generato ad esempio da un programma C, è
rigorosamente sequenziale, nel senso che viene eseguito una istruzione alla volta e,
dopo l’esecuzione di un’istruzione, è univocamente determinata la prossima istruzione
da eseguire.
In base a quanto noto finora, l’esecuzione di N programmi da parte di un
calcolatore dovrebbe anch’essa essere rigorosamente sequenziale, in quanto, dopo
avere eseguito la prima istruzione di un programma dovrebbero essere eseguite tutte
le successive fino alla fine del programma prima di poter iniziare l’esecuzione della
prima istruzione del programma successivo. Questo modello sequenziale è molto
comodo per il programmatore, perchè nella scrittura del programma egli sa che non ci
saranno “interferenze” da parte di altri programmi, in quanto gli altri programmi che
verranno eseguiti dallo stesso esecutore saranno eseguiti o prima o dopo l’esecuzione
del programma considerato.
Tuttavia, il modello di esecuzione sequenziale non è adeguato alle esigenze
della maggior parte dei sistemi di calcolo; questa inadeguatezza è facilmente
verificabile pensando ai seguenti esempi:
• server WEB: un server WEB deve poter rispondere a molti utenti
contemporaneamente; non sarebbe accettabile che un utente dovesse
attendere, per collegarsi, che tutti gli altri utenti si fossero già scollegati
• calcolatore multiutente: i calcolatori potenti vengono utilizzati da molti
utenti contemporaneamente; in particolare, i calcolatori centrali dei Sistemi
Informativi (delle banche, delle aziende, ecc…) devono rispondere
contemporaneamente alle richieste di moltissimi utilizzatori
contemporanei
• applicazioni multiple aperte da un utente: quando un utente di un normale
PC tiene aperte più applicazioni contemporaneamente esistono diversi
programmi che sono in uno stato di esecuzione già iniziato e non ancora
terminato
In base ai precedenti esempi risulta necessario un modello più sofisticato del
PROCESSI E FUNZIONI LINUX
Proff. Mauro Negri, Giuseppe Pelagatti 2
sistema; si osservi che tale modello deve garantirci due obiettivi tendenzialmente
contrastanti:
• fornire parallelismo, cioè permettere che l’esecuzione di un programma
possa avvenire senza attendere che tutti gli altri programmi in esecuzione
siano già terminati;
• garantire che ogni programma in esecuzione sia eseguito esattamente come
sarebbe eseguito nel modello sequenziale, cioè come se i programmi
fossero eseguiti uno dopo l’altro, evitando “interferenze” indesiderate tra
programmi diversi (vedremo che in alcuni casi esistono anche interferenze
“desiderate”, ma per ora è meglio non considerarle).
2 La nozione di processo Per ottenere un comportamento del sistema che soddisfa gli obiettivi indicati
sopra la soluzione più comune è quella rappresentata in figura 1, nella quale si vede
che sono stati creati tanti “esecutori” quanti sono i programmi che devono essere
eseguiti in parallelo. Nel contesto del sistema operativo LINUX (e in molti altri) gli
esecutori creati dinamicamente per eseguire diversi programmi sono chiamati
Processi. I processi devono essere considerati degli esecutori completi, e quindi la
struttura di figura 1 risponde in maniera evidente ad ambedue i requisiti contrastanti
definiti al precedente paragrafo: al primo requisito, perchè diversi processi eseguono
diversi programmi in parallelo, cioè senza che uno debba attendere la terminazione
degli altri, e al secondo requisito, perchè, essendo i diversi processi degli esecutori
indipendenti tra loro, non c’è interferenza tra i diversi programmi (come se fossero
eseguiti su diversi calcolatori).
I processi possono essere considerati come dei calcolatori o macchine
virtuali, nel senso che sono calcolatori realizzati dal software (sistema operativo) e
non esistono in quanto Hardware, anche se ovviamente il sistema operativo ha a sua
volta bisogno di essere eseguito da un calcolatore reale. Ogni processo deve possedere
ad ogni istante un unico programma in esecuzione; pertanto, la comunicazione tra due
processi coincide con la comunicazione tra i due corrispondenti programmi in
esecuzione.
PROCESSI E FUNZIONI LINUX
Proff. Mauro Negri, Giuseppe Pelagatti 3
Figura 1 – Processi e programmi eseguiti dai processi
Tuttavia, la nozione di processo come macchina virtuale è più ampia e mette
in luce alcune importanti caratterisitche del processo che estendono la normale
nozione di programma in esecuzione; in particolare:
• il processo possiede delle risorse, per esempio una memoria, dei file aperti,
un terminale di controllo, ecc…
• il programma eseguito da un processo può essere sostituito senza annullare
il processo stesso, ovvero un processo può passare dall’esecuzione di un
programma all’esecuzione di un diverso programma (attenzione, questo
Programma 1 Programma 2 Programma 3
Processo 1 (esecutore)
Processo 2 (esecutore)
Processo 3 (esecutore)
Istruzioni
Istruzioni
Istruzioni
HARDWARE + SO
Virtualizzazione
PROCESSI E FUNZIONI LINUX
Proff. Mauro Negri, Giuseppe Pelagatti 4
passaggio è sequenziale, cioè il primo programma viene totalmente
abbandonato per passare al nuovo);
Si osservi che, dato che il processo rimane lo stesso anche se cambia il programma da
esso eseguito, le risorse del processo non si annullano quando si passa all’esecuzione
di un nuovo programma e quindi il nuovo programma potrà utilizzarle; ad esempio, il
nuovo programma lavorerà sullo stesso terminale di controllo del programma
precedente.
Sorge però una domanda: come è possibile realizzare diversi processi che
procedono in parallelo se l’Hardware del calcolatore è uno solo ed è sequenziale? A
questa domanda si risponderà trattando la struttura interna del SO; per ora basta tenere
presente che il sistema operativo è in grado di virtualizzare diversi processi
indipendenti e che dal punto di vista del programmatore quello di figura 1 è il modello
di riferimento da utilizzare.
3 Caratteristiche generali dei processi Tutti i processi sono identificati da un apposito identificatore (PID = Process
Identifier).
Tutti i processi (ad eccezione del primo, il processo “init”, creato
all’avviamento dal SO) sono creati da altri processi. Ogni processo, a parte il processo
init, possiede quindi un processo padre che lo ha creato e può a sua volta creare molti
processi figli. La gerarchia delle relazioni di parentela dell’insieme dei processi creati
nel sistema può quindi essere rappresentata da un albero come mostrato in Figura 2.,
nel quale alla radice c’è in processo init che genera una serie di processi dedicati alla
gestione del Sistema Operativo. In particolare viene generato un processo per ogni
terminale (chiamato semplicemente “getty” in Figura 2) il quale si mette in attesa di
una richiesta di login da parte di un utente; quando avviene la richiesta il processo
attiva l’esecuzione del programma di login (in Figura 2 il processo “getty” che esegue
con successo il programma di login è chiamato “login”) il quale associa al processo le
caratteristiche dell’utente collegato (privilegi, limiti, ….) e genera un processo al
quale fa eseguire il programma dell’interprete comandi di default dell’utente (il
processo “shell” di Figura 2) che si mette in attesa di comandi da parte dell’utente.
Quando l’utente chiede di eseguire un proprio programma oppure un comando che
richieda l’esecuzione di un programma (ad esempio, un comando di copia di un file)
PROCESSI E FUNZIONI LINUX
Proff. Mauro Negri, Giuseppe Pelagatti 5
l’interprete comandi genera un processo dedicato all’esecuzione del programma
richiesto e poi si mette in attesa della sua terminazione; il programma in esecuzione
può poi invocare le funzioni del sistema operativo per la gestione dei processi al fine
di gestire la creazione/distruzione di ulteriori processi.
Figura 2. La gerarchia dei processi
Per semplicità ci concentreremo sui processi creati dagli utenti anche se alcune
caratteristiche hanno validità generale. La memoria di ogni processo è costituita (dal
punto di vista del programmatore) da 3 parti (dette anche “segmenti”):
1. il segmento codice (text segment): contiene il codice eseguibile del
programma lanciato in esecuzione sul processo;
2. il segmento dati (user data segment): contiene tutti i dati del programma,
ossia sia i dati statici, sempre presenti, sia i dati dinamici, che a loro volta
si dividono in dati allocati automaticamente in una zona di memoria detta
pila (stack) nella quale sono allocati i record di attivazione delle funzioni
(variabili locali, indirizzo di ritorno e parametri delle funzioni) e dati
dinamici allocati esplicitamente dal programma tramite la funzione
“malloc( )” in una zona di memoria detta heap.
3. il segmento di sistema (system data segment): questo segmento contiene
dati inerenti il processo stesso (ad esempio, la “tabella dei files aperti”) che
tuttavia non sono gestiti esplicitamente dal programma in esecuzione ma
dal sistema operativo per suo conto. Si noti che esistono strutture dati
necessarie alla gestione del singolo processo che non sono allocate in
questa area, ma in strutture dati interne del sistema operativo (ad esempio,
il descrittore del processo nella tabella dei processi). Tuttavia, al fine di
init
P1(SO) Pn(SO)
… getty (SO)
login (SO)
shell (user)
Processo1 (comando)
Processo1 )Processo2
PROCESSI E FUNZIONI LINUX
Proff. Mauro Negri, Giuseppe Pelagatti 6
semplificare la presentazione di quanto necessario per questo corso si
suppone che in questo segmento siano memorizzate tutte le informazioni
di sistema operativo necessarie alla comprensione della gestione
programmativa dei processi.
Per operare con i processi il sistema operativo, LINUX mette a disposizione
del programmatore un certo numero di servizi di sistema. I principali servizi
che tratteremo in questo testo consentono di:
• generare un processo figlio, che è copia del processo padre in esecuzione,
ossia che esegue lo stesso programma ;
• attendere la terminazione di un processo figlio;
• terminare un processo restituendo un eventuale codice di terminazione al
processo padre;
• sostituire il programma eseguito da un processo, cioè il segmento codice e
il segmento dati del processo, con il codice e i dati di un diverso
programma.
I servizi del sistema operativo sono messi a disposizione del programmatore
come funzioni in modo analogo alle operazioni di lettura e scrittura da terminale o
file. Queste funzioni sono diverse dalle normali funzioni implementate da un
programmatore perché esse non devono operare solo sui dati (segmento dati) del
programma, ma viceversa devono accedere alle strutture dati gestite dal sistema
operativo (ad esempio, per creare un processo nel sistema). Per ottenere questo
obiettivo esse richiamano il sistema operativo, demandandogli quelle operazioni che il
programma non è in grado di svolgere direttamente. Pertanto la comprensione del
comportamento reale di queste funzioni richiede di conoscere il funzionamento del
sistema operativo ed è per questo motivo che la descrizione del loro comportamento
sarà opportunamente semplificata.
4 Generazione e terminazione dei processi: le funzioni fork ed exit La funzione fork crea un processo figlio identico al processo padre all’istante
della fork come mostrato in Figura 3.
Prototipo della funzione fork.
pid_t fork( )
(pid_t è un tipo predefinito)
PROCESSI E FUNZIONI LINUX
Proff. Mauro Negri, Giuseppe Pelagatti 7
Tutti i segmenti del padre sono duplicati nel figlio, quindi sia il codice e le
variabili (segmenti codice e dati), sia i file aperti utilizzati (segmento di sistema). Si
noti che il segmento codice è identico e quindi si potrebbe evitare la duplicazione, ma
questo aspetto non è considerato ulteriormente.
Programma 1void main(){fork(); … } …
System segment
(file aperti,tty)
terminale
Programma 1void main(){fork()… } …
system segment
(file aperti,tty)
terminale
creazione
(clone)
Processo padre Processo figlio
data segment data segment
virtualcopy
PC-> <-PC
Figura 3. Creazione di un processo
La duplicazione del segmento di sistema prodotto dalla fork copia la tabella
dei file aperti e pertanto, come mostrato in Figura 4, entrambi i processi possono
operare sullo stesso file aperto (file 1), mentre successivi file aperti da uno dei due
processi (file 2) dopo la fork saranno invece immessi nella tabella dei file aperti del
solo processo che ha eseguito l’apertura. Si noti che la tabella degli i-node e dei file
aperti globali sono invece strutture dati referenziate dalle tabelle locali ai processi, ma
sono strutture dati del sistema operativo.
PROCESSI E FUNZIONI LINUX
Proff. Mauro Negri, Giuseppe Pelagatti 8
Tab. globale file aperti Tabella i_nodeProcesso 1 (system segment)
Tab. file aperti
file 1
Processo 2 (system segment)
Tab. file aperti
file 1
file 2
file 1
file 2
inode1
header file 1
block pointers
inode2header file 1block pointers
…..
Figura 4. La gestione dei file aperti
Il processo figlio eredita anche il valore del PC del processo padre, pertanto
entrambi i processi dopo la fork si trovano ad eseguire la stessa istruzione del
programma. Ciò significa che terminata l’esecuzione della fork entrambi i processi
proseguono ad eseguire la porzione dello stesso programma che segue l’istruzione di
invocazione della fork.
Tuttavia, come sarà illustrato in seguito, la creazione di un processo figlio avviene
spesso perché padre e figlio si distinguano eseguendo parti diverse di codice. Per
permettere ciò il sistema operativo restituisce, come valore ritornato dalla funzione
fork stessa, un valore diverso tra processo figlio e processo padre:
• nel processo padre la funzione fork restituisce un valore diverso da 0;
normalmente tale valore indica il pid del figlio, tranne -1, che indica un
errore e quindi che la fork non è stata eseguita;
• nel processo figlio la funzione fork restituisce il valore 0.
In questo modo dopo l’esecuzione di una fork è possibile sapere se siamo nel
processo padre oppure nel figlio interrogando tale valore all’interno dei due processi.
La funzione exit pone termine all’esecuzione del programma e provoca la
PROCESSI E FUNZIONI LINUX
Proff. Mauro Negri, Giuseppe Pelagatti 9
distruzione del processo corrente (la descrizione dettagliata del funzionamento di
questa funzione è ripresa più avanti). Un programma può terminare anche senza una
invocazione esplicita della funzione exit; in tal caso exit viene invocata
automaticamente dal sistema al termine dell’esecuzione del programma (questo
comportamento è simile a quello del comando return in una funzione C: esso permette
la terminazione della funzione in un punto qualsiasi, ma una funzione può anche
terminare raggiungendo la fine, senza return esplicita).
#include <stdio.h> #include <sys/types.h> void main( ) { pid_t pid; pid=fork( ); if (pid==-1) {printf(“errore esecuzione fork”); exit();} else if (pid==0) {printf("sono il processo figlio\n"); exit( ); } else {printf("sono il processo padre\n"); exit( ); /* non necessaria */ } } a)il programma fork1
b)un possibile risultato dell’esecuzione di fork1
Figura 5
In figura 5.a è mostrato un programma che utilizza i servizi fork ed exit e il
risultato della sua esecuzione. Si noti che l’ordine nel quale sono state eseguite le due
printf è casuale; dopo una fork il processo figlio evolve indipendentemente dal padre
e quindi può essere eseguito per primo sia il processo padre che il processo figlio.
Costituisce un grave errore concettuale ipotizzare nella programmazione un preciso
ordine di esecuzione di due processi, perché essi evolvono in parallelo.
PROCESSI E FUNZIONI LINUX
Proff. Mauro Negri, Giuseppe Pelagatti 10
A causa della struttura particolarmente semplice dell’esempio le due exit non
sarebbero necessarie, perché comunque il programma terminerebbe raggiungendo i
punti in cui sono le exit.
Si osservi che ambedue i processi scrivono tramite printf sullo stesso terminale
come mostrato in Figura 5.b. Questo fatto è dovuto alla duplicazione del segmento di
sistema nell’esecuzione di una fork: dato che la funzione printf scrive sullo standard
output, che è un file speciale, e dato che la tabella dei file aperti è replicata nei due
processi, le printf eseguite dai due processi scrivono sullo stesso standard output.
Infine si fa notare, con riferimento alla Figura 2, che il comando ./fork1 di Figura 5.b
è ricevuto dall’interprete comandi che crea un processo al quale associa l’esecuzione
del programma che a sua volta poi crea un ulteriore processo tramite la fork.
Possiamo arricchire l’esempio precedente stampando il valore del pid dei due
processi padre e figlio; a questo scopo possiamo utilizzare una funzione di libreria che
restituisce al processo che la invoca il valore del suo pid. Tale funzione si chiama
getpid ed ha il seguente prototipo:
pid_t getpid( )
In figura 6 sono riportati il testo ed il risultato dell’esecuzione del programma
forkpid1, che è simile al programma fork1 ma stampa i pid dei processi coinvolti. Si
osservi che le printf eseguite dai due processi sono mescolate tra loro in ordine
casuale, per i motivi discussi sopra. Per interpretare il risultato è quindi necessario
osservare bene il testo delle printf nel codice. #include <stdio.h> #include <sys/types.h> void main( ) { pid_t pid,miopid; pid=fork( ); if (pid==0) {miopid=getpid( ); printf("sono il processo figlio con pid: %i\n\n",miopid); exit( ); } else {printf("sono il processo padre\n"); printf("ho creato un processo con pid: %i\n", pid); miopid=getpid( ); printf("il mio pid e' invece: %i\n\n", miopid); exit( ); /* non necessaria */ } } a)il programma forkpid1
PROCESSI E FUNZIONI LINUX
Proff. Mauro Negri, Giuseppe Pelagatti 11
b)un possibile risultato dell’esecuzione di forkpid1
Figura 6
Un processo può creare più di un figlio, e un figlio può a sua volta generare
dei figli (talvolta si usa la dizione di processi “nipoti” per i figli dei figli), ecc… Si
viene a creare in questo modo la struttura gerarchica tra processi, mostrata in Figura 2,
in cima alla quale è situato, come già detto, il primo processo generato dal sistema
operativo durante l’inizializzazione.
Quanto descritto sinora permette di creare processi, ma come evidenziato nella
descrizione della exit ogni processo viene distrutto quando termina di eseguire il
programma del proprio segmento di codice. Dato che una volta creati i processi, essi
procedono autonomamente non esiste un criterio generale che stabilisca chi termini
per primo, quindi è possibile che un processo figlio termini la propria esecuzione
prima del padre o viceversa. Diventa quindi necessario stabilire cosa accada ai
processi figli che rimangono “orfani” a causa della terminazione e quindi distruzione
del loro processo padre. La convenzione adottata da Linux, mostrata in Figura 7, è
quella di far adottare i processi figli rimasti orfani (processi 2 e 3) e tutta la loro
discendenza (processo 4) al processo init del sistema operativo.
a) b)
Figura 7. L’adozione dei processi orfani
init …
login
shell Processo1
Processo2 Processo3
Processo4
init
… login
shell
Processo2 Processo3
Processo4
PROCESSI E FUNZIONI LINUX
Proff. Mauro Negri, Giuseppe Pelagatti 12
In figura 8 sono riportati il testo e il risultato dell’esecuzione del programma
forkpid2, che estende forkpid1 facendo generare al processo padre un secondo
processo figlio che eseguendo un ciclo infinito rimane orfano alla morte degli altri due
processi. Dato che i 3 processi scrivono sullo stesso terminale in ordine casuale, per
rendere il risultato più intelligibile tutte le stampe sono state numerate. Si noti che la
riallocazione del processo figlio nella gerarchia dei processi è eseguita
automaticamente dal sistema operativo. #include <stdio.h> #include <sys/types.h> void main( ) { pid_t pid,miopid; int i=0; pid=fork( ); if (pid==0) {miopid=getpid( ); printf("1)sono il primo processo figlio con pid: %i\n",miopid); while (i==0) continue; exit( ); } else { printf("2)sono il processo padre\n"); printf("3)ho creato un processo con pid: %i\n", pid); miopid=getpid( ); printf("4)il mio pid e' invece: %i\n", miopid); pid=fork( ); if (pid==0) {miopid=getpid( ); printf("5)sono il secondo processo figlio con pid: %i\n",miopid); exit(); } else {printf("6)sono il processo padre\n"); printf("7)ho creato un secondo processo con pid: %i\n", pid); exit( ); /* non necessaria */ } } } a)il programma forkpid2 b)un possibile risultato dell’esecuzione di forkpid2
Figura 8
PROCESSI E FUNZIONI LINUX
Proff. Mauro Negri, Giuseppe Pelagatti 13
5 Attesa della terminazione e stato restituito da un processo figlio: la
funzione wait e i parametri della funzione exit Nella precedente sezione i processi creati evolvevano indipendentemente,
tuttavia un processo crea un altro processo per svolgere un particolare compito e si
pone spesso in attesa del suo risultato prima di proseguire. La sincronizzazione tra il
processo padre e i processi figli avviene tramite la funzione wait che permette al
processo padre di sospendersi in attesa di un processo figlio e la funzione exit che
permette ad un processo figlio di informare un processo padre della sua terminazione
e di passare eventualmente un codice di errore. Poiché le due funzioni sono inserite in
programmi eseguiti in processi diversi che evolvono in modo indipendente le funzioni
saranno presentate considerando prima una sincronizzazione tra i due processi e poi il
reale comportamento asincrono dei processi.
La funzione wait sospende l’esecuzione del processo che la esegue ed attende
la terminazione di un qualsiasi processo figlio
Prototipo della funzione wait
pid_t wait(int *)
Esempio di uso: pid_t pid;
int stato;
pid = wait(&stato);
Dopo l’esecuzione la variabile pid assume il valore del pid del figlio
terminato; la variabile stato assume il valore del codice di terminazione del processo
figlio. Tale codice contiene una parte (gli 8 bit superiori) che può essere assegnato
esplicitamente dal programmatore tramite la funzione exit nel modo descritto sotto; la
parte restante è assegnata dal sistema operativo per indicare particolari condizioni di
terminazione (ad esempio quando un processo viene terminato a causa di un errore).
Dato che il valore restituito dalla exit è contenuto negli 8 bit superiori, lo stato
ricevuto dalla wait è lo stato della exit diviso per 256.
Prototipo della funzione exit
void exit(int);
Esempio: exit(5)
termina il processo e restituisce il valore 5 al padre.
PROCESSI E FUNZIONI LINUX
Proff. Mauro Negri, Giuseppe Pelagatti 14
Si noti che il processo figlio che emette una exit ha completato l’esecuzione
del programma ad esso associato e pertanto il processo dopo aver trasferito il codice
di stato al processo padre viene distrutto dal sistema operativo.
In Figura 9 sono riportati il testo e il risultato dell’esecuzione del programma
forkwait1, che crea un processo figlio e pone il processo padre in attesa della
terminazione di tale figlio. Il processo figlio a sua volta termina con una exit
restituendo il valore di stato 5. Dopo la terminazione del figlio il padre riprende
l’esecuzione e stampa l’informazione ottenuta dalla wait. Si osservi che per stampare
correttamente il valore dello stato restituito dal figlio è necessario dividerlo per 256 e
che il padre riceve anche il pid del figlio terminato; quest’ultimo dato è utile quando
un processo padre ha generato molti figli e quindi ha bisogno di sapere quale dei figli
è quello appena terminato. #include <stdio.h> #include <sys/types.h> void main( ) { pid_t pid, miopid; int stato_exit, stato_wait; pid=fork( ); if (pid==0) { miopid=getpid( ); printf("sono il processo figlio con pid %i \n", miopid); printf("termino \n\n"); stato_exit=5; exit(stato_exit); } else { printf("ho creato un processo figlio \n\n"); pid=wait (&stato_wait); printf("terminato il processo figlio \n"); printf("il pid del figlio e' %i, lo stato e' %i\n",pid,stato_wait/256); } } a)il programma forkwait1
b)risultato dell’esecuzione di forkwait1
Figura 9
PROCESSI E FUNZIONI LINUX
Proff. Mauro Negri, Giuseppe Pelagatti 15
La Figura 10 mostra una possibile evoluzione temporale dei programmi
eseguiti dai due processi compatibile con l’esito di Figura 9.b.
Figura 10. Esecuzione sincronizzata
La situazione ottimale descritta in Figura 10 non deve tuttavia trarre in
inganno poiché l’evoluzione temporale mostrata non è garantita dal sistema operativo
come mostrato dalla Figura 11 nella quale il processo figlio termina la propria
esecuzione prima ancora che il processo padre si metta in attesa della sua
terminazione.
Figura 11
Per supportare l’evoluzione asincrona dei due processi si deve quindi
complicare il comportamento delle funzioni wait e exit:
- funzione exit(stato). Il sistema operativo memorizza il valore di stato nella
parte di sistema operativo dedicata al processo, chiude tutti i file aperti
presenti nella tabella dei file aperti del segmento di sistema del processo e
passa il processo dallo stato di “attivo” allo stato di “zombie”.
In Figura 11 il processo figlio dopo l’exit rimane quindi in vita, ma solo per
t
padre
figlio fork()
wait(..)
miopid=getpid( ); … exit(stato_exit);
printf("ho creato…"); ……. …... …
t
Processo figlio fork() printf("ho creato…"); wait(..)
miopid=getpid( ); … … exit(stato_exit);
printf("terminato…");
Processo padre
PROCESSI E FUNZIONI LINUX
Proff. Mauro Negri, Giuseppe Pelagatti 16
aspettare che il processo padre possa recuperare lo stato;
- funzione wait (&stato). Il sistema operativo verifica se esiste uno dei suoi
processi figli che abbia eseguito la exit. In caso affermativo (esempio di
Figura 11) il sistema operativo sblocca immediatamente il processo padre
all’atto dell’esecuzione della wait, preleva il valore dello stato dal processo
figlio e permette al processo padre di ripartire nell’esecuzione del proprio
programma e infine distrugge definitivamente il processo figlio. In caso
negativo (esempio di Figura 10) sospende il processo padre in attesa che un
processo figlio eseguendo una exit() lo metta in condizione di attivare le
operazioni conseguenti.
Si analizzano quindi alcuni casi particolari:
a) Un processo padre che non ha generato processi figli esegue una wait. In questo
caso il sistema operativo restituisce il codice di errore -1 e non pone in attesa il
processo padre.
b) Un processo padre esegue una wait in presenza di un processo figlio che non
esegue mai una exit (ad esempio per un ciclo infinito); in questo caso il processo
padre rimane sospeso all’infinito. Questa situazione richiede un intervento esterno
per forzare la terminazione di entrambi i processi.
c) Un processo padre termina l’esecuzione del proprio programma, provocando la
propria distruzione senza eseguire una wait, in presenza di uno o più processi figli
attivi. In questo caso il sistema operativo prende tutti i processi rimasti orfani dalla
morte del processo padre e li fa adottare al processo init. Quando questi processi
figli eseguono l’exit passano allo stato zombie senza avere più un padre che li
aspetti. Si noti che periodicamente il processo init esegue una wait proprio al fine
di eliminare i processi zombie inutilmente presenti nel sistema.
Una variante di wait: la funzione waitpid
La funzione wait mette un processo padre in attesa della terminazione di un
qualsiasi processo figlio, mentre in taluni casi potrebbe essere più significativo porlo
in attesa di uno specifico processo figlio. Esiste una variante di wait che permette di
sospendere l’esecuzione del processo padre in attesa della terminazione di uno
specifico processo figlio, di cui viene fornito il pid come parametro. Questa possibilità
non è ulteriormente analizzata.
PROCESSI E FUNZIONI LINUX
Proff. Mauro Negri, Giuseppe Pelagatti 17
6 Sostituzione del programma in esecuzione: la funzione exec La funzione exec permette di sostituire il programma in esecuzione in un
processo con un altro programma indicato dai parametri della funzione exec.
Esistono molte varianti sintattiche della funzione exec che si differenziano
nella modalità di scrittura dei parametri passati al nuovo programma.
La forma più semplice è la execl, descritta di seguito.
Prototipo della funzione execl
int execl (char *nome_programma, char *arg0, char *arg1, …NULL );
Il parametro nome_programma è una stringa che deve contenere l’identificazione
completa (con tutto il pathname) di un file eseguibile contenente il nuovo programma
da lanciare in esecuzione.
I parametri arg0, arg1, ecc… sono puntatori a stringhe che verranno passate al
main del nuovo programma lanciato in esecuzione; l’ultimo puntatore deve essere
NULL per indicare la fine dei parametri; per convenzione, il parametro argv[0]
contiene sempre il nome del programma stesso questa volta senza pathname.
La funzione ritorna -1 in caso di errore.
Modalità di passaggio dei parametri al main da parte di exec
Per ricevere i parametri la funzione main del programma da eseguire è definita
con l’intestazione:
int main(int argc, char * argv[ ])
dove:
il parametro argc è un intero che indica il numero di parametri ricevuti e il
parametro argv[ ] è un vettore di puntatori a stringhe.
La Figura 12 descrive iol comportamento della funzione exec. La funzione
invoca il sistema operativo, il quale esegue le seguenti operazioni:
• copia in un’area di memoria (buffer) del sistema operativo i dati
indirizzati da tutti i parametri della funzione;
• verifica che il file del nuovo programma sia un eseguibile esistente e
accessibile;
• dealloca lo spazio di memoria dedicato al segmento codice e dati (di
PROCESSI E FUNZIONI LINUX
Proff. Mauro Negri, Giuseppe Pelagatti 18
utente) del processo corrente. Si noti che il segmento di sistema rimane
allocato e quindi, ad esempio, i file aperti rimangono disponibili);
• alloca un segmento codice e un segmento dati adatti a contenere il nuovo
programma eseguibile con i propri dati;
• copia il vettore dei parametri della funzione exec dal buffer del sistema
allo stack del segmento dati del nuovo programma. L’indirizzo del vettore
(parametro argv della funzione main) viene memorizzato nel record di
attivazione della funzione main allocato nello stack. Si noti che argv[0]
corrisponderà quindi al parametro arg0 della funzione exec, argv[1] ad
arg1 e così via. Nel record di attivazione è anche memorizzato il numero
dei parametri della funzione exec (paramtro argc della funzione main);
• il PC viene quindi caricato con l’indirizzo della prima istruzione
eseguibile del nuovo programma.
A questo punto il processo che è rimasto lo stesso e con lo stesso pid si trova
ad eseguire il nuovo programma.
Programma 1void main(){PC -> exec(programma2 )… } …
segmento sistema
(file aperti,tty)
PC, SP,…
Programma 2void main(){… … }…
segmento sistema
(file aperti,tty)
PC, SP,…
Sistema operativo
Processo prima dell’exec
segmento dati Segmento dati
Processo dopo l’exec
Codice eseguibile
Nuovo programma
PC
argc, argv
Sistema operativo
BUFFER
Figura 12 Esecuzione della funzione exec
Si consideri la Figura 13 nella quale il programma main1 stampa il numero di
parametri ricevuti (argc) e i parametri stessi e il programma exec1 che lancia in
esecuzione il precedente programma main1 passandogli alcuni parametri.
PROCESSI E FUNZIONI LINUX
Proff. Mauro Negri, Giuseppe Pelagatti 19
#include <stdio.h> void main (int argc, char *argv[ ] ) { int i; printf("\nsono il programma main1\n"); printf("ho ricevuto %i parametri\n", argc); for (i=0; i<argc; i++) printf("il parametro %i è: %s\n", i, argv[i]); } a)il programma main1 #include <stdio.h> #include <sys/types.h> void main( ) { char P0[ ]="main1"; char P1[ ]="parametro 1"; char P2[ ]="parametro 2"; printf("sono il programma exec1\n"); execl("/home/pelagatt/esempi/main1", P0, P1, P2, NULL); printf("errore di exec"); /*normalmente non si arriva qui!*/ } b)il programma exec1
Figura 13
In figura 14 è mostrato il risultato dell’esecuzione del programma exec1. Si
noti che main1 scrive sullo stesso terminale di exec1, perché lo standard output è
rimasto identico, trattandosi dello stesso processo. Si noti che l’aver passato come
parametro P0 il nome del programma invocato “main1” non è una convenzione
richiesta dalla funzione exec.
Figura 14. Risultato dell’esecuzione di exec1
Il servizio exec assume particolare rilevanza in collaborazione con il servizio
PROCESSI E FUNZIONI LINUX
Proff. Mauro Negri, Giuseppe Pelagatti 20
fork, perchè utilizzando entrambi i servizi è possibile per un processo far eseguire un
programma e poi riprendere la propria esecuzione, come fanno ad esempio gli
interpreti di comandi. In Figura 15.a è mostrato a questo scopo lo pseudocodice di un
interprete comandi semplificato (programma simpleshell) che legge da terminale un
comando, procede a creare un processo figlio dedicato all’esecuzione del comando,
mentre il processo padre ne attende la sua terminazione prima di ripetere la richiesta
di un altro comando.
#include <stdio.h> #include <sys/types.h> #define fine “logout” #define prompt “simpleshell:” void main( ) { pid_t pid; int stato_wait;
…. while (not logout dell’utente) { printf (“%s”,prompt); //lettura riga di comando e identificazione componenti del comando
pid=fork( ); if (pid==0) {execl(comando, arg0, arg1, … argn, NULL); printf("errore di exec"); /*normalmente non si arriva qui!*/ exit( ); } else wait(&stato_wait ); } exit( ); } a)il programma simpleshell
b)risultato dell’esecuzione di simpleshell senza parametri
simpleshell: ./main1 sono il programma main1 ho ricevuto 1 parametri il parametro 0 è: ./main1 simpleshell:
PROCESSI E FUNZIONI LINUX
Proff. Mauro Negri, Giuseppe Pelagatti 21
c)risultato dell’esecuzione di simpleshell con tre parametri
Figura 15
Supponendo che il programma simpleshell sia in esecuzione, le Figure 15.b e 15.c
mostrano l’esecuzione del comando che invoca il programma main1 di Figura 13.a
con zero e tre parametri rispettivamente. Si noti che il programma simpleshell invoca
la funzione exec passando come primo parametro il nome del file poiché questa è la
convenzione che viene adottata dagli interpreti comandi; per averne una riprova è
sufficiente lanciare in esecuzione il programma main1 di Figura 13 direttamente da
interprete comandi.
Altre versioni della funzione exec
Esistono altre versioni della funzione exec che differiscono tra loro nel modo
in cui vengono passati i parametri al programma lanciato in esecuzione. In particolare
la execv permette di sostituire la lista di stringhe dalla execl con un puntatore a un
vettore di stringhe, in maniera analoga al modo in cui i parametri sono ricevuti nel
main; altre 2 versioni (execlp e execvp) permettono di sostituire il nome completo
(pathname) dell’eseguibile con il semplice nome del file e utilizzano il direttorio di
default per cercare tale file; infine 2 ulteriori versioni (execle e execve) hanno un
parametro in più che si riferisce all’ambiente di esecuzione (environment) del
processo.
simpleshell: ./main1 par1 par2 par3 sono il programma main1 ho ricevuto 4 parametri il parametro 0 è: ./main1 il parametro 1 è: par1 il parametro 2 è: par2 il parametro 3 è: par3 simpleshell:
PROCESSI E FUNZIONI LINUX
Proff. Mauro Negri, Giuseppe Pelagatti 22
7. Esecuzione parallela e valore delle variabili L’esecuzione parallela dei processi può rendere impossibile la definizione
dell’ordine temporale con il quale le variabili sono modificate nei differenti processi.
Per comprendere il problema si consideri il programma di Figura 16 e le tre tabelle,
una per il processo padre e una per ognuno dei due processi figli, aventi la struttura
mostrata in Figura 17, indicando, negli istanti di tempo specificati, il valore delle
variabili i, j, k, pid1 e pid2, e utilizzando le seguenti convenzioni:
• nel caso in cui al momento indicato la variabile non esista (in quanto non
esiste il processo) riportare NE;
• se la variabile esiste ma non se ne conosce il valore o non è determinabile
con certezza riportare U; a causa del parallelismo può essere impossibile
stabilire se gli altri processi hanno eseguito certe istruzioni e quindi se
hanno modificato certe variabili;
• si suppone che tutte le istruzioni fork abbiano successo e che il sistema
operativo assegni ai processi figli creati valori di pid pari a 500, 501.
Attenzione: la frase “dopo l’istruzione x” definisce l’istante di tempo
immediatamente successivo all’esecuzione dell’istruzione x da parte di un processo
(tale processo è univoco, data la struttura del programma);
La soluzione è riportata in Figura 18. Nel processo padre le variabili esistono
sempre, il valore di pid1 dopo l’istruzione 6 è assegnato a 500 e non cambia nelle
istruzioni successive, il valore di pid2 è sempre indeterminato, le variabili j e k
mantengono sempre il loro valore iniziale. Più complessa è la valutazione dello stato
della variabile i dopo le istruzioni 9 e 11, perchè queste istruzioni sono eseguite dai
processi figli e quindi non possiamo sapere se il processo padre ha già eseguito in
questi istanti l’istruzione 18, che modifica i. questo è il motivo per l’indicazione U
nelle corrispondenti caselle. Dopo l’istruzione 19 ovviamente i vale 11.
Nel primo processo figlio l’ultima riga vale NE perchè sicuramente in
quell’istante tale processo è sicuramente terminato. I valori di pid1, pid2, j e k sono
ovvi. Il valore di i è indeterminato sostanzialmente per lo stesso motivo indicato per il
processo padre.
Nel secondo processo figlio le motivazioni sono derivabili da quelle fornite
per i casi precedenti.
PROCESSI E FUNZIONI LINUX
Proff. Mauro Negri, Giuseppe Pelagatti 23
01: main() 02: { 03: int i, j, k, stato; 04: pid_t pid1, pid2; 05: i=10; j=20; k=30; 06: pid1 = fork(); /*creazione del primo figlio / 07: if(pid1 == 0) { 08: j=j+1; 09: pid2 = fork(); /*creazione del secondo figlio */ 10: if(pid2 == 0) { 11: k=k+1; 12: exit();} 13: else { 14: wait(&stato); 15: exit(); } 16: } 17: else { 18: i=i+1; 19: wait(&stato); 20: exit(); } 21: }
Figura 16
Valore delle variabili Istante
pid1 pid2 I j k
Dopo l’istruzione 6
dopo l’istruzione 9
Dopo l’istruzione 11
Dopo l’istruzione 19
figura 17 - Struttura delle 3 tabelle da compilare
PROCESSI E FUNZIONI LINUX
Proff. Mauro Negri, Giuseppe Pelagatti 24
Valore delle variabili Istante
pid1 pid2 I j k
Dopo l’istruzione 6 500 U 10 20 30
dopo l’istruzione 9 500 U U 20 30
Dopo l’istruzione 11 500 U U 20 30
Dopo l’istruzione 19 500 U 11 20 30
1. Valore delle variabili nel processo padre.
Valore delle variabili Istante
pid1 pid2 I j k
dopo l’istruzione 6 0 U 10 20 30
dopo l’istruzione 9 0 501 10 21 30
Dopo l’istruzione 11 0 501 10 21 30
Dopo l’istruzione 19 NE NE NE NE NE
2. Valore delle variabili nel primo processo figlio.
Valore delle variabili Istante
pid1 pid2 I j k
dopo l’istruzione 6 NE NE NE NE NE
dopo l’istruzione 9 0 0 10 21 30
Dopo l’istruzione 11 0 0 10 21 31
Dopo l’istruzione 19 NE NE NE NE NE
3. Valore delle variabili nel secondo processo figlio
Figura 18 – Soluzione dell’esercizio