Programmazione di sistema e gestione dei processi in

  • Slides: 57
Download presentation
Programmazione di sistema e gestione dei processi in C

Programmazione di sistema e gestione dei processi in C

Indice • Dalla macchina astratta sequenziale C • Alla macchina “parallela” – Processi e

Indice • Dalla macchina astratta sequenziale C • Alla macchina “parallela” – Processi e programmi • Le primitive per la gestione dei processi – Creazione, sospensione, morte dei processi – Esecuzione dei programmi • Semplici(ssime) applicazioni

Processo • Una singola macchina astratta cui viene assegnato un compito – Non solo

Processo • Una singola macchina astratta cui viene assegnato un compito – Non solo in ambito “informatico”: • Processo chimico • Processo industriale • … – Sistema: • • Diversi processi che cooperano (o si coordinano, o competono) procedendo In parallelo Spesso in modo asincrono – Qui ci interessano processi realizzati mediante macchine astratte informatiche (C in particolare) • Al momento si coordinano tra loro • In futuro: – – Internet Controllo di impianti Sistemi embedded …

Processo C • Macchina astratta C • Che esegue un programma C

Processo C • Macchina astratta C • Che esegue un programma C

Sistema di processi su un calcolatore Programma 1 Programma 2 Programma n Processo 1:

Sistema di processi su un calcolatore Programma 1 Programma 2 Programma n Processo 1: Processo 2: Processo n: Macchina astratta C Macchina astratta non C Virtualizzazione Hardware + Sistema Operativo

Struttura e primitive di un processo (C- Linux) • PID • Ogni processo (tranne

Struttura e primitive di un processo (C- Linux) • PID • Ogni processo (tranne init) creati da altri processi padri, figli, ecc. • Memoria (stato) di un processo: – Segmento codice – Segmento dati • Statici • Dinamici – Stack – Heap – Segmento di sistema (interfaccia con il SO) • Tabella file aperti • Socket (vedi programmazione di rete) • …

La gerarchia dei processi init P 1(SO) getty (SO) Pn(SO) … login (SO) shell

La gerarchia dei processi init P 1(SO) getty (SO) Pn(SO) … login (SO) shell (user) Processo 1 Processo 2 Processo 1 (comando)

Primitive per la gestione di processi (C- Linux) • Generare un processo figlio •

Primitive per la gestione di processi (C- Linux) • Generare un processo figlio • Attendere la terminazione di un processo figlio • Terminare un processo figlio • Sostituire il programma (segmento codice) eseguito da un processo

Generazione e terminazione di processi • pid_t fork (void) • esempio: fork ( )

Generazione e terminazione di processi • pid_t fork (void) • esempio: fork ( ) – Biforca il processo in padre e figlio: al figlio restituisce sempre 0; al padre restituisce il pid 0 del figlio biforcato; restituisce 1 se la biforcazione del processo fallisce • (restituisce solo al padre, ovviamente, visto che il figlio non è stato biforcato). • ***Fig. 3 Dispensa Negri***

Tutti i segmenti del padre sono duplicati nel figlio, quindi sia il codice e

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). La duplicazione del segmento di sistema prodotto dalla fork copia la tabella dei file aperti e pertanto entrambi i processi possono operare sullo stesso file aperto, mentre successivi file aperti da uno dei due processi dopo la fork saranno invece immessi nella tabella dei file aperti del solo processo che ha eseguito l’apertura. 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.

 • void exit (int stato) • esempio: exit() – (per il momento senza

• void exit (int stato) • esempio: exit() – (per il momento senza parametro == exit(0)) – Termina il processo corrente – Simile alla return: può essere superflua, e. g. se il programma giunge alla fine del codice

Primo esempio: il programma fork 1 #include <stdio. h> #include <sys/types. h> void main(

Primo esempio: il programma fork 1 #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 figlion"); exit( ); } else {printf("sono il processo padren"); exit( ); /* non necessaria */ } • • • I “processi procedono” in modo asincrono Condividono le risorse (e. g. , il terminale) Quindi l’ordine di esecuzione – e stampa dei risultati- non è prevedibile!

Primo esempio: risultato dell’esecuzione di fork 1 NB: ma le due scritte potrebbero anche

Primo esempio: risultato dell’esecuzione di fork 1 NB: ma le due scritte potrebbero anche essere in ordine inverso

 • pid_t getpid () – Restituisce al processo che la chiama il valore

• pid_t getpid () – Restituisce al processo che la chiama il valore del suo pid

Secondo esempio: il programma forkpid 1 • • • • • #include <stdio. h>

Secondo esempio: il programma forkpid 1 • • • • • #include <stdio. h> #include <sys/types. h> void main( ) { pid_t pid, miopid; • • • Un processo può creare n figli I figli possono creare figli di figli (nipoti) ecc. Il primo processo (radice): init, creato dal S. O. pid=fork( ); if (pid==0) {miopid=getpid( ); printf("sono il processo figlio con pid: %inn", miopid); exit( ); } else {printf("sono il processo padren"); printf("ho creato un processo con pid: %in", pid); miopid=getpid( ); printf("il mio pid e' invece: %inn", miopid); exit( ); /* non necessaria */ } }

Secondo esempio: risultato dell’esecuzione di forkpid 1

Secondo esempio: risultato dell’esecuzione di forkpid 1

Terzo esempio: il programma forkpid 2 #include <stdio. h> #include <sys/types. h> void main(

Terzo esempio: il programma forkpid 2 #include <stdio. h> #include <sys/types. h> void main( ) { pid_t pid, miopid; pid=fork( ); if (pid==0) {miopid=getpid( ); printf("1)sono il primo processo figlio con pid: %in", miopid); exit( ); } else {printf("2)sono il processo padren"); printf("3)ho creato un processo con pid: %in", pid); miopid=getpid( ); printf("4)il mio pid e' invece: %in", miopid); pid=fork( ); if (pid==0) {miopid=getpid( ); printf("5)sono il secondo processo figlio con pid: %in", miopid); exit; } else printf("6)sono il processo padren"); printf("7)ho creato un secondo processo con pid: %in", pid); exit( ); /* non necessaria */ } } }

Terzo esempio: (un possibile) risultato dell’esecuzione di forkpid 2

Terzo esempio: (un possibile) risultato dell’esecuzione di forkpid 2

Che accade se un padre termina prima del(i) figli(o)? La convenzione adottata da Linux

Che accade se un padre termina prima del(i) figli(o)? La convenzione adottata da Linux è 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. init … Processo 1 Processo 2 … login shell Processo 3 Processo 4 login Processo 2 Processo 3 Processo 4

Sincronizzare processi asincroni • Elemento fondamentale della gestione del parallelismo: processi indipendenti fino a

Sincronizzare processi asincroni • Elemento fondamentale della gestione del parallelismo: processi indipendenti fino a … • pid_t wait (int *) – Sospende l’esecuzione del processo che la esegue e attende la terminazione di un – qualsiasi- processo figlio; – se un figlio è già terminato la wait del padre si sblocca immediatamente (nessun effetto) – ritorna il pid del processo figlio terminato

 • Esempio: – pid_t pid; – int stato; – pid = wait (&stato);

• Esempio: – pid_t pid; – int stato; – pid = wait (&stato); • stato, parametro passato per indirizzo: – codice di terminazione del processo – 8 bit superiori: possono essere assegnati esplicitamente come parametro di exit; – altri bit di stato assegnati dal S. O. per indicare condizioni di terminazione (e. g. , errore)

 • exit con parametro • void exit (int stato) • Esempio: exit(5) –

• exit con parametro • void exit (int stato) • Esempio: exit(5) – termina il processo e restituisce il valore 5 al padre; – se il padre è già terminato lo stato viene restituito all’interprete comandi; – dettaglio importante: • il valore restituito è costituito dagli 8 bit superiori di stato lo stato ricevuto da wait è il parametro di exit moltiplicato per 256

Quarto esempio: il programma forkwait 1 #include <stdio. h> #include <sys/types. h> void main(

Quarto esempio: il programma forkwait 1 #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 nn"); stato_exit=5; exit(stato_exit); } else { printf("ho creato un processo figlio nn"); pid=wait (&stato_wait); printf("terminato il processo figlio n"); printf("il pid del figlio e' %i, lo stato e' %in", pid, stato_wait/256); } } • NB: per stampare correttamente il valore di stato è necessario dividere il parametro ricevuto in stato_wait per 256

Quarto esempio: risultato dell’esecuzione di forkwait 1

Quarto esempio: risultato dell’esecuzione di forkwait 1

Però attenzione: Caso 1 Processo padre Processo figlio fork() printf("ho creato…"); wait(. . )

Però attenzione: Caso 1 Processo padre Processo figlio fork() printf("ho creato…"); wait(. . ) miopid=getpid( ); … … exit(stato_exit); printf("terminato…");

Caso 2 Processo padre fork() printf("ho creato…"); Processo figlio miopid=getpid( ); … … exit(stato_exit);

Caso 2 Processo padre fork() printf("ho creato…"); Processo figlio miopid=getpid( ); … … exit(stato_exit); wait(. . ) printf("terminato…"); Se il figlio è già terminato la wait del padre si sblocca immediatamente 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”. Il processo figlio dopo l’exit rimane quindi in vita, ma solo per aspettare che il processo padre possa recuperare lo stato.

Riassumendo: a) Un processo padre che non ha generato processi figli esegue una wait.

Riassumendo: 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. Si cominciano a intravvedere le difficoltà di una programmazione non più Orientata alla costruzione di un singolo algoritmo sequenziale –per quanto complesso – per la soluzione di un singolo problema!

Una variante di wait: la funzione waitpid • pid_t waitpid (pid_t pid, int stato,

Una variante di wait: la funzione waitpid • pid_t waitpid (pid_t pid, int stato, int opzioni) • Esempio: waitpid (10, &stato, opzioni) • Mette un processo in stato di attesa dell’evento di terminazione di un processo figlio con pid “pid” (10 in questo caso) e ne restituisce il pid (10 in questo caso); la variabile “stato” assume il valore di “exit” del processo figlio terminato. Il parametro “opzioni” specializza la funzione “waitpid”.

Sostituzione del programma in esecuzione • exec – sostituisce i segmenti codice e dati

Sostituzione del programma in esecuzione • exec – sostituisce i segmenti codice e dati • (utente; non il segmento di sistema! i file aperti rimangono aperti) del processo in esecuzione con codice e dati di un programma contenuto in un file eseguibile specificato; - il processo rimane lo stesso (stesso pid): programma processo! - può passare parametri al nuovo programma (main è una particolare funzione: main (!!)) - esistono diverse varianti di exec

 • exec 1 (char *nome_programma, char *arg 0, char *arg 1, …, NULL);

• exec 1 (char *nome_programma, char *arg 0, char *arg 1, …, NULL); – nome_programma: stringa che identifica completamente (pathname) il file eseguibile contenente il programma da lanciare – arg 0, arg 1, …: puntatori a stringhe da passare come parametri al main da lanciare; l’ultimo è NULL perché il numero di arg è variabile. – infatti: • Il main, che è una particolare funzione, può avere a sua volta dei parametri! finalmente main ()

 • void main (int argc, char *argv[]) – argc: numero di parametri ricevuti

• void main (int argc, char *argv[]) – argc: numero di parametri ricevuti – argv[]: vettore di puntatori a stringhe; • ogni stringa è un parametro • argv[0] contiene sempre il nome del programma • exec 1 provoca quindi l’esecuzione del (“chiama” il) programma il cui eseguibile si trova nel file nome_programma e gli passa come parametri (per indirizzo: sono puntatori) arg 0, arg 1, …)

Quinto esempio: il programma main 1 #include <stdio. h> void main (int argc, char

Quinto esempio: il programma main 1 #include <stdio. h> void main (int argc, char *argv[ ] ) { int i; printf("nsono il programma main 1n"); printf("ho ricevuto %i parametrin", argc); for (i=0; i<argc; i++) printf("il parametro %i è: %sn", i, argv[i]); }

Quinto esempio: risultato dell’esecuzione di main 1 da riga di comando, senza parametri

Quinto esempio: risultato dell’esecuzione di main 1 da riga di comando, senza parametri

Quinto esempio: risultato dell’esecuzione di main 1 da riga di comando, con 3 parametri

Quinto esempio: risultato dell’esecuzione di main 1 da riga di comando, con 3 parametri

Quinto esempio: il programma exec 1 #include <stdio. h> #include <sys/types. h> void main(

Quinto esempio: il programma exec 1 #include <stdio. h> #include <sys/types. h> void main( ) { char P 0[ ]="main 1"; char P 1[ ]="parametro 1"; char P 2[ ]="parametro 2"; printf("sono il programma exec 1n"); exec 1("/home/pelagatt/esempi/main 1", P 0, P 1, P 2, NULL); printf("errore di exec"); } /*normalmente non si arriva qui!*/

Quinto esempio: risultato dell’esecuzione di exec 1

Quinto esempio: risultato dell’esecuzione di exec 1

 • Altre versioni di exec – execv: sostituisce alla lista di stringhe di

• Altre versioni di exec – execv: sostituisce alla lista di stringhe di exec 1 un puntatore a un vettore di stringhe char argv – execlp e execvp permettono di sostituire il pathname completo con il solo nome del file nel direttorio di default – execle e execve hanno un parametro in più che specifica l’ambiente di esecuzione del processo.

exec e fork in combinazione • il padre crea uno o più figli e

exec e fork in combinazione • il padre crea uno o più figli e assegna loro un compito • attende i loro risultati • quando hanno finito e prodotto i risultati li raccoglie e li gestisce – interprete comandi

Sesto esempio: il programma forkexec 1 #include <stdio. h> #include <sys/types. h> void main(

Sesto esempio: il programma forkexec 1 #include <stdio. h> #include <sys/types. h> void main( ) { pid_t pid; int stato_wait; char P 0[ ]="main 1"; char P 1[ ]="parametro 1"; char P 2[ ]="parametro 2"; pid=fork( ); if (pid==0) { printf("nsono il processo figlio n"); printf("lancio in esecuzione il programma main 1n"); exec 1("/home/pelagatt/esempi/main 1", P 0, P 1, P 2, NULL); printf("errore di exec"); /*normalmente non si arriva qui!*/ exit( ); } else { wait(&stato_wait ); printf("nsono il processo padren"); printf("il processo figlio è terminaton"); exit( ); } }

Sesto esempio: risultato dell’esecuzione di forkexec 1

Sesto esempio: risultato dell’esecuzione di forkexec 1

Settimo esempio • Pseudocodice di un interprete comandi semplificato (programma simpleshell) che legge da

Settimo esempio • 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.

Settimo esempio • • #include <stdio. h> #include <sys/types. h> #define “logout” #define prompt

Settimo esempio • • #include <stdio. h> #include <sys/types. h> #define “logout” #define prompt “simpleshell: ” void main( ) { pid_t pid; int stato_wait; …. while (! logout dell’utente) { printf (“%s”, prompt); [ lettura riga di comando e identificazione componenti del comando ] pid=fork( ); if (pid==0) { exec 1(comando, arg 0, arg 1, … argn, NULL); printf("errore di exec"); /*normalmente non si arriva qui!*/ exit( ); } else wait(&stato_wait ); } exit( ); } a) il programma simpleshell

Settimo esempio • • • simpleshell: . /main 1 sono il programma main 1

Settimo esempio • • • simpleshell: . /main 1 sono il programma main 1 ho ricevuto 1 parametri il parametro 0 è: . /main 1 simpleshell: b)risultato dell’esecuzione di simpleshell senza parametri

Settimo esempio • • simpleshell: . /main 1 par 2 par 3 sono il

Settimo esempio • • simpleshell: . /main 1 par 2 par 3 sono il programma main 1 ho ricevuto 4 parametri il parametro 0 è: . /main 1 il parametro 1 è: par 1 il parametro 2 è: par 2 il parametro 3 è: par 3 simpleshell: c)risultato dell’esecuzione di simpleshell con tre parametri

Ottavo esempio • Si devono riempire tabelle • da parte di 3 processi: –

Ottavo esempio • Si devono riempire tabelle • da parte di 3 processi: – un padre e due figli (più precisamente, un figlio e un nipote) • ogni tabella deve indicare: – il valore di variabili i, j, k, pid 1, pid 2 – in specifici punti (linee di codice) dell’esecuzione del programma eseguito dal processo; – Se nel momento indicato la variabile non esiste (perché non esiste il processo) la tabella deve riportare NE, – se la variabile esiste ma non se ne conosce il valore con certezza (perché non si sa a che punto si sia dell’esecuzione del singolo processo) la tabella deve riportare U; • Si suppone che tutte le fork abbiano successo e che il S. O. assegni ai figli creati i valori di pid a partire da 500.

01: main() 02: { 03: int i, j, k, stato; 04: pid_t pid 1,

01: main() 02: { 03: int i, j, k, stato; 04: pid_t pid 1, pid 2; 05: i=10; j=20; k=30; 06: pid 1 = fork(); /*creazione del primo figlio / 07: if (pid 1 == 0) { 08: j=j+1; 09: pid 2 = fork(); /*creazione del secondo figlio */ 10: if (pid 2 == 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: }

Struttura delle 3 tabelle da compilare Valore delle variabili Istante Dopo l’istruzione 6 dopo

Struttura delle 3 tabelle da compilare Valore delle variabili Istante Dopo l’istruzione 6 dopo l’istruzione 9 dopo l’istruzione 11 dopo l’istruzione 19 pid 1 pid 2 i j k

Valore delle variabili nel processo padre Valore delle variabili Istante pid 1 pid 2

Valore delle variabili nel processo padre Valore delle variabili Istante pid 1 pid 2 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 * I = U perché il padre non esegue queste istruzioni e non si sa se quando un figlio è alla 9 o 11 nel frattempo il padre abbia eseguito la 18

Valore delle variabili nel primo processo figlio Valore delle variabili Istante pid 1 pid

Valore delle variabili nel primo processo figlio Valore delle variabili Istante pid 1 pid 2 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

Valore delle variabili nel secondo processo figlio (nipote) Valore delle variabili Istante pid 1

Valore delle variabili nel secondo processo figlio (nipote) Valore delle variabili Istante pid 1 pid 2 i j k Dopo l’istruzione 6 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

Esempio conclusivo • A: – “Sistema” che sceglie la soluzione (algoritmo) più rapida per

Esempio conclusivo • A: – “Sistema” che sceglie la soluzione (algoritmo) più rapida per un problema • B: – “Sistema” che scompone un problema in due sottoproblemi da risolvere separatamente ma indipendentemente e ne combina le soluzioni

A #include … void main () { pid_t int file pid; stato_wait, scelta; *filedati,

A #include … void main () { pid_t int file pid; stato_wait, scelta; *filedati, *filerisultati, *filedati 1, *filedati 2, *filerisultati 1, *filerisultati 2; [apre il file di dati in *filedati; copia *filedati in *filedati 1 e *filedati 2]; scelta = 1; pid = fork(); if (pid !== 0) {scelta = 2; pid = fork() }; if (pid == 0) { if (scelta == 1) {exec 1 (“programma 1”, NULL); exit (1) } if (scelta == 2) {exec 1 (“programma 2”, NULL); exit (2) } } else { pid = wait(&stato_wait); if (stato_wait/256 == 1) { [copia filerisultati 1 in filerisultati]; pintf(“il primo programma a fornirmi i risutati è stato programma 1; n esso è stato eseguito dal processo %in”, pid) } else { [copia filerisultati 2 in filerisultati]; pintf(“il primo programma a fornirmi i risutati è stato programma 2; n esso è stato eseguito dal processo %in”, pid) } }; [chiude tutti i file] }

 • NB – programma 1 risolve lo stesso problema di programma 2 usando

• NB – programma 1 risolve lo stesso problema di programma 2 usando un diverso algoritmo – programma 1 e programma 2 ricevono rispettivamente i dati su cui operare nel file (puntato da) filedati 1 e filedati 2 lasciano il risultato in filerisultati/1/2. – Non è detto che il primo processo a terminare sia quello che esegue l’algoritmo più veloce.

B #include … void main () { pid_t int file pid; stato_wait; *filedati, *filerisultati,

B #include … void main () { pid_t int file pid; stato_wait; *filedati, *filerisultati, *fd 1, *fd 2, *fris 1, *fris 2; [apre –o crea-i vari file; da *filedati prepara i file *fd 1, *fd 2 da far elaborare a programma 1 e programma 2 rispettivamente]; scelta = 1; pid = fork(); if (pid !== 0) {scelta = 2; pid = fork()}; if (pid == 0) { if (scelta == 1) {exec 1 (“programma 1”, NULL); exit (1)} if (scelta == 2) {exec 1 (“programma 2”, NULL); exit (2)} } else { /* ilpadre attende la terminazione dei due programmi figli indipendentemente da quale dei due termina prima; se è “curioso”, oppure se nel frattempo ha assegnato altri compiti ad altri processi …*/ pid = wait(&stato_wait); [costruisce il rilsultato finale in *filerisultati, combinando le soluzioni lasciate dai due programmi rispettivamente in *fris 1 e *fris 2]; } [chiude tutti i file] }

 • NB – Ovviamente da un parallelismo puramente logico di questo tipo non

• NB – Ovviamente da un parallelismo puramente logico di questo tipo non ci si possono attendere grandi risultati pratici in termini di efficienza, anzi … – però il parallelismo logico diventa tanto più utile quanto più “indipendenti” diventano i singoli compiti dei vari processi – Quando poi il parallelismo logico diventa anche fisico (coprocessori) …