Sei sulla pagina 1di 34

RIPASSO SERVER PARALLELO

      #    & ' & & 0 2 3 3    2 2 & 6  & 0 # &

.... sd = socket (...); bind (sd, ...); listen (sd, ...); while (1) { /* il server in un ciclo perenne */ new_sd = accept (sd, ...); pid = fork (); if (pid == 0) { /* sono nel figlio */ /* esegui le richieste del cliente utilizzando il socket connesso new_sd; alla fine chiudi new_sd ed esegui exit */ } else { /* sono nel padre */ /* chiudi new_sd e riprendi a eseguire la accept su sd */ } /* end if */ } /* end while */

Informatica 2 - L. Breveglieri - Esercitazione del 23 Marzo 2002

RIPASSO
      "     &

void main (int argc, char * argv[])

#include <stdio.h> void main (int argc, char *argv[]) { int i; printf (Il valore di argc e %d\n, argc); for (i = 0; i < argc; i++) { printf (Parametro numero %d = %s\n, i, argv[i]); } }
Y c c W f a X Y c V

Per sostituire il codice del figlio con un nostro programma e lanciarlo in esecuzione: MODO 1) argv[0] = prog; argv[1] = arg; argv[2] = (char ) NULL; execv (prog, argv);

MODO 2) execl (./prog, prog, arg, (char ) NULL); infatti abbiamo: int execl (char * path_prog, char * arg0, char * arg1, char * argn)

Informatica 2 - L. Breveglieri - Esercitazione del 23 Marzo 2002

Esercizio 1 Un processo padre (parent) crea due processi figli (children) e attende la loro terminazione. Se, e solo se, il secondo processo figlio (nellordine di creazione) termina prima del primo processo figlio, il processo padre deve creare un terzo processo figlio e fargli eseguire il codice del programma il cui nome la stringa prog, passandogli come argomento la stringa arg. Prima di terminare a sua volta, il processo padre deve attendere la terminazione di tutti i suoi processi discendenti, siano essi due o tre.

Informatica 2 - L. Breveglieri - Esercitazione del 23 Marzo 2002

TRACCIA DI SOLUZIONE DELLESERCIZIO 1 main () { / intestazione pid_t p1, p2; int stato; char argv[3]; / padre biforca due figli p1 = fork (); if (p1 == 0) { / 1o figlio istruzioni (omesse) finisce con exit ... exit (0); } / if / p2 = fork (); if (p2 == 0) { / 2o figlio istruzioni (omesse) finisce con exit ... exit (0); } / if / / biforca 2o figlio / biforca 1 figlio
o

/ / / / / / /

/ variabili di salvataggio PID / stato di uscita processo figlio / vettore per passaggio parametri

/ /

if (p2 == wait (&stato)) { / padre attesa termin. del 1 o 2 figl. / / terminato il figlio creato per secondo if (fork () == 0) {
o

/
o

/ padre - biforca 3 figlio

/ /

/ 3 figlio passaggio parametri e mutazione di codice argv[0] = prog; argv[1] = arg; argv[2] = (char ) NULL; execv (prog, argv); exit (1); } else { wait (&stato); / padre attesa terminazione figlio } / if / } / if / wait (&stato); / padre attesa terminazione figlio exit (0); } / main / Per semplificare la scrittura, si ricorda che in C le due notazioni seguenti sono equivalenti: pid_i p; pid_i p; p = fork (); if (p == 0) { ... } else { ... } / if / Informatica 2 - L. Breveglieri - Esercitazione del 23 Marzo 2002 5 if ((p = fork ()) == 0) { ... } else { ... } / if / / padre tutti i figli terminati, uscire / termina con errore se la exit fallisce

/ /

/ /

CODICE ESEGUITO DAL PADRE


main () { il padre qui attende la pid_t p1, p2; inizia qui int stato; terminazione del char argv[3]; 1 o del 2 figlio if ((p1 = fork ()) == 0) { / istruzioni del 1o figlio finisce con exit (0) / } if ((p2 = fork ()) == 0) { / istruzioni del 2o figlio finisce con exit (0) / } if (p2 == wait (&stato)) { qui attende la if (fork () == 0) { terminazione del 3 ... / istruzioni del 3o figlio / o del 1 figlio execv (prog,...); exit (1); } else { qui attende la wait (&stato); } terminazione del } rimanente figlio wait (&stato); exit (0); e termina qui }

Informatica 2 - L. Breveglieri - Esercitazione del 23 Marzo 2002

CODICE ESEGUITO DAL 1O FIGLIO


main () { il 1o figlio pid_t p1, p2; inizia qui int stato; char argv[3]; if ((p1 = fork ()) == 0) { / istruzioni del 1o figlio finisce con exit (0) } if ((p2 = fork ()) == 0) { / istruzioni del 2o figlio finisce con exit (0) } if (p2 == wait (&stato)) { if (fork () == 0) { ... execv (prog,...); exit (1); } else { wait (&stato); } } wait (&stato); exit (0); }

e termina qui

Informatica 2 - L. Breveglieri - Esercitazione del 23 Marzo 2002

CODICE ESEGUITO DAL 2O FIGLIO


main () { pid_t p1, p2; int stato; char argv[3]; if ((p1 = fork ()) == 0) { / istruzioni del 1o figlio finisce } if ((p2 = fork ()) == 0) { / istruzioni del 2o figlio finisce } if (p2 == wait (&stato)) { if (fork () == 0) { ... execv (prog, ...); exit (1); } else { wait (&stato); } } wait (&stato); exit (0); }

il 2o figlio inizia qui


/

con exit (0)

con exit (0)

e termina qui

Informatica 2 - L. Breveglieri - Esercitazione del 23 Marzo 2002

CODICE ESEGUITO DALLEVENTUALE 3O FIGLIO


main () { pid_t p1, p2; il 3o figlio int stato; inizia qui char argv[3]; if ((p1 = fork ()) == 0) { / istruzioni del 1o figlio finisce con exit (0) / } if ((p2 = fork ()) == 0) { / istruzioni del 2o figlio finisce con exit (0) / } e qui muta if (p2 == wait (&stato)) { codice if (fork () == 0) { (il codice "prog" ... execv (prog,...); terminer con una exit) exit (1); } else { wait (&stato); } termina qui } se la exec wait (&stato); fallisce exit (0); }

Informatica 2 - L. Breveglieri - Esercitazione del 23 Marzo 2002

Esercizio 2 Un server di rete, facente uso del protocollo TCP/IP, svolge la funzione seguente:

Attende perennemente connessioni dai clienti (diversi clienti possono essere connessi e serviti in parallelo); Per ogni connessione riceve dal clienti due stringhe (terminate dal carattere \0) che rappresentano: il nome completo di un programma da eseguire; un parametro da passare a tale programma. Il server deve eseguire il programma richiesto e quindi aspettare un nuovo comando dallo stesso cliente, senza perdere la connessione; La connessione con un cliente viene chiusa solo quando il server riceve come nome il programma q.

Tutte le stringhe sono di lunghezza inferiore a 40. Usare la funzione: exec (char nomeprogramma, char argv[]) per eseguire il programma. Si pu anche supporre gi scritta tutta la parte iniziale del server comprese le primitive socket, bind e listen. TRACCIA DI SOLUZIONE DELLESERCIZIO 2 ARCHITETTURA DEL SISTEMA DI RETE
punto terminale: ind. IP, port TCP> < punto terminale: <ind. IP, 1024>

Cliente
Canale TCP/IP q arg file

Server

processo richidente

coda delle connessioni pendenti

processo server

Il sistema costituito dalle parti seguenti:

Calcolatore server, che esegue il processo server e i vari programmi; il processo server dotato di parallelismo, ovvero in grado di servire pi richieste provenienti dai clienti.

Calcolatore cliente, che esegue il processo cliente.

Canale TCP/IP: viene aperto in modo attivo da parte del processo cliente, e in modo passivo da parte del processo server; tramite questo canale il processo cliente richiede un servizio al processo server, che viene poi espletato da parte di un programma.

Informatica 2 - L. Breveglieri - Esercitazione del 23 Marzo 2002

10

File server.c #define NUM_BYTES 40 #define SERVER_TCP_PORT 1024 #define MAX_PENDING_CONNECTIONS 10 main () { char comando[LENGTH], parametro[LENGTH]; char argv[3] = { (char ) NULL, (char ) NULL, (char ) NULL }; int public_sd, private_sd; struct sockaddr_in server, client; int client_lun = sizeof (client); int stato; / configura il punto terminale del server addr_initialize (&server, htons (SERVER_TCP_PORT), INADDR_ANY); / crea il socket TCP/IP del server e associalo al punto terminale public_sd = socket (AF_UNIX, SOCK_STREAM, 0); bind (public_sd, &server, sizeof (server));

/ /

/ e dimensiona la coda delle connessioni pendenti


listen (public_sd, MAX_PENDING_CONNECTIONS); / ora il socket associato al punto terminale del server while (1) { / ciclo perenne per lattesa delle connessioni private_sd = accept (public_sd, &client, &client_lun); if (fork () == 0) { / fork del server / codice del subserver / il subserver riceve il primo comando dal cliente if (recv (private_sd, comando, NUM_BYTES, 0) == 0) { exit (-1); / la connessione chiusa termina con errore } / if /

/
/ / / / / /

Informatica 2 - L. Breveglieri - Esercitazione del 23 Marzo 2002

11

while (strcmp (comando, q) != 0) { / confronto tra stringhe if (fork () == 0) { / fork del subserver / codice del subsubserver / il subsubserver esegue il programma comando if (recv (private_sd, parametro, NUM_BYTES, 0) == 0) { exit (-1);/ la connessione chiusa termina con errore } / if / argv[0] = comando; / assegnamento nome programma argv[1] = parametro; / assegnamento parametro execv (comando, argv);/ mutazione codice: comando exit (-1); / la exec fallita termina con errore } else { / codice del subserver wait (&stato); / attesa della fine del programma comando if (stato == -1) { / subsubserver terminato per errore printf (Il comando %s non esiste!\n, comando); } / if / } / if / / il subserver riceve un nuovo comando dal cliente if (recv (private_sd, comando, NUM_BYTES) == 0) { exit (-1); / la connessione chiusa termina con errore } / if / } / while / / il subserver ha ricevuto il comando q close (private_sd); / il subserver chiude il socket privato exit (0); / e termina correttamente } / if / / codice del server close (private_sd); / il server chiude il socket privato } / while / / e cicla subito su una nuova accept } / main /

/ / / / / / / / / / / /

/ /

/ / / / / /

Informatica 2 - L. Breveglieri - Esercitazione del 23 Marzo 2002

12

Esercizio 3 Scrivere in linguaggio C un processo server di rete (che fa uso del protocollo TCP/IP), in grado di ricevere richieste di connessione da parte di processi clienti. Il server un processo perenne e funziona secondo le specifiche seguenti:

Ogni cliente pu inviare una successione di comandi, contenente almeno un comando; i comandi sono costituiti da singoli caratteri, come per esempio a, b, c, ecc. Il comando q indica che il cliente ha terminato il colloquio con il server. Allinizio il server opera in modo sequenziale, ovvero serve un solo cliente alla volta. Il comando p forza il server a operare in modo parallelo, ovvero a servire pi clienti alla volta, senza limite sul numero di clienti serviti in parallelo. Il comando p forza la chiusura immediata della connessione con il cliente che lo ha inviato. Una volta entrato in modo parallelo, il server non ne esce pi, e ignora i successivi comandi p. I rimanenti comandi sono nomi di file, contenenti programmi da mettere in esecuzione. Non occorre passare argomenti a questi programmi. Quando il server mette in esecuzione un comando: se opera in modo sequenziale, attende sempre che il comando termini, prima di ricevere e interpretare il comando successivo proveniente dallo stesso cliente; se opera in modo parallelo, immediatamente disponibile a ricevere e interpretare il comando successivo proveniente dallo stesso cliente.

Si supponga gi data la parte iniziale del server, contenente le istruzioni di inizializzazione del socket e le chiamate alle primitive bind e listen.

char comando; int public_sd, private_sd; int modo = 0; / modo=0: sequenziale; modo=1; parallelo / ... bind(...); listen(...); ... while (1) {

Informatica 2 - L. Breveglieri - Esercitazione del 23 Marzo 2002

13

TRACCIA DI SOLUZIONE DELLESERCIZIO 3 ARCHITETTURA DEL SISTEMA DI RETE

processo richiedente

processo server

Canale TCP/IP
invia comandi a, b, , p, q

Cliente Server
Il sistema costituito dalle parti seguenti:

Calcolatore server, che esegue il processo server; tale processo server dotato di parallelismo, ovvero in grado di servire pi richieste provenienti dai clienti.

Calcolatore cliente, che esegue il processo cliente.

Canale di rete TCP/IP, tramite il quale il processo cliente invia comandi al processo server.

Il punto terminale (la coppia <indirizzo IP, port TCP>) del processo server non esplicitamente indicato nel testo dellesercizio: ovviamente sottinteso che esiste. Nella soluzioni baster indicare le primitive necessarie per programmare correttamente il socket del server, senza precisarne gli argomenti relativi al punto terminale.

Informatica 2 - L. Breveglieri - Esercitazione del 23 Marzo 2002

14

CODICE C DEL SERVER File server.c #include <...> / file di header #define SERVER_PUBLIC_PORT ... / port pubblico server main () { char com; int public_sd, private_sd; / socket del server struct sockaddr_in server, client; / punti terminali int server_lun = sizeof (server); / lunghezza p. term. int client_lun = sizeof (client); / lunghezza p. term. int stato; / stato di uscita int modo = 0; / modo = 0: sequenziale; modo = 1: parallelo addr_initialize (&server, htons (SERVER_PUBLIC_PORT), INADDR_ANY); public_sd = socket (AF_INET,SOCK_STREAM, 0); / crea socket bind (public_sd, &server, server_lun); / associa socket listen (public_sd, MAXCONN); / dimensiona coda while (1) { / server principale / accetta connessione private_sd = accept (public_sd, &client, &client_lun); if (fork () == 0) { / subserver recv (private_sd, &com, 1, 0); / riceve 1 comando while (com != q) { / finch non q if (com == p) { / comando 'p' if (modo == 0) { / in sequenziale modo = 1; / entra in parallelo exit (modo); / subserver termina } else { / p duplicato printf (multiple p command: ignored.\n); } / if / / fine analisi 'p' } else { / comando non 'p' if (fork () == 0) { / subsubserver execl (com1, ...2); / esegue comando exit (1); / termina per errore } else { / subserver attende 3 if (modo == 0) wait (void ); / sequenziale: attesa } / if / / fine analisi com. } / if / / fine analisi com. recv (private_sd, &com, 1, 0); / riceve nuovo comando } / while / / fine comandi close (private_sd); / chiude canale exit (0); / subserver termina } / if / / fine subserver if (modo == 0) wait (&modo); / sequenziale: attesa close (private_sd); / chiude canale } / while / / fine server princ. } / main /
Non sarebbe giusto, perch non una stringa, bens un carattere; ma non importa. La lista degli eventuali argomenti da passare al nuovo programma prog. 3 L'argomento sarebbe formalmente obbligatorio, ma qui non ha alcuna funzione.
2 1

/ / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /

Informatica 2 - L. Breveglieri - Esercitazione del 23 Marzo 2002

15

Esiste anche una soluzione banale, che prevede di introdurre due cicli while (1) in cascata: il primo con wait, il secondo senza: si cade dal primo nel secondo (con break) quando arriva il comando p ...

Informatica 2 - L. Breveglieri - Esercitazione del 23 Marzo 2002

16

1. int 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. }
Y w t g d q v

main () { /* processo P */ int *p, pid, pid1, status; p = malloc (sizeof (int)); pid = fork (); /* crea processo Q */ *p = pid; /*assegna il valore di pid alla variabile puntata da p */ if (pid == 0) { pid1 = fork (); /* crea processo R */ if (pid1 == 0) { free (p); /* dopo la free il valore di p non piu' valido (NV) */ exit (0); } else { pid = wait (&status); exit (0); } } else { *p = 0; return (0); }

*p pid
d q q d ` g V Y ` d

pid1
e


i b Y

g b

"

&

ESERCIZIO DI PARALLELISMO TRA PROCESSI: METODOLOGIA ED ESEMPIO Il programma seguente viene eseguito inizialmente da un processo P, che crea due processi figli PF1 e PF2 (i cui identificatori vengono assegnati alle variabili F1 e F2); a sua volta il processo PF1 crea due processi figli PN1 e PN2 (i cui identificatori vengono assegnati alle variabili N1 e N2); tali processi PN1 e PN2 sono immaginabili come nipoti del processo P. Nel programma sono indicati 3 punti di esecuzione, marcati con le etichette T1, T2 e T3. Queste etichette indicano gli istanti di tempo in cui lesecuzione del programma raggiunge tali punti. Si noti come tali istanti di tempo siano univoci, in quanto ognuno dei 3 punti di esecuzione raggiunto nellambito di uno solo dei processi creati; in particolare, per listante T1 si fa lipotesi di considerare lassegnamento alla variabile F1 contemporaneo allesecuzione della fork() stessa. Si chiede di riempire le tabelle seguenti, che indicano i valori assunti dalle diverse copie delle variabili del programma negli istanti di tempo T1, T2 e T3, a seconda dei contesti dei processi che vengono via via creati. Nota: fun() indica una generica funzione di cui non si conoscono i dettagli. main() { / programma principale crea vari figli e nipoti int F1, F2, N1, N2; Listante di tempo quello: F1 = fork(); if (F1 == 0) { N1 = fork(); if (N1 == 0) { fun(); exit(void); } else { N2 = fork(); } / if / if (N2 == 0) { fun(); exit(void); } else { /

T1

subito dopo la fork()

/ biforcazione / biforcazione / terminazione / biforcazione

/ / / /

/ terminazione subito prima della exit() / terminazione

/ /

T2

exit(void); } / if / } else { wait(void); F2 = fork(); if (F2 == 0) { exit(void); } else { wait(void);

/ sincronizzazione / / biforcazione / / terminazione subito prima della exit() /

T3

exit(void); } / if / } / if / } / main /

/ sincronizzazione / / terminazione /

La simbologia da usare per riempire le caselle delle varie tabelle la seguente: NE: significa che il contesto non esiste (e dunque non esiste neppure la variabile), nel senso che il processo in questione non esiste ancora o gi terminato; 0: significa che la variabile esiste e ha valore zero; PF1, PF2, PN1 e PN2: indicano i valori dei pid dei rispettivi processi; Informatica 2 - Sezione B (L. Breveglieri) - A.A. 2001 / 2002 4

X: significa che la variabile esiste, ma il suo valore non stato assegnato, ovvero la variabile non ancora stata inizializzata; ?: significa che la variabile pu non esistere e/o essere caratterizzata da diversi dei valori precedenti, senza per che si possa stabilirne lo stato effettivo, perch dipende dallordine di esecuzione di processi paralleli. TABELLE DA COMPILARE

Contesto di P T1 T2 F1 F2 N1 N2 Contesto di PF1 T1 T2 F1 F2 N1 N2

T3

Contesto di PF2 T1 T2 F1 F2 N1 N2 Contesto di PN1 T1 T2 F1 F2 N1 N2

T3

T3

T3

Informatica 2 - Sezione B (L. Breveglieri) - A.A. 2001 / 2002

METODOLOGIA DI SOLUZIONE DELLESERCIZIO DI PARALLELISMO TRA PROCESSI La soluzione di questo esercizio ricavata seguendo una precisa metodologia, consistente nell'effettuare prima alcune analisi relative alla struttura e al comportamento del programma proposto, e poi nell'effettuare una sintesi riunendo gli esiti delle varie analisi allo scopo di rispondere alle domande poste. Le fasi sono le seguenti: Analisi:

Individuare i processi e tracciare l'albero dei processi, evidenziante le relazioni padre-figlio sussistenti tra i vari processi.

Enucleare (o "esporre") le parti di codice eseguite da ciascun processo, in modo da chiarire le attivit effettivamente svolte da ciascun processo.

Tracciare i diagrammi temporali evidenzianti l'andamento dei vari processi. I diagrammi possono essere pi di uno, perch vi saranno, in generale, vari casi da esaminare, giacch il comportamento temporale di ciascun processo potrebbe non essere interamente definito dalle informazioni fornite del codice o contenute nel testo del problema. Elencare, per ogni istante di tempo, lo stato di funzionamento di ciascun processo, dandone una breve descrizione; questi dati si ricavano "incrociando" le informazioni fornite da ciascuna delle tre fasi di analisi precedenti.

Sintesi:

Riportare nelle tabelle, servendosi della simbologia proposta nel testo del problema, gli esiti della sintesi appena effettuata. Questa non che una mera trascrizione simbolica.

Ecco la prima fase di analisi: nomi e albero dei processi. ALBERO DEI PROCESSI

PADRE

FIGLI

PF1

PF2

NIPOTI
     

PN1

PN2

&

&

&

necessario ricordare che la sincronizzazione tra processi, tramite la primitiva wait(), pu avvenire solo tra processo padre e processo figlio, non tra processo padre e processo nipote (o pronipote, ecc). Per esempio, P pu sincronizzarsi, tramite una chiamata alla primitiva wait(), sulla terminazione di PF1 o di PF2, ma non di PN1 n di PN2. Si noti come lalbero dei processi di Figura 1 renda ragione solo dei rapporti di discendenza esistenti tra i vari processi; esso non contiene alcuna informazione di carattere temporale, cio non specifica alcun ordine di esecuzione dei processi, tranne linformazione (peraltro banale, giacch sempre vera) che un discendente non pu iniziare a esistere prima dellinizio dellesistenza di un suo qualsiasi antenato. Pi avanti sar necessario tracciare dei diagrammi di flusso temporale per analizzare il comportamento temporale dei vari processi. Prima per, per familiarizzarsi con la struttura del problema, conviene esporre il codice eseguito da ciascun processo. Ecco la seconda fase di analisi: enucleazione (o "esposizione") del codice eseguito da ciascun processo. In grigio vengono oscurate le parti di codice non eseguite dal processo sotto analisi. Informatica 2 - Sezione B (L. Breveglieri) - A.A. 2001 / 2002 6

Esposizione del codice eseguito dal processo P: definisce univocamente listante di tempo T1.
main() { / programma principale crea vari figli e nipoti int F1, F2, N1, N2; Listante di tempo quello: F1 = fork(); / biforcazione subito dopo T1 if (F1 == 0) { la fork() N1 = fork(); / biforcazione if (N1 == 0) { fun(); lassegnamento a F1 exit(void); / terminazione contemporaneo a fork } else { N2 = fork(); / biforcazione } / if / if (N2 == 0) { fun(); T1 viene univocamente exit(void); / terminazione definito in questo punto } else { exit(void); / terminazione subito prima T2 } / if / } else { della exit() wait(void); / sincronizzazione F2 = fork(); / biforcazione if (F2 == 0) { exit(void); / terminazione subito prima } else { T3 della exit() wait(void); / sincronizzazione exit(void); / terminazione } / if / } / if / } / main / / / /

/ /

/ /

/ / / / /

Esposizione del codice eseguito dal processo PF1: definisce univocamente listante di tempo T2.
main() { / programma principale crea vari figli e nipoti int F1, F2, N1, N2; Listante di tempo quello: F1 = fork(); / biforcazione subito dopo if (F1 == 0) { T1 la fork() N1 = fork(); / biforcazione if (N1 == 0) { fun(); exit(void); / terminazione lassegnamento a F1 } else { contemporaneo a fork N2 = fork(); / biforcazione } / if / T2 viene univocamente if (N2 == 0) { definito in questo punto fun(); exit(void); / terminazione } else { exit(void); / terminazione subito prima T2 } / if / della exit() } else { wait(void); / sincronizzazione F2 = fork(); / biforcazione if (F2 == 0) { exit(void); / terminazione subito prima T3 } else { della exit() wait(void); / sincronizzazione exit(void); / terminazione } / if / } / if / } / main / / / /

/ /

/ /

/ / / / /

Informatica 2 - Sezione B (L. Breveglieri) - A.A. 2001 / 2002

Esposizione del codice eseguito dal processo PF2: definisce univocamente listante di tempo T3.
main() { / programma principale crea vari figli e nipoti int F1, F2, N1, N2; Listante di tempo quello: F1 = fork(); / biforcazione subito dopo if (F1 == 0) { T1 la fork() N1 = fork(); / biforcazione if (N1 == 0) { fun(); lassegnamento a F1 exit(void); / terminazione contemporaneo a fork } else { N2 = fork(); / biforcazione } / if / if (N2 == 0) { fun(); exit(void); / terminazione } else { exit(void); / terminazione subito prima T2 } / if / della exit() } else { wait(void); / sincronizzazione T3 viene univocamente F2 = fork(); / biforcazione definito in questo punto if (F2 == 0) { exit(void); / terminazione subito prima } else { T3 della exit() wait(void); / sincronizzazione exit(void); / terminazione } / if / } / if / } / main / / / /

/ /

/ /

/ / / / /

Esposizione del codice eseguito dal processo PN1: non definisce nessun istante di tempo.
main() { / programma principale crea vari figli e nipoti int F1, F2, N1, N2; Listante di tempo quello: F1 = fork(); / biforcazione subito dopo if (F1 == 0) { T1 la fork() N1 = fork(); / biforcazione if (N1 == 0) { lassegnamento a F1 fun(); contemporaneo a fork exit(void); / terminazione } else { N2 = fork(); / biforcazione } / if / if (N2 == 0) { fun(); exit(void); / terminazione } else { exit(void); / terminazione subito prima } / if / T2 della exit() } else { wait(void); / sincronizzazione F2 = fork(); / biforcazione if (F2 == 0) { exit(void); / terminazione subito prima T3 } else { della exit() wait(void); / sincronizzazione exit(void); / terminazione } / if / } / if / } / main / / / /

/ /

/ /

/ / / / /

Informatica 2 - Sezione B (L. Breveglieri) - A.A. 2001 / 2002

Le regioni del codice mascherate in grigio sono quelle che il processo relativo non pu raggiungere, e dunque che non esegue. Si noti come gli istanti di tempo T1, T2 e T3 siano univocamente definiti. Infatti, ogni etichetta Tx (con x = 1, 2, 3) ha la punta che cade in una zona del codice non mascherata nellambito di uno solo dei quattro processi P, PF1, PF2 e PN1 (e infatti PN1 non definisce nessun istante di tempo, giacch ci sono 4 processi ma le etichette Tx sono solo 3). Letichetta T1 sembra fare eccezione, perch la sua punta cade in una zona non mascherata del codice nellambito di due processi (e precisamente nellambito di P e PF1), i quali di per s non sarebbero tenuti a raggiungere tale punto di esecuzione proprio nello stesso istante di tempo. Ma lipotesi lassegnamento a F1 contemporaneo alla fork() fa si che listante di tempo T1 debba essere necessariamente lo stesso sia nellambito di P sia nellambito di PF1, giacch la fork() una primitiva di sistema operativo, non unoperazione interna al processo, e dunque viene eseguita ununica volta in un istante di tempo ben preciso, che deve essere lo stesso per P e PF1. Cos, anche listante di tempo T1 definito univocamente, al pari di T2 e T3. Per ipotesi lassegnamento F1 = fork() va considerato non interrompibile: questo significa che allistante di tempo T1 la variabile F1 certamente assegnata con il valore restituito dalla fork() sia nel contesto di P sia nel contesto di PF1 (naturalmente i due valori sono differenti). Ora tutto pronto per tracciare i diagrammi temporali dei vari processi (terza fase di analisi): si possono distinguere tre casi, mostrati nella Figura 2, Figura 3 e Figura 4, rispettivamente. DIAGRAMMI DI FLUSSO TEMPORALE DEI PROCESSI
Asse dei Processi Asse dei Tempi P

crea T1 F1 = fork() PF1 crea wait() N1 = fork() PN1 f u n () exit() crea PN2 f u n ()

P sospeso

N2 = fork()

T2 P si risveglia sincronizza F2 = fork() crea wait() P sospeso T3 P si risveglia sincronizza exit()


     " # % ' ) 0 3 6  7  8 0 7

exit()

PF2 exit()

exit()

Il diagramma di flusso temporale di Figura 2 comprende lasse dei tempi, su cui sono riportati gli istanti di tempo T1, T2 e T3, e lasse dei processi, su cui figurano i processi via via creati. Gli istanti di tempo Tx (con x = 1, 2, 3) sono identificati dal raggiungimento, da parte di un processo ben definito, di un determinato punto di esecuzione. Il tempo avanza in modo discreto. Ogni processo ha un proprio sviluppo temporale, punteggiato da chiamate alle primitive del sistema operativo. Queste primitive possono creare nuovi processi, far terminare il processo stesso oppure sincronizzarlo con la terminazione di uno dei suoi processi figli. Talvolta la disposizione delle primitive tale da permettere di trarre conclusioni circa lordinamento temporale dei flussi di esecuzione dei vari processi, ma non sempre. In particolare, nulla si pu dire circa gli istanti di tempo in cui i processi PN1 e PN2 terminano, perch il loro padre comune (il processo PF1) non si sincronizza con la loro terminazione. Questo d origine a svariati altri casi, oltre a quello mostrato nella Figura 2; i casi rilevanti sono mostrati nella Figura 3 e Figura 4. Informatica 2 - Sezione B (L. Breveglieri) - A.A. 2001 / 2002 9

Asse dei Processi Asse dei Tempi P

crea T1 F1 = fork() PF1 crea wait() N1 = fork() PN1 crea P sospeso N2 = fork() f u n () PN2 f u n ()

T2 P si risveglia sincronizza F2 = fork() crea PF2 exit()

exit() exit()

wait() P sospeso T3 P si risveglia sincronizza exit()


      ! # % & ' ) 1 4 7  9  @  1 9 1 7 @  E

exit()

Asse dei Processi Asse dei Tempi P

crea T1 F1 = fork() PF1 crea wait() N1 = fork() PN1 crea P sospeso N2 = fork() f u n () PN2 f u n ()

T2 P si risveglia sincronizza F2 = fork() crea wait() P sospeso T3 P si risveglia sincronizza exit() PF2 exit()

exit()

exit() exit()
F      ! # % & ' ) 1 4 7  9  @  E

Losservazione fondamentale sui tre diagrammi di flusso temporale di Figura 2, Figura 3 e Figura 4 che i processi PF1 e PF2 sono certamente serializzati, perch P crea PF2 solo dopo essersi sincronizzato sulla terminazione di PF1. Invece, nulla si pu dire circa la terminazione di PN1 e PN2 (lunica certezza che essi vengono creati dopo T1), giacch essi eseguono la funzione fun(), la cui durata per ipotesi impredicibile. Le tre figure mostrano le possibili terminazioni di PN1, che sono rilevanti ai fini dei quesiti posti dallesercizio. I casi di terminazione di PN2 sarebbero altrettanti, ma il processo PN2 ha poca o nessuna importanza ai fini dei quesiti. Informatica 2 - Sezione B (L. Breveglieri) - A.A. 2001 / 2002 10

Dai tre diagrammi di flusso temporale si deduce che: listante di tempo T1 precede senza dubbio listante di tempo T2; listante di tempo T2 precede senza dubbio listante di tempo T3.

Si pu ora passare alla sintesi. In forza delle considerazioni sopra fatte e dei diagrammi di flusso temporale tracciati, si possono fare le osservazioni particolari seguenti, relative allo stato dei vari processi: al tempo T1: il processo P in esecuzione (e definisce listante di tempo T1); il processo PF1 esiste ( stato appena creato); i processi PF2, PN1 e PN2 non esistono ancora. STATO ESEC NUOVO NON SOSP ESEC NON SCONOSC

al tempo T2: il processo P in stato di sospensione (in attesa della terminazione di PF1); il processo PF1 in esecuzione (e definisce listante di tempo T2); il processo PF2 non esiste ancora; i processi PN1 e PN2 potrebbero esistere ancora o essere gi terminati.

al tempo T3: il processo P in stato di sospensione (in attesa della terminazione di PF2); il processo PF1 non esiste pi; il processo PF2 in esecuzione (e definisce listante di tempo T3); i processi PN1 e PN2 potrebbero esistere ancora o essere gi terminati. SOSP TERMIN ESEC SCONOSC

Con queste informazioni facile compilare le colonne delle tabelle proposte. Per le colonne in cui il processo (e dunque il contesto) certamente esistente, occorre indicare i valori delle variabili: questi si ricavano facilmente esaminando il contesto del processo. necessario tenere presente che ogni processo possiede le proprie copie delle quattro variabili F1, F2, N1 e N2. Ogni processo procede ad aggiornare le proprie copie delle quattro variabili in modo autonomo. Laggiornamento di una variabile nel contesto di un dato processo non ha alcun effetto sui valori delle variabili nei contesti degli altri processi. Quando un processo si biforca, padre e figlio hanno, inizialmente, copie di valore identico delle quattro variabili. Questo vale anche per le variabili che non fossero ancora state assegnate nemmeno una volta (e neppure inizializzate in sede di dichiarazione). I valori delle variabili possono in seguito differenziarsi, via via che i processi padre e figlio si sviluppano diversamente. TABELLE COMPILATE

Contesto di P T1 T2 F1 PF1 PF1 F2 X X N1 X X N2 X X Contesto di PF1 T1 T2 F1 0 0 F2 X X N1 X PN1 N2 X PN2

T3 PF1 PF2 X X

Contesto di PF2 T1 T2 F1 NE NE F2 NE NE N1 NE NE N2 NE NE Contesto di PN1 T1 T2 F1 NE ? F2 NE ? N1 NE ? N2 NE ?

T3 PF1 0 X X

T3 NE NE NE NE

T3 ? ? ? ?

Informatica 2 - Sezione B (L. Breveglieri) - A.A. 2001 / 2002

11

In un caso particolare padre e figlio possono avere fin dal principio (cio subito dopo la biforcazione del processo padre e la conseguente creazione del processo figlio) una variabile che nel contesto del padre presenta un valore diverso da quello che ha nel contesto del figlio. Se un processo padre si biforca e crea un processo figlio tramite unistruzione del tipo F = fork(), allora la variabile F (supponendo che non fosse gi stata inizializzata in sede di dichiarazione) possiede da subito (cio un infinitesimo di tempo dopo che la biforcazione avvenuta ed entrambi i processi sono nellesistenza) un valore ben definito in entrambi i contesti, che nel contesto del padre sar per il pid del processo figlio, mentre nel contesto del figlio sar 0. Questo non sarebbe per del tutto esatto. Infatti lassegnamento eseguito dal codice del processo, non dal codice del sistema operativo, che per parte sua si limita a eseguire la primitiva fork() e a lasciarne il valore di uscita sulla cima della pila di entrambi i processi padre e figlio (naturalmente i due valori sono diversi). Inoltre lassegnamento richiede un certo tempo, che per quanto breve non per infinitesimo; non dunque detto che i due processi padre e figlio portino a termine lassegnamento nel medesimo istante di tempo (anzi, a rigore certo che listante di tempo non possa essere lo stesso, perch lo schedulatore del sistema operativo dovr pur scegliere quale dei due processi padre e figlio mettere per primo in stato di esecuzione). Ma lipotesi lassegnamento a F contemporaneo a fork(), specificata nel testo dellesercizio, elimina anche questa (seppur lieve) ambiguit, poich identifica lassegnamento alla primitiva fork() stessa, la quale eseguita da parte del sistema operativo una sola volta (ovvero lesecuzione della primitiva fork() la stessa per padre e figlio) in un istante di tempo ben definito. Come conclusione, si pu osservare che la tabella relativa al contesto del processo PN2, se fosse richiesta, sarebbe la seguente:

Contesto di PN2 T1 T2 F1 NE ? F2 NE ? N1 NE ? N2 NE ?

T3 ? ? ? ?

ovvero coinciderebbe con la tabella relativa al contesto di PN1.

(fine dell'esercizio)

Informatica 2 - Sezione B (L. Breveglieri) - A.A. 2001 / 2002

12

ESERCIZIO (SISTEMI OPERATIVI) Un processo padre P crea nellordine i tre processi figli F1, F2 e F3, e, dopo averli creati, si mette in attesa della loro terminazione. I tre figli evolvono in modo autonomo, eseguendo tre programmi diversi, e il cui comportamento peraltro sconosciuto. Quando i processi figli sono tutti terminati, anche il processo padre termina, visualizzando i pid dei tre processi figli, in ordine di terminazione. A questo scopo, il processo padre P memorizza lelenco dei pid dei processi figli biforcati in un array di tipo pid_t pid[3], nel quale scrive, uno dopo laltro, i pid dei tre processi figli creati tramite le primitiva fork(). Si devono completare le quattro tabelle seguenti, mostranti il contenuto dellarray pid[] nel contesto del processo padre P, un istante di tempo dopo la creazione del padre stesso e dei suoi tre figli F1, F2 e F3, e nel contesto dei processi figli F1, F2 e F3 stessi, un istante dopo la loro rispettiva creazione (cio per F1 subito dopo la creazione di F1, per F2 subito dopo la creazione di F2 e per F3 subito dopo la creazione di F3; s'intende che non ha senso chiedersi cosa valesse per esempio pid[0] nel contesto di F1 subito dopo la creazione del padre P, perch in quel momento il processo F1, e quindi il suo contesto, non esisteva ancora; allo stesso modo, subito dopo la creazione del processo F1 i processi F2 e F3 non esistono ancora).

Contesto di P pid[] pid[0] pid[1] pid[2] Contesto di F1 pid[] pid[0] pid[1] pid[2] Contesto di F2 pid[] pid[0] pid[1] pid[2]

Pid[] subito dopo la creazione di: P F1 F2 F3

Pid[] subito dopo la creazione di: P F1 F2 F3

Pid[] subito dopo la creazione di: P F1 F2 F3

Informatica 2 - Sezione B (L. Breveglieri) - A.A. 2001 / 2002

13

Contesto di F3 pid[] pid[0] pid[1] pid[2]

Pid[] subito dopo la creazione di: P F1 F2 F3

Informatica 2 - Sezione B (L. Breveglieri) - A.A. 2001 / 2002

14

TRACCIA DI SOLUZIONE DELLESERCIZIO

Contesto di P pid[] pid[0] pid[1] pid[2] Contesto di F1 pid[] pid[0] pid[1] pid[2] Contesto di F2 pid[] pid[0] pid[1] pid[2] Contesto di F3 pid[] pid[0] pid[1] pid[2]

pid[] subito dopo la creazione di: P X X X F1 F1 X X F2 F1 F2 X F3 F1 F2 F3

pid[] subito dopo la creazione di: P F1 non esiste F1 0 X X pid[] subito dopo la creazione di: P F2 non esiste F1 F2 F1 0 X F3 comp. di F2 ignoto comportamento di F1 ignoto F2 F3

pid[] subito dopo la creazione di: P F1 F3 non esiste F2 F3 F1 F2 0

Legenda: il simbolo Fx (con x = 1, 2, 3) indica il pid del processo Fx; il simbolo 0 indica il valore 0; il simbolo X indica che la variabile non ancora stata inizializzata.

Informatica 2 - Sezione B (L. Breveglieri) - A.A. 2001 / 2002

15

CODICE C DEL PROCESSO PADRE <unistd.h> <stdlib.h> <sys/wait.h> <stdio.h>

crea P

#include #include #include #include main() {

/ file di intestazione / file di intestazione / file di intestazione / file di intestazione

/ / / /
/ / / / / / /

pid_t pid[3]; pid_t term[3]; int n; int stato; / padre / pid[0] = fork (); if (pid[0] == 0) { pid[1] = fork (); if (pid[1] == 0) { pid[2] = fork (); if (pid[2] == 0) { / padre /

/ pid figli, in ordine di biforcazione / pid figli, in ordine di terminazione / variabile accessoria / riceve stato di terminazione figlio crea F1 / biforca 1o figlio / corpo 1 figlio / ...; exit (); } / biforca 2o figlio / corpo 2 figlio / ...; exit (); } / biforca 3o figlio / corpo 3 figlio / ...; exit (); }

/ ciclo di attesa terminazione figli for (n = 0; n < 3; n++) {

crea F2

/ / /

term[n] = wait (&stato); / un figlio termina } / for / / stampa pid figli in ordine di terminazione for (n = 0; n < 3; n++) {

crea F3

printf (Terminato figlio con pid %d\n, term[n]); } / for / exit (0); / il padre termina } / main / /

(fine dell'esercizio)

Informatica 2 - Sezione B (L. Breveglieri) - A.A. 2001 / 2002

16

01: 02: 03: 04: 05: 06: 07: 08: 09: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20:
` w t g e e v f W v W g W

main() { int i, j, dati[2], status; pid_t pid; i = 0; dati[0] = dati[1] = -1; for (j=0; j<2; j++) { pid = fork(); if(pid == 0) { dati[i] = j; if (j==0) { execl("/bin/pwd", "/bin/pwd", NULL); exit(1); } exit(1); } if (j==1) pid = waitpid(pid, &status, 0); i++; } exit(0); }
q v f e g e q v e g g e e c ` q q ` f W t ` f q v q e W a d W X v a d ` a e h g W W c q v a q W d W q e w t ` c t e X W W X v q W W g e v g ` f e d e g g e W t W d d v q W h i j e d v q W h j k l q q e a m W ` a e

a s t u


q c ` t e

| u

g v f h `

` u

s t s t s t s t

s t s t s t s t

s t s t s t s t

"

'

'

'

"

Esercizio - Processi Paralleli

Il programma seguente viene eseguito inizialmente da un processo P, che crea due processi figli PF1 e PF2 (i cui identificatori vengono assegnati alle variabili F1 e F2); a sua volta il processo PF1 crea un processo figlio PN (il cui identificatore viene assegnato alla variabile N); tale processo PN immaginabile come nipote del processo P. Nel programma sono indicati tre punti di esecuzione, marcati con le etichette T1, T2 e T3. Queste etichette indicano gli istanti di tempo in cui lesecuzione del programma raggiunge tali punti. Si noti come tali istanti di tempo siano univoci, in quanto ognuno dei tre punti di esecuzione raggiunto nellambito di uno solo dei processi creati; in particolare, per l'istante T1 si fa lipotesi di considerare l'assegnamento alla variabile F1 contemporaneo allesecuzione della fork() stessa. Si chiede di riempire le tabelle seguenti, che indicano i valori assunti dalle diverse copie delle variabili del programma negli istanti di tempo T1, T2 e T3, a seconda dei contesti dei processi che vengono via via creati.
main () { / programma principale crea vari figli e nipoti int status; Listante di tempo quello: int F1, F2, N; F1 = fork (); / biforcazione subito dopo if (F1 == 0) { T1 la fork N = fork (); / biforcazione if (N == 0) { / mutazione codice execl ("nipote", NULL); exit (-1); / terminazione } / if / subito dopo T2 wait (&status); / sincronizzazione la exec exit (N); / terminazione } / if / subito prima T3 della exec wait (&status); / sincronizzazione F2 = fork (); / biforcazione if (F2 == 0) { execl ("figlio", NULL); / mutazione codice exit (-1); / terminazione } / if / } / main /
/

/ /
/

/ / / / / / /

La simbologia da usare per riempire le caselle delle varie tabelle la seguente:

nel caso in cui al momento indicato la variabile non esista (in quanto non esiste il processo cui la variabile appartiene) riportare NE;

quando non si pu dire con certezza se la variabile esista e / o quale ne sia il valore, riportare U;

quando si pu dire con certezza che la variabile esiste e se ne conosce il valore, riportare il valore della variabile stessa.
1

Informatica II - prof. Luca Breveglieri - A.A. 2001 / 2002

TABELLE DA COMPILARE

Var. F1 F2 N status

Contesto di P Istanti di tempo T1 T2 T3

Var. F1 F2 N status

Contesto di PF1 Istanti di tempo T1 T2 T3

Var. F1 F2 N status

Contesto di PF2 Istanti di tempo T1 T2 T3

Var. F1 F2 N status

Contesto di PN Istanti di tempo T1 T2 T3

Informatica II - prof. Luca Breveglieri - A.A. 2001 / 2002

TRACCIA DI SOLUZIONE

main () { int status; Listante di tempo quello: int F1, F2, N; F1 = fork (); subito dopo if (F1 == 0) { T1 la fork N = fork (); if (N == 0) { execl ("nipote", NULL); exit (-1); } / if / subito dopo T2 wait (&status); la exec exit (N); subito prima T3 } / if / della exec wait (&status); F2 = fork (); if (F2 == 0) { execl ("figlio", NULL); exit (-1); } / if / } / main /

Informatica II - prof. Luca Breveglieri - A.A. 2001 / 2002

TRACCIA DI SOLUZIONE

Var. F1 F2 N status

Contesto di P Istanti di tempo T1 T2 T3 PF1 PF1 U U U U U U U U U U Contesto di PF1 Istanti di tempo T1 T2 T3 0 0 NE NE U U PN NE U NE U U Contesto di PF2 Istanti di tempo T1 T2 T3 NE NE PF1 NE NE 0 NE NE U NE NE PN Contesto di PN Istanti di tempo T1 T2 T3 NE NE U NE NE U NE NE U NE NE U

Var. F1 F2 N status

Var. F1 F2 N status

Var. F1 F2 N status

la variabile esiste, ma non stata ancora inizializzata. il processo cui la variabile appartiene ha mutato codice. il processo cui la variabile appartiene potrebbe essere esistente, gi terminato o non ancora iniziato.

Informatica II - prof. Luca Breveglieri - A.A. 2001 / 2002