Nicola Gessa 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 Gestione dei processi Domanda: Come definire un processo?
Nicola Gessa 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 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 Stato di avanzamento dei processi IN ATTESA IN ESECUZIONE PRONTO PRERILASCIO SOSPENSIONE RIATTIVAZIONE ASSEGNAZIONE
Nicola Gessa 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 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 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 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 Tabella dei processi
Nicola Gessa 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 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 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 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 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 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 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 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 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 Environment di un processo Esempio di struttura di variabili d’ambiente
Nicola Gessa 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 Esempio con la fork #include 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 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 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 Risultato dell’esempio precedente La funzione write NON è 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 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 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 Esempio con atexit #include 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!”); 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 secondo exit handler
Nicola Gessa 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 core dump (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 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 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 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 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 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 Esempio di creazione di zombie Cosa succede se il padre non ha verifica la terminazione del figlio? #include int main(void){ pid_tpid; if ( (pid = fork()) < 0)err_sys("fork error"); else if (pid == 0)/* figlio */exit(0); /* padre */ sleep(4);system("ps"); exit(0); }
Nicola Gessa Output dell’esempio precedente $a.out PID TTY TIME CMD 3150 pts/1 00:00:00 bash pts/1 00:00:00 a.out pts/1 00:00:00 a.out pts/1 00:00:00 ps PID TTY TIME CMD 3150 pts/1 00:00:00 bash pts/1 00:00:00 a.out pts/1 00:00:00 a.out 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 Esempio con wait() - 1 #include int main(void){ pid_tpid; intstatus; 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 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 Output dell’esercizio precedente $ a.out normale, stato = 7 anormale, stato = 6 anormale, stato = 8
Nicola Gessa Esempio con waitpid(2) #include main(void) {pid_tpid; 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 Output dell’esempio precedente (2) $a.out muore il processo1 $ secondo figlio, parent pid = 1; muore il processo 3 Processo 1 Processo 2 Processo 3 printf Exit() Aspetta 2 secondi printf
Nicola Gessa 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 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 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 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 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 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 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 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 Esempio con la execle #include 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 Risultato dell’esempio precedente $ a.out argv[0]:echoall argv[1]:arg1 argv[2]:arg2 USER=unknown PATH=/tmp argv[0]:echoall $ argv[1]:newarg USER=stevens HOME=/home/myhome ….. EDITOR=/usr/vi
Nicola Gessa Ancora sull’esempio con la execle #include 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); } Manca qualcosa?
Nicola Gessa Ancora sull’esempio con la execle #include 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); } NO! Sarebbe inutile inserire il comando di exit poiché la dalla funzione exec(a meno di errori nella sua esecuzione), non ritorna mai
Nicola Gessa Esecuzione dei programmi CODICE DELLA SHELL CODICE DEL PROGRAMMA PROCESSO CHE ESEGUE LA SHELL PROCESSO CHE ESEGUE IL PROGRAMMA Duplicazione Terminazione Caricamento PROGRAMMI PROCESSI Caricamento
Nicola Gessa Esempio di programma shell-like Come esempio sul funzionamento della exec vediamo un programma che riprende la struttura base di una shell. I punti fondamentali sono: Si usa la funzione gets per leggere dentro ad un ciclo infinito una linea per volta dallo standard input. L’inserimento come primo carattere di un NULL fa terminare l’esecuzione. Calcoliamo la lunghezza della stringa e sostituiamo l’ultimo carattere di newline con un byte null per poter passare questa stringa come argomento della successiva funzione execlp Chiamiamo la funzione fork per creare un nuovo processo che sarà la copia del padre.
Nicola Gessa Esempio di programma shell-like Nel processo figlio chiamiamo la funzione execpl per eseguire il comando inserito dall’utente dallo standard input. Rimpiazziamo quindi il processo figlio con il nuovo programma. Il processo padre rimane in attesa della terminazione del processo figlio chiamando la funzione waitpid. In questo caso non riusciamo a passare argomenti al comando eseguito. Per far questo dovremmo parserizzare l’input dell’utente e separare gli argomenti contenuti nella stringa per poterli poi passare alla funzione execpl.
Nicola Gessa Esempio di programma shell-like int main(void){ char buf[MAXLINE]; pid_t pid; int status; print (“%”); while(fgets(buf,MAXLINE,stdin)!=NULL){ buf[strlen(buf)-1]=0; if((pid=fork())< 0) err_sys(“errore nella fork”); else if (pid==0){ if(execlp(buf,buf,(char *) 0)<0) errsys(“errore nella execlp”); } if((pid=waitpid(pid,&status,0))<0) err_sys(“errore nella wait”); print (“%”); } exit(0); }
Nicola Gessa Risultato $a.out %datecomandi letti dalla mia shell Fri Jun 10 10:30:23 MST 2002 %pwd /home/students/rossi %ls a.out………………. % ^D $
Nicola Gessa Schema del ciclo di vita di un processo User function C start-up routine Main function Exit function Exit handler exit call return _exit kernel exec _exit Standard I/O cleanup User process call return …………...
Nicola Gessa Tempo di esecuzione di un processo Unix registra 3 valori relativi al tempo di esecuzione di un processo: clock time: rappresenta il tempo che un processo ha impiegato per essere eseguito e il suo valore dipende dal numero di processi totali che sono in esecuzione user CPU time: è il tempo di CPU che stato impiegato dalle istruzioni utente system CPU time: è il tempo di CPU impiegato alle esecuzioni delle istruzioni eseguite dal kernel per conto dell’utente La somma di user CPU time e system CPU time è detta CPU time Questi valori sono ottenuti con la funzione time
Nicola Gessa La funzione system Può essere conveniente eseguire un comando dall’interno di un programma: la funzione system fornisce l’interfaccia per questa operazione. int system(const char *cmdstring) Il parametro cmdstring specifica il comando da eseguire. La funzione system è implementata chiamando le funzioni fork, exec e waitpid. Il valore di ritorno dipende da quale di queste funzioni fallisce. Il vantaggio di usare la funzione system invece delle tre funzioni risiede nel fatto che questa funzione gestisce sia gli errori che i segnali richiesti.
Nicola Gessa Esempio con la funzione system #include #include"ourhdr.h" int main(void){ intstatus; if ( (status = system("date")) < 0) err_sys("system() error"); pr_exit(status); if ( (status = system("nosuchcommand")) < 0) err_sys("system() error"); pr_exit(status); if ( (status = system("who; exit 44")) < 0) err_sys("system() error"); pr_exit(status); exit(0); }
Nicola Gessa Risultato dell’esempio precedente $a.out Thu Aug MST 2001 normal termination, exit status = 0 sh: nosuchcommand: not found normal termination, exit status = 1 stevens console Aug stevens ttyp0Aug 29 05:23 stevens ttyp1Aug 29 05:24 stevens ttyp2Aug 29 05:25 normal termination, exit status = 44