1 / 63

In questa sezione

In questa sezione. Come funziona la gestione dei processi in UNIX e quali interfacce UNIX fornisce per poter operare con i processi Esempi in C sul funzionamento delle più importati system call per la gestione dei processi. Gestione dei processi. Domanda: Come definire un processo?.

jereni
Download Presentation

In questa sezione

An Image/Link below is provided (as is) to download presentation Download Policy: Content on the Website is provided to you AS IS for your information and personal use and may not be sold / licensed / shared on other websites without getting consent from its author. Content is provided to you AS IS for your information and personal use only. Download presentation by click this link. While downloading, if for some reason you are not able to download a presentation, the publisher may have deleted the file from their server. During download, if you can't get a presentation, the file might be deleted by the publisher.

E N D

Presentation Transcript


  1. In questa sezione • Come funziona la gestione dei processi in UNIX e quali interfacce UNIX fornisce per poter operare con i processi • Esempi in C sul funzionamento delle più importati system call per la gestione dei processi. Nicola Gessa

  2. Gestione dei processi Domanda: Come definire un processo? Nicola Gessa

  3. Gestione dei processi Come definire un processo? Un processo è un’istanza in esecuzione di un programma. La gestione dei processi include: • creazione del processo. • esecuzione del processo e controllo del suo ciclo di vita. • terminazione del processo. • Il comando ps -A visualizza una lista di tutti i processi e dei loro PID al momento in esecuzione. • Il comando pstree mostra i processi in esecuzione secondo la loro struttura ad albero Nicola Gessa

  4. Esempio di lista dei processi PID TTY TIME CMD 1 ? 00:00:04 init 2 ? 00:00:00 keventd 3 ? 00:00:00 kapmd 4 ? 00:00:00 ksoftirqd_CPU0 9 ? 00:00:00 bdflush 5 ? 00:00:00 kswapd 6 ? 00:00:00 kscand/DMA 7 ? 00:00:00 kscand/Normal 8 ? 00:00:00 kscand/HighMem 10 ? 00:00:00 kupdated 641 pts/0 00:00:00 bash 2671 pts/1 00:00:00 bash 2710 ? 00:00:07 gedit 2883 ? 00:00:00 cupsd 3117 pts/1 00:00:00 ps Nicola Gessa

  5. Stato di avanzamento dei processi SOSPENSIONE IN ESECUZIONE PRERILASCIO ASSEGNAZIONE IN ATTESA PRONTO RIATTIVAZIONE Nicola Gessa

  6. Quali funzioni per gestire i processi? L'impiego di funzioni (primitive di controllo di processo) per: • Creare nuovi processi (fork, vfork) ; • Attivare nuovi programmi (famiglia exec); • Attendere la terminazione di un processo (wait, waitpid); • Terminare un processo (exit, _exit,return). Nicola Gessa

  7. Identificatore di processi • Ogni processo ha assegnato un unico ID che lo identifica, un intero non negativo. • Sull’ID del processo vengono poi costruiti altri identificativi che devono essere unici (come ad esempio nomi di file temporanei). • Solitamente il processo 0 è lo scheduler, che fa parte del kernel del SO. • Il processo con ID 1 è di solito il processo INIT invocato alla fine della fase di bootstrap. Questo processo non termina mai. • Il processo con ID 2 è il pagedaemon, che si occupa della gestione della memoria virtuale. Nicola Gessa

  8. Identificatore di processi • Il pid viene assegnato in forma progressiva ogni volta che un nuovo processo viene creato, fino ad un certo limite. Oltre questo valore l'assegnazione riparte dal numero più basso disponibile a partire da un minimo fissato, che serve a riservare i pid più bassi ai processi eseguiti dal direttamente dal kernel. • Tutti i processi inoltre memorizzano anche il pid del genitore da cui sono stati creati, questo viene chiamato in genere ppid (da parent process id). Questi due identificativi possono essere ottenuti da programma usando le funzioni: pid_t getpid(void) restituisce il pid del processo corrente. pid_t getppid(void) restituisce il pid del padre del processo corrente. Nicola Gessa

  9. Tabella dei processi • Il kernel mantiene una tabella dei processi attivi, la cosiddetta process table ; per ciascun processo viene mantenuta una voce nella tabella dei processi costituita da una struttura task_struct , che contiene tutte le informazioni rilevanti per quel processo. Tutte le strutture usate a questo scopo sono dichiarate in un header file (es: linux/sched.h) Nicola Gessa

  10. Tabella dei processi Nicola Gessa

  11. Alcune definizione per la tabella… • Stato di un processo: #define TASK_RUNNING 0 #define TASK_INTERRUPTIBLE 1 …… • Politiche di scheduling #define SCHED_OTHER 0 #define SCHED_FIFO 1 #define SCHED_RR 2 • Time slice #define DEF_PRIORITY (20*HZ/100) /* 200 ms time slices */ Nicola Gessa

  12. Tabella dei processi, cosa contiene? La struttura dei processi task_struct contiene informazioni come: • Stato del processo volatile long state; • Tipo di errore generato int errno; • Priorità statica long priority; • Codice di terminazione e segnale che ha causato la terminazione int exit_code, exit_signal; Nicola Gessa

  13. Tabella dei processi, cosa contiene? • Identificatori per il processo: int pid; int pgrp; • Collegamenti ai processi parenti… struct task_struct *p_opptr, *p_pptr, *p_cptr, *p_ysptr, *p_osptr; • Informazioni sul filsystem struct fs_struct *fs; • Informazioni file aperti struct files_struct *files; • Memoria utilizzata dal processo struct mm_struct *mm; Nicola Gessa

  14. Creazione di un processo • L’unico modo per poter creare un processo è tramite la chiamata alla funzione fork. Questo non si applica solo ai processi speciali - scheduler, init e pagedaemon - che vengono creati al momento del bootstrap. pid_t fork(void) • Il processo creato tramite la funzione fork è chiamato child process Nicola Gessa

  15. Creazione di un processo • La funzione fork è chiamata una volta ma, una volta avvenuta la creazione del nuovo processo, ritorna “due volte”, una nel processo padre (il valore restituito è l’ID del figlio) e una nel processo figlio (il valore restituito è 0). • Il padre memorizza l’ID del figlio ricevuto al termine della chiamata alla funzione fork mentre il processo figlio lo puo’ calcolare tramite la funzione getppid(). • Sia il padre che il figlio continuano di seguito l’esecuzione del codice che segue la fork: il figlio è una copia del padre, non condividono la memoria. Nicola Gessa

  16. Cosa avviene nella tabella dei processi? • Alloca memoria per un nuovo task_struct da associare al nuovo processo • Cerca un elemento libero nella tabella dei processi • Copia le informazioni del processo padre nel nuovo processo • Imposta correttamente i puntatori del processo padre e del processo figlio Nicola Gessa

  17. Creazione di un processo • In generale dopo il ritorno dalla funzione fork non è possibile sapere se verrà eseguito prima il padre o il figlio. • I motivi principali per cui la fork può fallire e non può essere quindi creato un nuovo processo nel sistema: • troppi processi attivi nel sistema. • troppi processi attivi per l’utente che la sta eseguendo. Nicola Gessa

  18. Creazione di processo • Usi tipici della creazione di un nuovo processo sono: • quando un processo vuole duplicare se stesso così che il processo padre e il processo figlio possano eseguire differenti sezione del codice: ad esempio nel modello client-server i server possono creare processi nuovi per gestire le richieste mentre il processo principale aspetta di riceverne di nuove. • quando un processo desidera eseguire programmi diversi: è questo il caso delle shell. In questo caso il processo figlio esegue un exec immediatamente dopo il ritorno dalla funzione fork. Nicola Gessa

  19. Environment di un processo • Ogni processo riceve una lista con i parametri di ambiente. • Questa lista è un array di puntatori a carattere che contengono gli indirizzi di una stringa C terminata con un carattere null. La lunghezza stessa dell’array non è fissa: l’array è terminato da un puntatore nullo. • L’indirizzo dell’array di puntatori è memorizzato in una variabile global environ. extern char ** environ • Per convenzione le stringhe rappresentano delle coppie name=value. • L’accesso alle variabili d’ambiente è possibile anche tramite le funzioni getenv and putenv. Nicola Gessa

  20. Environment di un processo Esempio di struttura di variabili d’ambiente Nicola Gessa

  21. Environment di un processo • L’uso delle variabili d’ambiente è riservata alle applicazioni e ad alcune funzioni di libreria; in genere esse costituiscono un modo comodo per definire un comportamento specifico senza dover ricorrere all'uso di opzioni a linea di comando o di file di configurazione. • La shell, ad esempio, ne usa molte per il suo funzionamento (come PATH per la ricerca dei comandi) e alcune di esse (come HOME , USER , etc.) sono definite al login. In genere, è cura dell'amministratore definire le opportune variabili di ambiente in uno script di avvio. Alcune servono poi come riferimento generico per molti programmi (come EDITOR, che indica l'editor preferito da invocare in caso di necessità). Nicola Gessa

  22. Esempio con la fork #include <sys/types> int glob=6; char buf[]=“stampa su stdout\n”; int main(void){ int var; pid_t pid; var = 88; if(write(STDOUT_FILENO,buf,sizeof(buf)-1)!= sizeof(buf)-1) err_sys(“errore nelle stampa!”); printf(“prima della fork\n”); if((pid=fork())< 0) err_sys(“errore nella fork”); else if (pid==0){ glob++; var++; //il figlio modifica le variabili }else sleep(2); printf(“pid=%d,glod=%d,var=%d\n”,getpid(), glob, var); exit(0); } Nicola Gessa

  23. Output dell’esempio precedente $a.out stampa su stdout prima della fork pid = 430, glob = 7, var = 89 //esecuzione del figlio pid = 429, glob = 6, var = 88 //esecuzione del padre //le variabili non sono modificate $a.out > temp.out $cat temp.out stampa su stdout prima della fork pid = 433, glob = 7, var = 89 prima della fork pid = 432, glob = 6, var = 88 Nicola Gessa

  24. Output dell’esempio precedente $a.out stampa su stdout prima della fork pid = 430, glob = 7, var = 89 //esecuzione del figlio pid = 429, glob = 6, var = 88 //esecuzione del padre //le variabili non sono modificate $a.out > temp.out $cat temp.out stampa su stdout prima della fork pid = 433, glob = 7, var = 89 prima della fork pid = 432, glob = 6, var = 88 ? Nicola Gessa

  25. Risultato dell’esempio precedente • La funzione writeNON è bufferizzata, così la prima stampa è eseguita solo una volta. • La funzione printf invece è bufferizzata quindi • eseguendo il programma in maniera interattiva si ottiene una copia della stampa “prima della fork” perchè il carattere di newline “\n” esegue il flush del buffer di standard output • ridirigendo lo standard output del programma verso un file si ottengono due copie della stampa “prima della fork” perché in questo caso la stringa rimane nel buffer anche alla chiamata della fork. Il buffer è copiato nel processo figlio e in questo buffer è inserita la stringa stampata nella seconda printf. Il buffer è quindi stampato quando il processo termina e ne viene fatto il flush. Nicola Gessa

  26. Terminazione di un processo • Ci sono tre metodi per la terminazione normale del processo: • ritornando dalla funzione principale main eseguendo la funzione return. • chiamando la funzione exit. Questa funzione è definita nello standard ANSI C e comporta la chiamata di tutti gestori della exit che sono stati registrati con la funzione atexit e la chiusura di tutti gli stream di I/O. • chiamando la funzione _exit. Il processo termina immediatamente senza eseguire nessun tipo di gestione degli stream o chiamata ad altre funzioni. Nicola Gessa

  27. La funzione atexit • E’ possibile registrare delle funzioni che vengono in seguito chiamate automaticamente dalla funzione exit() per la gestione della chiusura del processo. • Questa registrazione è fatto utilizzando la funzione atexit: int atexit(void (*func)(void)); • Questa dichiarazione specifica che deve essere passato l’indirizzo della funzione da richiamare come parametro della atexit(). • La funzione exit() chiama le funzioni che sono state registrate in ordine inverso rispetto alla loro registrazione e tante volte quante sono state registrate Nicola Gessa

  28. Esempio con atexit #include <sys/types> static void my_exit1(void), my_exit2(void) int main(void){ if(atexit(my_exit2) !=0 ) err_sys(“impossibile registrare my_exit2!”); if(atexit(my_exit1) !=0 ) err_sys(“impossibile registrare my_exit1!”); if(atexit(my_exit1) !=0 ) err_sys(“impossibile registrare my_exit1!”); print(“main terminato”); return(0); } static void my_exit1(void){ print(“primo exit handler\n”); } static void my_exit2(void){ print(“secondo exit handler\n”); } RISULTATO $a.out main terminato primo exit handler primo exit handler secondo exit handler Nicola Gessa

  29. Terminazione di un processo • Ci sono due metodi per la terminazione anomala di un processo: • chiamando la funzione abort, che genera un segnale di SIGABRT. • quando un processo riceve un certo segnale. I segnali possono essere generati dallo stesso processo, da altri processi o dal kernel ( ad esempio quando un processo cerca di fare dei riferimenti a zone di memoria che non sono nel suo spazio di memoria) • La terminazione di un processo comporta la chiusura di tutti i suoi descrittori e il rilascio della memoria. • Può darsi che la terminazione (anomala) di un processo provochi il coredump (scarico della memoria). In pratica si ottiene la creazione di un file nella working directory, contenente l’immagine del processo interrotto. Questi file servono soltanto a documentare un incidente di funzionamento ed a permetterne l’analisi attraverso strumenti diagnostici opportuni (debugger). Nicola Gessa

  30. Terminazione di un processo • Il processo che termina deve essere in grado di informare della modalità di terminazione il suo processo padre. Utilizzando le funzioni exit, _exit o return questo è consentito passando alle due funzioni un argomento. In caso di terminazione anomala, il kernel genera uno stato di terminazione che indica i motivi della terminazione anormale del processo. • Se il processo termina chiamando le funzioni di exit o return senza parametro, lo stato di uscita rimane indefinito • Il padre del processo può ottenere lo stato della terminazione utilizzando le funzioni wait e waitpid Nicola Gessa

  31. Terminazione di un processo • Cosa capita quando il processo padre termina prima del figlio? In questo caso il processo init diventa il nuovo padre: il processo figlio è stato ereditato da init. In pratica quando un processo termina viene controllato se questo aveva dei figli, e nel caso ne vengano trovati, l’ID del loro padre diventa 1. • Cosa capita se il processo figlio termina prima del padre? In questo caso il padre potrebbe non essere in grado di ottenere il suo stato di terminazione. Il sistema allora mantiene un certo numero di informazioni sul processo terminato in modo da poterle passare al processo padre quando questi ne fa richiesta ( tramite le funzioni wait o waitpid). Queste informazioni comprendono l’ID del processo, lo stato di terminazione e il tempo di CPU impiegato. Processi terminati il cui padre non ha ancora eseguito le funzioni come wait sono detti zombie. Nicola Gessa

  32. Terminazione dei processi • Quando un processo termina, sia normalmente o in maniera anomala, il padre viene avvisato dal kernel tramite il segnale SIGCHLD. • Questo segnale è la notifica asincrona del kernel della morte del processo figlio. Quindi il processo può definire un gestore di questo segnale per poter gestire la terminazione dei processi figli. Nel caso tale gestore non sia definito il segnale viene ignorato. • Per ottenere informazioni sulla terminazione di un processo figlio si utilizzano le funzioni wait e waitpid: pid_t wait(int *statloc) pid_t waitpid(pid_t pid, int *statloc, int option); Nicola Gessa

  33. Terminazione dei processi • La chiamata alle funzioni wait e waitpid può • bloccare il processo che l’esegue se tutti i suoi figli sono ancora in esecuzione. • ritornare immediatamente restituendo il codice di terminazione del figlio se un figlio ha già terminato e si aspetta che il suo stato di terminazione sia registrato. • ritornare un errore, se il processo non ha nessun figlio. • Le differenze fra le funzioni wait e waitpid sono: • la wait è bloccante mentre la waitpid ha delle opzioni per evitare di bloccare il processo padre. • la waitpid riceve dei parametri per specificare di quale processo aspettare la terminazione. Nicola Gessa

  34. La funzione waitpid pid_t waitpid(pid_t pid, int *statloc, int option); • L’argomento pid_t permette di specificare l’ID del processo di cui si vuole attendere la terminazione. • La funzione ritorna l’ID del processo che ha terminato e nella variabile statloc lo stato di terminazione. • Ritorna errore se si specifica un ID che non appartiene all’insieme dei figli del processo. • Consente una versione non bloccante della funzione wait tramite l’uso dell’argomento option. Nicola Gessa

  35. Esempio di creazione di zombie Cosa succede se il padre non ha verifica la terminazione del figlio? #include <sys/types.h> #include <sys/wait.h> #include <unistd.h> int main(void){ pid_t pid; if ( (pid = fork()) < 0) err_sys("fork error"); else if (pid == 0) /* figlio */ exit(0); /* padre */ sleep(4); system("ps"); sleep(4); system("ps"); exit(0); } Nicola Gessa

  36. Output dell’esempio precedente $a.out PID TTY TIME CMD 3150 pts/1 00:00:00 bash 11448 pts/1 00:00:00 a.out 11449 pts/1 00:00:00 a.out <defunct> 11450 pts/1 00:00:00 ps PID TTY TIME CMD 3150 pts/1 00:00:00 bash 11448 pts/1 00:00:00 a.out 11449 pts/1 00:00:00 a.out <defunct> 11451 pts/1 00:00:00 ps • La vita del processo termina solo quando la notifica della sua conclusione viene ricevuta dal processo padre, a quel punto tutte le risorse allocate nel sistema ad esso associate vengono rilasciate Nicola Gessa

  37. Esempio con wait() - 1 #include <sys/types.h> #include <sys/wait.h> #include <unistd.h> int main(void){ pid_t pid; int status; if ( (pid = fork()) < 0) err_sys("fork error"); else if (pid == 0) /* child */ exit(7); if (wait(&status) != pid) /* wait for child */ err_sys("wait error"); pr_exit(status); /* and print its status */ /* CONTINUA….. */ Nicola Gessa

  38. Esempio con wait() - 2 if ( (pid = fork()) < 0) err_sys("fork error"); else if (pid == 0)/* figlio */abort(); /* generates SIGABRT */ if (wait(&status) != pid)/* wait for child */err_sys("wait error"); pr_exit(status); /* and print its status */ if ( (pid = fork()) < 0)err_sys("fork error"); else if (pid == 0) /* figlio */status /= 0;/* divide by 0 generates SIGFPE */ if (wait(&status) != pid)/* wait for child */err_sys("wait error"); pr_exit(status); /* and print its status */ exit(0); } Nicola Gessa

  39. Output dell’esercizio precedente $ a.out normale, stato = 7 anormale, stato = 6 anormale, stato = 8 Nicola Gessa

  40. Esempio con waitpid(2) #include <sys/types> #include <sys/wait.h> #include <unistd.h> main(void) { pid_t pid; if ( (pid = fork()) < 0) err_sys("fork error"); else if (pid == 0) { /* primo figlio */ if ( (pid = fork()) < 0) err_sys("fork error"); else if (pid > 0) exit(0);/*primo figlio */ sleep(2); printf(“secondo figlio, parent pid = %d\n", getppid()); printf(“muore il processo 3\n”); exit(0); } if (waitpid(pid, NULL, 0) != pid) /* aspetta il primo figlio */ err_sys("waitpid error"); else printf(“muore il processo 1\n”); exit(0); } Nicola Gessa

  41. Output dell’esempio precedente (2) $a.out muore il processo1 $secondo figlio, parent pid = 1; muore il processo 3 Aspetta 2 secondi printf Processo 3 Processo 2 Processo1 Exit() printf Exit() Nicola Gessa

  42. Ordinamento nell’esecuzione dei processi • Poiché non si conosce a priori l’ordine di esecuzione dei processi padre e figli, il risultato potrebbe essere imprevedibile, e inoltre la situazione di errore risultare difficilmente riproducibile in fase di debugging. • Il processo padre può attendere la fine del processo figlio tramite la funzione waitpid. • Il processo figlio potrebbe attendere la fine dell’esecuzione del processo padre ? Nicola Gessa

  43. Ordinamento nell’esecuzione dei processi • Poiché non si conosce a priori l’ordine di esecuzione dei processi padre e figli, il risultato potrebbe essere imprevedibile, e inoltre la situazione di errore risultare difficilmente riproducibile con difficoltà in fase di debugging. • Il processo padre può attendere la fine del processo figlio tramite la funzione waitpid. • Il processo figlio potrebbe attendere la fine dell’esecuzione del processo padre con un codice come while(getppid()!=1) sleep(1); dove si riconosce la fine del padre dal fatto che il PID e quello del processo init. E’ una buona soluzione ?. Nicola Gessa

  44. Ordinamento nell’esecuzione dei processi • Poiché non si conosce a priori l’ordine di esecuzione dei processi padre e figli, il risultato potrebbe essere imprevedibile, e inoltre la situazione di errore risultare difficilmente riproducibile con difficoltà in fase di debugging. • Il processo padre può attendere la fine del processo figlio tramite la funzione waitpid. • Il processo figlio potrebbe attendere la fine dell’esecuzione del processo padre con un codice come while(getppid()!=1) sleep(1); dove si riconosce la fine del padre dal fatto che il PID e quello del processo init.Questa soluzione è dispendiosa e non sempre applicabile (attesa attiva). Nicola Gessa

  45. Ordinamento nell’esecuzione dei processi static void charatatime(char *); /* funzione per la stampa di caratteri uno alla volta*/ int main(void) { pid_t pid; if((pid=fork())< 0) err_sys(“errore nella fork”); else if (pid==0){charatatime(“output dal figlio\n”); }else{charatatime(“output dal padre\n”);} exit(0); } static void charatatime(char *str){ char *ptr; int c; setbuf(stdout,NULL); /* output unbuffered */ for(ptr=str;c = *ptr++;) putc(c,stdout); } Nicola Gessa

  46. Risultato dell’esempio precedente • A seconda delle condizioni di esecuzione, o degli algoritmi di scheduling adottati, i risultati ottenuti con il programma precedente possono essere anche molto diversi: $ a.out output from child output from parent $ a.out oouuttppuutt ffrroomm cphairlednt $ a.out ooutput from child utput from parent Nicola Gessa

  47. La funzione exec • Quando un processo chiama la funzione exec, quel processo inizia l’esecuzione del codice del nuovo programma specificato, e il nuovo programma inizia la sua esecuzione partendo dalla sua funzione main(). • NON viene creato un nuovo processo, quindi l’ID non cambia. • Con la fork quindi si creano nuovi processi, mentre con la exec si avviano nuovi programmi. • Le funzioni exit, wait e waitpid sono usate sempre per gestire la terminazione dei processi. Nicola Gessa

  48. Le funzioni exec • int execl(const char *pathname, const char *arg0,.,/*(char *)0*/); • int execv(const char *pathname, char *const argv[]); • int execle(const char *pathname, const char *arg0,.,/*(char *)0, char *const envp[]*/); • int execve(const char *pathname, char *const argv[], char *const envp[]); • int execlp(const char *filename, const char *arg0,.,/*(char *)0*/); • int execvp(const char *filename, char *const argv[]); ritornano -1 in caso di errore. Nicola Gessa

  49. La funzione exec • La funzione exec prende come parametri anche i parametri da passare al programma da eseguire; tali argomenti possono essere passati come lista oppure come array. • Normalmente un processo consente di propagare il suo ambiente di esecuzione ai processi figli, ma è possibile specificare anche particolari ambienti di esecuzione. • Nel sistema si possono fissare dei limiti alla dimensione degli argomenti e alla lista delle variabili d’ambiente. • Ogni descrittore di file ha associato un flag che consente di forzare la chiusura dei del descrittore del file quando viene eseguita una chiamata ad una exec. Nicola Gessa

  50. Esempio con la execle #include <sys/types> char *env_init[]={“USER=unknown”,PATH=“/tmp”,NULL} int main(void){ pid_t pid; if((pid=fork())< 0) err_sys(“errore nella fork”); else if (pid==0){/* figlio*/ if(execle(“/home/bin/echoall”,”echoall”,”arg1”,”arg2”, (char *) 0,env_init)<0) errsys(“errore nella execle”);} if(waitpid(pid,NULL,0)<0) err_sys(“errore nella wait”); if((pid=fork())< 0) err_sys(“errore nella fork”); else if (pid==0){ if(execlp(“echoall”,”echoall”,”newarg”, (char*)0,env_init)<0) errsys(“errore nella execle”);} exit(0); } Nicola Gessa

More Related