04_controllo_processi_1X PDF - Laboratorio di Sistemi Operativi

Summary

These notes detail topics in operating systems, focusing on process control and Unix-related concepts. They include discussions of process IDs, parent-child relationships, memory layout, and programming aspects using C code examples.

Full Transcript

laboratorio di sistemi operativi il controllo dei processi Daniele Radicioni argomenti del laboratorio UNIX introduzione a UNIX; integrazione C: operatori bitwise, precedenze, preprocessore, pacchettizzazione del codice, compilazione condizionale e utility make; controllo...

laboratorio di sistemi operativi il controllo dei processi Daniele Radicioni argomenti del laboratorio UNIX introduzione a UNIX; integrazione C: operatori bitwise, precedenze, preprocessore, pacchettizzazione del codice, compilazione condizionale e utility make; controllo dei processi; segnali; pipe e fifo; code di messaggi; memoria condivisa; semafori; introduzione alla programmazione bash. Daniele Radicioni - Laboratorio di Sistemi Operativi, turno 1 corso A 2 il materiale di queste lezioni è tratto da: - lucidi del Prof. Gunetti; - Michael Kerrisk, The Linux Programming interface - a Linux and UNIX® System Programming Handbook, No Starch Press, San Francisco, CA, 2010; - W. Richard Stevens, Stephen A. Rago, Advanced Programming in the UNIX® Environment (2nd Edition), Addison-Wesley, 2005; Daniele Radicioni - Laboratorio di Sistemi Operativi, turno 1 corso A 3 Process ID and Parent Process ID each process has a process ID (PID), a positive integer that uniquely identifies the process on the system - process IDs are used and returned by a variety of system calls the getpid() system call returns the process ID of the calling process #include pid_t getpid(void); Always successfully returns process ID of caller Daniele Radicioni - Laboratorio di Sistemi Operativi, turno 1 corso A 4 Process ID and Parent Process ID each process has a parent: the process that created it. a process can find out the process ID of its parent using the getppid() system call the parent process ID attribute of each process represents the tree-like relationship of all processes on the system. the parent of each process has its own parent, and so on, going all the way back to process 1, init, the ancestor of all processes #include pid_t getppid(void); Always successfully returns process ID of parent of caller Daniele Radicioni - Laboratorio di Sistemi Operativi, turno 1 corso A 5 Memory Layout of a Process the memory allocated to each process is composed of a number of parts, usually referred to as segments. - the text segment contains the machine-language instructions of the program run by the process. it is read-only so that a process doesn’t accidentally modify its own instructions via a bad pointer value. sharable so that a single copy of the program code can be mapped into the virtual address space of all of the processes. Daniele Radicioni - Laboratorio di Sistemi Operativi, turno 1 corso A 6 Memory Layout of a Process - the initialized data segment contains global and static variables that are explicitly initialized. The values of these variables are read from the executable file when the program is loaded into memory. - the uninitialized data segment contains global and static variables that are not explicitly initialized. Before starting the program, the system initializes all memory in this segment to 0. the main reason for placing global and static variables that are initialized into a separate segment from those that are uninitialized is that, when a program is stored on disk, it is not necessary to allocate space for the uninitialized data. Daniele Radicioni - Laboratorio di Sistemi Operativi, turno 1 corso A 7 Memory Layout of a Process the stack is a dynamically growing and shrinking segment containing stack frames. - one stack frame is allocated for each currently called function. a frame stores the function’s local variables (so-called automatic variables), arguments, and return value. the heap is an area from which memory (for variables) can be dynamically allocated at run time. Daniele Radicioni - Laboratorio di Sistemi Operativi, turno 1 corso A 8 #include #include char char_buf;// segmento dei dati non inizializzati int numeri_primi[] = { 2, 3, 5, 7 };// dati inizializzati static int square(int x) { // allocato nel frame di square int result; // allocato nel frame di square() result = x * x; return result; // valore di ritorno } static void compute(int val) { // allocato nel frame di compute() printf("il quadrato di %d e` %d\n", val, square(val)); if (val < 1000) { int t; // allocato nel frame di compute() t = val * val * val; printf("il cubo di %d e` %d\n", val, t); } }... Daniele Radicioni - Laboratorio di Sistemi Operativi, turno 1 corso A 9 int main(int argc, char *argv[]) { // nel frame del main() static int key = 1234; // segmento dei dati inizializzati static char mbuf; // segm. dati non inizializzati char *p; // allocato nel frame del main() p = malloc(1024); // allocato nello heap compute(key); exit(EXIT_SUCCESS); } Daniele Radicioni - Laboratorio di Sistemi Operativi, turno 1 corso A 10 Virtual memory address (hexadecimal) /proc/kallsyms Kernel provides addresses of Michael Kerrisk, The Linux Programming interface - a Linux and UNIX® System Programming Handbook, No Starch Press, San (mapped into process kernel symbols in this virtual memory, but not region ( /proc/ksyms in accessible to program) kernel 2.4 and earlier) 0xC0000000 argv, environ Stack (grows downwards) Top of stack (unallocated memory) Program Francisco, CA, 2010. break Heap (grows upwards) increasing virtual addesses &end Uninitialized data (bss) &edata Initialized data &etext Text (program code) 0x08048000 Daniele Radicioni - Laboratorio di Sistemi Operativi, turno 1 corso A 0x00000000 Figure 6-1: Typical memory layout of a process on Linux/x86-32 The upshot of locality of reference is that it is possible to execute a program while controllo dei processi process control con controllo dei processi si indica un insieme di operazioni che include la creazione di nuovi processi, l'esecuzione di processi, e la loro terminazione. a queste operazioni corrispondono le system call fork(), exit(), wait(), and execve(). - a system call is a request for the operating system to do something on behalf of the user's program. - the system calls are functions used in the kernel itself; to the programmer it appears as a normal C function call. Daniele Radicioni - Laboratorio di Sistemi Operativi, turno 1 corso A 13 fork() la syscall fork() permette a un processo, il padre, di crearne un altro detto figlio. - il figlio è (quasi!) una copia esatta del padre: ottiene copie degli stack, data, heap, and text segments del padre. - il termine fork deriva dal fatto che ci si può raffigurare il processo padre come un processo che si suddivide in due. Daniele Radicioni - Laboratorio di Sistemi Operativi, turno 1 corso A 14 exit(status) la funzione di libreria exit(status) termina un processo, rendendo le risorse utilizzate dal processo (memoria, descrittori dei file aperti, etc.) nuovamente disponibili per essere allocate dal kernel. - l'argomento status è un intero che descrive lo stato di terminazione del processo: utilizzando la system call wait() il processo padre può risalire a tale status. Daniele Radicioni - Laboratorio di Sistemi Operativi, turno 1 corso A 15 wait(&status) la system call wait(&status) ha due fini: - se un figlio non ha ancora concluso la propria esecuzione chiamando la exit(), la wait() sospende l'esecuzione del processo chiamante finché uno dei figli non ha terminato la propria esecuzione. - dopo la terminazione del figlio, lo stato di terminazione del figlio è restituito nell'argomento status della wait(). Daniele Radicioni - Laboratorio di Sistemi Operativi, turno 1 corso A 16 execve(pathname, argv, envp) la system call execve(pathname, argv, envp) carica un nuovo programma (pathname, con il relativo argomento list argv, e l'environment envp) nella memoria del processo. il testo del programma precedente è cancellato e stack, dati, e heap sono creati per il nuovo programma. - questa operazione è riferita come "execing" di un nuovo programma. varie funzioni di libreria utilizzano la syscall execve(), della quale ciascuna costituisce una variazione nell'interfaccia. Daniele Radicioni - Laboratorio di Sistemi Operativi, turno 1 corso A 17 Parent process running program “A” Michael Kerrisk, The Linux Programming interface - a Linux and UNIX® System Programming Handbook, No Starch Press, San A Child process fork() running program “A” Memo ry of parent copied to child Parent may perform A other actions here Child may perform further actions here wait(&status) execve(B,...) Francisco, CA, 2010. (optional) (optional) Execution of parent pa Chi ss ld B suspended ed st to atu pa s re nt Execution of program “B” Kernel restarts parent and exit(status) Daniele Radicioni optionally delivers - Laboratorio SIGCHLD di Sistemi Operativi, turno 1 corso A 18 Figure 24-1: Overview of the use of fork(), exit(), wait(), and execve() 24.2 Creating a New Process: fork() creazione di processi Parent process running program “A” Michael Kerrisk, The Linux Programming interface - a Linux and UNIX® System Programming Handbook, No Starch Press, San A Child process fork() running program “A” Memo ry of parent copied to child Parent may perform A other actions here Child may perform further actions here wait(&status) execve(B,...) Francisco, CA, 2010. (optional) (optional) Execution of parent pa Chi ss ld B suspended ed st to atu pa s re nt Execution of program “B” Kernel restarts parent and exit(status) Daniele Radicioni optionally delivers - Laboratorio SIGCHLD di Sistemi Operativi, turno 1 corso A 20 Figure 24-1: Overview of the use of fork(), exit(), wait(), and execve() 24.2 Creating a New Process: fork() fork() la creazione di processi può essere uno strumento utile per suddividere un compito. - per esempio, un server di rete può ascoltare le richieste da parte dei client e creare un nuovo processo figlio per gestire ciascuna richiesta, e nel frattempo continuare a restare in ascolto di ulteriori contatti da parte di altri client. la system call fork() crea un nuovo processo, il figlio, che è una copia quasi esatta del processo chiamante, il padre. Daniele Radicioni - Laboratorio di Sistemi Operativi, turno 1 corso A 21 fork() #include pid_t fork(void); In parent: returns process ID of child on success, or –1 on error; in successfully created child: always returns 0 dopo l'esecuzione della fork(), esistono 2 processi e in ciascuno l'esecuzione riprende dal punto in cui la fork() restituisce. Daniele Radicioni - Laboratorio di Sistemi Operativi, turno 1 corso A 22 padre nello spazio di indirizzamento del processo ------- figlio viene creata una copia delle variabili del ------- padre, col valore loro assegnato al momento int c = 5 ------- della fork. ------- i=fork() figlio il nuovo processo ------- incomincia l’esecuzione a ------- c==5 partire dalla prima ------- ------- istruzione successiva alla fork che lo ha creato. Daniele Radicioni - Laboratorio di Sistemi Operativi, turno 1 corso A 23 fork() i due processi eseguono lo stesso testo, ma mantenendo copie distinte di stack, data, e heap. - stack, dati, e heap del figlio sono inizialmente esatti duplicati delle corrispondenti parti della memoria del padre. dopo la fork(), ogni processo può modificare le variabili in tali segmenti senza influenzare l'altro processo. Daniele Radicioni - Laboratorio di Sistemi Operativi, turno 1 corso A 24 distinguere i processi all'interno del codice di un programma possiamo distinguere i due processi per mezzo del valore restituito dalla fork(). - nell'ambiente del padre, fork() restituisce il process ID del figlio appena creato. è utile perché il padre può creare —e tenere traccia di— vari figli. per attenderne la terminazione può usare la wait() o altra syscall della stessa famiglia. - nell'ambiente del figlio la fork() restituisce 0. se necessario il figlio può ottenere il proprio process ID con la getpid(), e il process ID del padre con la getppid(). Daniele Radicioni - Laboratorio di Sistemi Operativi, turno 1 corso A 25 schema tipico di utilizzo della fork() pid_t procPid; // pid_t è una rinomina del tipo // int: signed integer type; da // usare includendo procPid = fork(); if ( procPid == -1 ) exit(1); if ( procPid ) {// equivale a "if(procPid != 0)"... // - - - - codice del padre } else { // if( procPid == 0 )... // - - - - codice del figlio } Daniele Radicioni - Laboratorio di Sistemi Operativi, turno 1 corso A 26 schema tipico di utilizzo della fork() pid_t procPid; switch (procPid = fork()) { case -1: case 0: default: } Daniele Radicioni - Laboratorio di Sistemi Operativi, turno 1 corso A 27 quale processo sarà eseguito? dopo una fork() è indeterminato quale dei due processi sarà scelto per ottenere la CPU. - in programmi scritti male, questa indeterminatezza può causare errori noti come race conditions. - se abbiamo bisogno di garantire un particolare ordine di esecuzione, è necessario utilizzare una qualche tecnica di sincronizzazione. Daniele Radicioni - Laboratorio di Sistemi Operativi, turno 1 corso A 28... // headers static int idata = 111; // allocata nel segmento dati int main(int argc, char *argv[]) { int istack = 222; // allocata nello stack pid_t procPid; switch (procPid = fork()) { case -1: errExit("fork"); // gestione dell'errore case 0: // - - - - codice del figlio idata *= 3; istack *= 3; break; default: // - - - - codice del genitore sleep(3); // lasciamo che venga eseguito il figlio break; } // entrambi eseguono la printf printf("PID=%ld %s idata=%d istack=%d\n", (long) getpid(), (procPid == 0) ? "(child)" : "(parent)", idata, istack); exit(EXIT_SUCCESS); } Daniele Radicioni - Laboratorio di Sistemi Operativi, turno 1 corso A 29 $./t_fork... // headers PID=28557 (child) idata=333 istack=666 static int idata = 111; // allocata nel segmento dati PID=28556 (parent) idata=111 istack=222 int main(int argc, char *argv[]) { int istack = 222; // allocata nello stack pid_t childPid; switch (procPid = fork()) { case -1: errExit("fork"); case 0: idata *= 3; istack *= 3; break; default: sleep(3); // lasciamo che venga eseguito il figlio break; } // sia il padre sia il figlio eseguono la printf printf("PID=%ld %s idata=%d istack=%d\n", (long) getpid(), (procPid == 0) ? "(child) " : "(parent)", idata, istack); exit(EXIT_SUCCESS); } Daniele Radicioni - Laboratorio di Sistemi Operativi, turno 1 corso A 30 File Sharing Between Parent and Child all'esecuzione della fork(), il figlio riceve duplicati di tutti i descrittori di file del padre. - quindi gli attributi di un file aperto sono condivisi fra genitore e figlio. - se il figlio aggiorna l'offset del file, tale modifica è visibile attraverso il corrispondente descrittore nel padre. Daniele Radicioni - Laboratorio di Sistemi Operativi, turno 1 corso A 31 esercizio scrivere un programma in cui un padre e un figlio condividono un file aperto: il figlio modifica il file e il padre, dopo avere atteso la terminazione del figlio, stampa a video il contenuto del file. Daniele Radicioni - Laboratorio di Sistemi Operativi, turno 1 corso A 32 terminazione di processi Parent process running program “A” Michael Kerrisk, The Linux Programming interface - a Linux and UNIX® System Programming Handbook, No Starch Press, San A Child process fork() running program “A” Memo ry of parent copied to child Parent may perform A other actions here Child may perform further actions here wait(&status) execve(B,...) Francisco, CA, 2010. (optional) (optional) Execution of parent pa Chi ss ld B suspended ed st to atu pa s re nt Execution of program “B” Kernel restarts parent and exit(status) Daniele Radicioni optionally delivers - Laboratorio SIGCHLD di Sistemi Operativi, turno 1 corso A 34 Figure 24-1: Overview of the use of fork(), exit(), wait(), and execve() 24.2 Creating a New Process: fork() terminating a Process a process may terminate in two general ways. - one of these is abnormal termination, caused by the delivery of a signal whose default action is to terminate the process (with or without a core dump). - alternatively, a process can terminate normally, using the _exit() system call. #include void _exit(int status); Daniele Radicioni - Laboratorio di Sistemi Operativi, turno 1 corso A 35 system call _exit() the status argument given to _exit() defines the termination status of the process, which is available to the parent of this process when it calls wait(). although defined as an int, only the bottom 8 bits of status are actually made available to the parent. - by convention, a termination status of 0 indicates that a process completed successfully, and a nonzero status value indicates that the process terminated unsuccessfully. Daniele Radicioni - Laboratorio di Sistemi Operativi, turno 1 corso A 36 la funzione exit() programs generally don’t call _exit() directly, but instead call the exit() library function, which performs various actions before calling _exit(). the following actions are performed by exit(): - exit handlers (functions registered with atexit() and on_exit()) are called; - the stdio stream buffers are flushed. - the _exit() system call is invoked, using the value supplied in status. #include void exit(int status); Daniele Radicioni - Laboratorio di Sistemi Operativi, turno 1 corso A 37 interactions with fork(), stdio Buffers and _exit() int main(int argc, char *argv[]) { printf("Hello world\n"); write(STDOUT_FILENO, "Ciao\n", 5); if (fork() == -1) errExit("fork"); // sia padre sia figlio eseguono la exit exit(EXIT_SUCCESS); } Daniele Radicioni - Laboratorio di Sistemi Operativi, turno 1 corso A 38 esercizio compilare ed eseguire il programma nel lucido precedente provando una prima volta ad eseguirlo con output diretto sul terminale, e redirigendo l'output su file. verificare se vi sono differenze e ragionare sulle possibili cause e soluzioni. - suggerimento: consultare il manuale per printf() e per write(), con 'man -s 2 write' - suggerimento: studiare il funzionamento delle funzioni fflush() e setbuf(). Daniele Radicioni - Laboratorio di Sistemi Operativi, turno 1 corso A 39 monitoraggio dei processi Parent process running program “A” Michael Kerrisk, The Linux Programming interface - a Linux and UNIX® System Programming Handbook, No Starch Press, San A Child process fork() running program “A” Memo ry of parent copied to child Parent may perform A other actions here Child may perform further actions here wait(&status) execve(B,...) Francisco, CA, 2010. (optional) (optional) Execution of parent pa Chi ss ld B suspended ed st to atu pa s re nt Execution of program “B” Kernel restarts parent and exit(status) Daniele Radicioni optionally delivers - Laboratorio SIGCHLD di Sistemi Operativi, turno 1 corso A 41 Figure 24-1: Overview of the use of fork(), exit(), wait(), and execve() 24.2 Creating a New Process: fork() the wait() System Call #include pid_t wait(int *status); Returns process ID of terminated child, or -1 on error in molti casi un processo padre deve essere informato quando uno dei figli cambia stato, quando termina o è bloccato da un segnale. con la system call wait() un processo genitore attende che uno dei processi figli termini e scrive lo stato di terminazione di quel figlio nel buffer puntato da status. Daniele Radicioni - Laboratorio di Sistemi Operativi, turno 1 corso A 42 the wait() System Call 1.se nessun figlio del processo chiamante ha già terminato, la chiamata si blocca finché uno dei figli termina. se un figlio ha già terminato al momento della chiamata, wait() restituisce immediatamente. 2.se status non è NULL, l'informazione sulla terminazione del figlio è assegnata all'intero cui punta status (NB: status è di tipo int *). 3.cosa restituisce wait(): wait() restituisce il process ID del figlio che ha terminato la propria esecuzione. pid_t wait(int *status); Returns process ID of terminated child, or –1 on error Daniele Radicioni - Laboratorio di Sistemi Operativi, turno 1 corso A 43 error in caso di errore, wait() restituisce -1. un possibile errore è che il processo chiamante potrebbe non avere figli, il che è indicato dal valore ECHILD di errno. possiamo utilizzare questo ciclo per attendere la terminazione di tutti i figli di un processo: while ((childPid = wait(NULL)) != -1) continue; if (errno != ECHILD) // errore inatteso errExit("wait"); // gestione errore... Daniele Radicioni - Laboratorio di Sistemi Operativi, turno 1 corso A 44 per conoscere gli errori... #include #include #include #include #include int main(int argc, char** argv) { printf("process with PID %d\n", getpid()); if((wait(NULL)) == -1){ printf("error was %d, \"%s\"\n", errno, strerror(errno)); } exit(0); } Daniele Radicioni - Laboratorio di Sistemi Operativi, turno 1 corso A 45 esercizio scrivere un programma che prende in input un numero variabile di interi i1, i2, …, in - il programma genera n figli: ogni figlio si mette in attesa (usare il comando sleep) il primo per i1 secondi, il secondo per i2 etc., e termina. - il genitore aspetta la terminazione di tutti i propri figli e termina a sua volta. - predisporre un insieme di stampe a video per provare che vengano eseguite tutte le operazioni richieste. Daniele Radicioni - Laboratorio di Sistemi Operativi, turno 1 corso A 46 la System Call waitpid() se un processo padre ha creato vari figli, non è possibile attendere la terminazione di un particolare figlio con la wait(); la wait() permette semplicemente di attendere che uno dei figli del chiamante termini. se nessun figlio ha già terminato, la wait() si blocca. in alcuni casi è preferibile eseguire una nonblocking wait, in modo da ottenere immediatamente l'informazione che nessun figlio ha ancora terminato la propria esecuzione. - con la wait() è possibile avere informazioni sui figli che hanno terminato. non è invece possibile ricevere notifiche su cambiamenti di stato dei figli, come quando un figlio è bloccato da un segnale (come SIGSTOP) o quando un figlio stopped è risvegliato da un segnale SIGCONT. Daniele Radicioni - Laboratorio di Sistemi Operativi, turno 1 corso A 47 process group and getpgrp() #include #include #include int main(int argc, char** argv) { pid_t proc_pid; proc_pid = fork(); if(proc_pid){ // parent code printf("parent with pid: %d and group: %d\n", getpid(), getpgrp()); } else { // child code printf("child with pid: %d and group: %d\n", getpid(), getpgrp()); } exit(0); } Daniele Radicioni - Laboratorio di Sistemi Operativi, turno 1 corso A 48 The waitpid() System Call #include pid_t waitpid(pid_t pid, int *status, int options); Returns process ID of child, 0, or –1 on error L'argomento pid permette di selezionare il figlio da aspettare, secondo queste regole: - se pid > 0, attendi per il figlio con quel pid. - se pid == 0, attendi per qualsiasi figlio nello stesso gruppo di processi del chiamante (padre). - se pid < -1, attendi per qualsiasi figlio il cui process group è uguale al valore assoluto di pid. - se pid == -1, attendi per un figlio qualsiasi. la chiamata waitpid(–1, &status, 0) è equivalente a wait(&status). Daniele Radicioni - Laboratorio di Sistemi Operativi, turno 1 corso A 49 The waitpid() System Call #include pid_t waitpid(pid_t pid, int *status, int options); Returns process ID of child, 0, or –1 on error L'argomento options è una bit mask che può includere (in OR) zero o più dei seguenti flag: - WUNTRACED: oltre a restituire info quando un figlio termina, restituisci informazioni quando il figlio viene bloccato da un segnale. - WCONTINUED: restituisci informazioni anche nel caso il figlio sia stopped e venga risvegliato da un segnale SIGCONT. - WNOHANG: se nessun figlio specificato da pid ha cambiato stato, restituisci immediatamente, invece di bloccare il chiamante. in questo caso, il valore di ritorno di waitpid() è 0. se il processo chiamante non ha figli con il pid Daniele richiesto, Radicioni waitpid() - Laboratorio di Sistemifallisce Operativi,con turno l'errore 1 corso A ECHILD. 50 Wait Status Value Il valore status restituito da wait() e waitpid() ci consente di distinguere fra i seguenti eventi per il figlio: - il figlio ha terminato l'esecuzione chiamando la exit(), specificando un codice d'uscita (exit status) intero. - il figlio ha terminato l'esecuzione per la ricezione di un segnale non gestito. - il figlio è stato bloccato da un segnale, e waitpid() è stata chiamata con il flag WUNTRACED. - Il figlio ha ripreso l'esecuzione per un segnale SIGCONT, e waitpid() è stata chiamata con il flag WCONTINUED. Daniele Radicioni - Laboratorio di Sistemi Operativi, turno 1 corso A 51 WCONTINUED flag. We use the term wait status to encompass all of the above cases. The designation termination status is used to refer to the first two cases. (In the shell, we can obtain the termination status of the last command executed by examining the contents Michael Kerrisk, The Linux Programming interface - a Linux and UNIX® System of the variable $?.) Wait Status Value Although defined as an int, only the bottom 2 bytes of the value pointed to by status are actually used. The way in which these 2 bytes are filled depends on which Programming Handbook, No Starch Press, San Francisco, CA, 2010. of the above events occurred for the child, as depicted in Figure 26-1. Sebbene sia definito come int, solo gli ultimi 2 byte del valore Figure 26-1 shows the layout of the wait status value for Linux/x86-32. The puntato da status sono effettivamente utilizzati. Il modo in cui details vary across implementations. SUSv3 doesn6t specify any particular layout for this information, or even require that it is contained in the bottom 2 bytes questi 2 byte sono scritti dipende da quale è evento è occorso of the value pointed to by status. Portable applications should always use the macros described in this section to inspect this value, rather than directly per il figlio inspecting its bit-mask components. 15 bits 8 7 0 Normal termination exit status (0-255) 0 Killed by signal unused (0) termination signal (!= 0) core dumped flag Stopped by signal stop signal 0x7F Continued by signalRadicioni - Laboratorio di Sistemi Operativi, Daniele 0xFFFF turno 1 corso A 52 Figure 26-1: Value returned in the status argument of wait() and waitpid() Wait Status Value l'header file definisce un insieme standard di macro che possono essere utilizzate per interpretare un wait status. applicate allo status restituito da wait() o waitpid(), solo una delle seguenti macro restituirà true: - WIFEXITED(status). Restituisce true se il processo figlio è terminato normalmente. in questo caso la macro WEXITSTATUS(status) restituisce l'exit status del processo figlio. if (WIFEXITED(status)) { printf("child exited, status=%d\n", WEXITSTATUS(status)); } Daniele Radicioni - Laboratorio di Sistemi Operativi, turno 1 corso A 53 Wait Status Value - WIFSIGNALED(status). Restituisce true se il figlio è stato ucciso da un segnale. In questo caso, la macro WTERMSIG(status) restituisce il numero del segnale che ha causato la terminazione del processo. - WIFSTOPPED(status). Restituisce true se il figlio è stato bloccato da un segnale. In questo caso, la macro WSTOPSIG(status) restituisce il numero del segnale che ha bloccato il processo. - WIFCONTINUED(status). Restituisce true se il figlio è stato risvegliato da un segnale SIGCONT. Daniele Radicioni - Laboratorio di Sistemi Operativi, turno 1 corso A 54 esercizio scrivere un programma testa_wstatus_value.c che utilizzi la funzione (da implementare) void print_wait_status(char* msg, int status); per sperimentare le funzionalità delle macro W*. In particolare, un processo padre deve fare una fork(): il figlio deve mettersi in attesa. il padre deve ricevere informazioni sullo stato del processo figlio per mezzo della chiamata (che significano gli argomenti?? rispondere usando il manuale) childPid = waitpid(-1, &status, WUNTRACED | WCONTINUED); e invocherà la funzione print_wait_status() cui è affidato il compito di analizzare lo status assegnato dalla waitpid. verificare cosa capita lanciando il programma in background (./testa_wstatus_value &) e inviando i seguenti segnali (cercare sul manuale il loro significato): kill -STOP pid_figlio (pid_figlio è il pid del processo figlio) kill -CONT pid_figlio kill -ABRT pid_figlio Daniele Radicioni - Laboratorio di Sistemi Operativi, turno 1 corso A 55 int main(int argc, char *argv[]) { int status; pid_t childPid; switch(fork()) { case -1: errExit(“fork"); case 0: printf("Child started with PID = %ld\n", (long) getpid()); sleep(30); exit(EXIT_FAILURE); Figlio resta 30 secs in attesa di ricevere un default: segnale (dall’utente, da terminale) while(1) { childPid = waitpid(-1, &status, WUNTRACED | WCONTINUED); if (childPid == -1) errExit("waitpid"); print_wait_status(status); if (WIFEXITED(status) || WIFSIGNALED(status)) exit(EXIT_SUCCESS); } } exit(EXIT_SUCCESS); } Daniele Radicioni - Laboratorio di Sistemi Operativi, turno 1 corso A 56 void print_wait_status(int status) { if (WIFEXITED(status)) { printf("child exited, status=%d\n", WEXITSTATUS(status)); } else if (WIFSIGNALED(status)) { printf("child killed by signal %d (%s)", WTERMSIG(status), strsignal(WTERMSIG(status))); } else if (WIFSTOPPED(status)) { printf("child stopped by signal %d (%s)\n", WSTOPSIG(status), strsignal(WSTOPSIG(status))); } else if (WIFCONTINUED(status)) { printf("child continued\n"); } else { printf("what happened to this child? (status=%x)\n", (unsigned int) status); } } Daniele Radicioni - Laboratorio di Sistemi Operativi, turno 1 corso A 57 orphans and zombies in generale o il padre sopravvive al figlio, o viceversa. - chi diventa il padre di un processo figlio orfano? il figlio orfano è adottato da init, il progenitore di tutti i processi, il cui process ID è 1. - in altre parole, dopo che il genitore di un processo figlio termina, una chiamata a getppid() restituirà il valore 1. può essere utile per capire se il vero padre di un processo figlio è ancora vivo. Daniele Radicioni - Laboratorio di Sistemi Operativi, turno 1 corso A 58 orphans and zombies cosa capita a un figlio che termina prima che il padre abbia avuto modo di eseguire una wait()? - sebbene il figlio abbia terminato, il padre dovrebbe poter avere la possibilità di eseguire una wait() in un momento successivo per determinare come è terminato il figlio. - il kernel garantisce questa possibilità trasformando il figlio in uno zombie. gran parte delle risorse gestite da un figlio sono rilasciate al sistema per essere assegnate ad altri processi. l'unica parte del processo che resta è un'entry nella tabella dei processi che registra il process ID del figlio, il termination status, e le statistiche sull'utilizzo delle risorse. Daniele Radicioni - Laboratorio di Sistemi Operativi, turno 1 corso A 59 zombies un processo zombie non può essere ucciso da un segnale, neppure SIGKILL. questo assicura che il genitore possa sempre eventualmente eseguire una wait(). - quando il padre esegue una wait(), il kernel rimuove lo zombie, dal momento che l'ultima informazione sul figlio è stata fornita all'interessato. se il genitore termina senza fare la wait(), il processo init adotta il figlio ed esegue automaticamente una wait(), rimuovendo dal sistema il processo zombie. Daniele Radicioni - Laboratorio di Sistemi Operativi, turno 1 corso A 60 zombies se un genitore crea un figlio, ma fallisce la relativa wait(), un elemento relativo allo zombie sarà mantenuto indefinitamente nella tabella dei processi del kernel. - se il numero degli zombie cresce eccessivamente, gli zombie possono riempire la tabella dei processi, e questo impedirebbe la creazione di altri processi. poiché gli zombie non possono essere uccisi da un segnale, l'unico modo per rimuoverli dal sistema è uccidere il loro padre (o attendere la sua terminazione). a quel momento gli zombi possono essere adottati da init e rimossi. Daniele Radicioni - Laboratorio di Sistemi Operativi, turno 1 corso A 61 esecuzione di programmi Parent process running program “A” Michael Kerrisk, The Linux Programming interface - a Linux and UNIX® System Programming Handbook, No Starch Press, San A Child process fork() running program “A” Memo ry of parent copied to child Parent may perform A other actions here Child may perform further actions here wait(&status) execve(B,...) Francisco, CA, 2010. (optional) (optional) Execution of parent pa Chi ss ld B suspended ed st to atu pa s re nt Execution of program “B” Kernel restarts parent and exit(status) Daniele Radicioni optionally delivers - Laboratorio SIGCHLD di Sistemi Operativi, turno 1 corso A 63 Figure 24-1: Overview of the use of fork(), exit(), wait(), and execve() 24.2 Creating a New Process: fork() executing a new Program: execve() la system call execve() carica un nuovo programma nella memoria di un processo. con questa operazione, il vecchio programma è abbandonato, e lo stack, i dati, e lo heap del processo sono sostituiti da quelli del nuovo programma. - dopo avere eseguito l'inizializzazione del codice, il nuovo programma inizia l'esecuzione dalla propria funzione main(). varie funzioni di libreria, tutte con nomi che iniziano con exec, sono basate sulla system call execve(). - ciascuna di queste funzioni fornisce una diversa interfaccia alla stessa funzionalità. Daniele Radicioni - Laboratorio di Sistemi Operativi, turno 1 corso A 64 il codice che viene dopo una P1 exec in un programma non verrà mai eseguito! P2 viene eseguito solo se la ------- chiamata alla exec fallisce... ------- exec ------- ------- ------- ------- Daniele Radicioni - Laboratorio di Sistemi Operativi, turno 1 corso A 65 #include int execve(const char *pathname, char *const argv[], char *const envp[]); Never returns on success; returns –1 on error l'argomento pathname contiene il pathname del programma che sarà caricato nella memoria del processo. l'argomento argv specifica gli argomenti della linea di comando da passare al nuovo programma. si tratta di una lista di puntatori a stringa, terminati da puntatore a NULL. - il valore fornito per argv corrisponde al nome del comando. tipicamente, questo valore è lo stesso del basename (i.e., l'ultimo elemento) del pathname. l'ultimo argomento, envp, specifica la lista environment list per il nuovo programma. l'argomento envp corrisponde all'array environ; è una lista di puntatori a stringhe (terminata da puntatore a NULL) Daniele Radicioni nella forma - Laboratorio di Sisteminame=value. Operativi, turno 1 corso A 66 valori di ritorno poiché sostituisce il programma che la ha chiamata, una chiamata di execve() che va a buon fine non restituisce. non abbiamo quindi bisogno di controllare il valore di ritorno di execve(); sarà sempre –1. - il fatto che abbia restituito un qualche valore ci informa che è occorso un errore, e come sempre è possibile utilizzare errno per determinarne la causa. Daniele Radicioni - Laboratorio di Sistemi Operativi, turno 1 corso A 67 condizioni di errore Fra gli errori che possono essere restituiti in errno: - EACCES. l'argomento pathname non si riferisce a un file normale, il file non è un eseguibile, o una delle componenti del pathname non è ricercabile (i.e., sono negati i permessi di esecuzione sulla directory). - ENOENT. Il file riferito dal pathname non esiste. - ENOEXEC. Il file riferito dal pathname è marcato come un eseguibile ma non è riconosciuto come in un formato effettivamente eseguibile. - ETXTBSY. Il file riferito dal pathname è aperto in scrittura da un altro processo. - E2BIG. Lo spazio complessivo richiesto dalla lista degli argomenti e dalla lista dell'ambiente supera la massima dimensione consentita. Daniele Radicioni - Laboratorio di Sistemi Operativi, turno 1 corso A 68 per conoscere gli errori... #include #include... // --- stampa l'elenco degli errori noti sul sistema int idx = 0; // numero errore for( idx = 0; idx < sys_nerr; idx++ ) printf( "Error #%3d: %s\n", idx, strerror( idx ) ); da dove viene strerror()? cercare sul manuale…. Daniele Radicioni - Laboratorio di Sistemi Operativi, turno 1 corso A 69 esempio d'uso... int main(int argc, char *argv[]) { int i; char *argVec[VEC_SIZE] = {"buongiorno", "ciao", "cordiali saluti", "all the best", NULL}; char *envVec[VEC_SIZE] = {"silvia", "paolo", "mario", "carla", NULL}; switch(fork()) { case -1: fprintf(stderr,"fork fallita\n"); exit(0); case 0: printf("PID(figlio): %d\n", getpid()); execve(argv, argVec, envVec); exit(EXIT_FAILURE); Daniele Radicioni - Laboratorio di Sistemi Operativi, turno 1 corso A 70 esempio d'uso (continua dal precedente) switch(fork()) { case 0: printf("PID(figlio): %d\n", getpid()); execve(argv, argVec, envVec); exit(EXIT_FAILURE); default: printf("PID(padre): %d\n", getpid()); printf(" --- padre dorme per 3 secondi --- \n"); for (i = 0; i < 3; i++){ sleep(1); printf("*\n"); } printf(" --- terminazione padre --- \n"); exit(EXIT_SUCCESS); } } Daniele Radicioni - Laboratorio di Sistemi Operativi, turno 1 corso A 71 exec() Library Functions Esistono alcune funzioni di libreria che forniscono API alternative per eseguire una exec(). Tutte queste funzioni utilizzano la execve(), e differiscono le une dalle altre e dalla execve() per il modo in cui sono specificati il nome del programma, la lista degli argomenti e l'ambiente del nuovo programma. #include int execle(const char *pathname, const char *arg,... ); int execlp(const char *filename, const char *arg,... ); int execvp(const char *filename, char *const argv[]); int execv(const char *pathname, char *const argv[]); int execl(const char *pathname, const char *arg,... ); None of the above returns on success; all return –1 on error Daniele Radicioni - Laboratorio di Sistemi Operativi, turno 1 corso A 72 int execlp(const char *filename, const char *arg,... ); int execvp(const char *filename, char *const argv[]); - gran parte delle funzioni exec() si aspettano un pathname per specificare il nuovo programma da caricare. invece, execlp() e execvp() ci consentono di specificare solo il filename. il filename è cercato nella lista di directory specificata dalla variabile d'ambiente PATH. - la variabile d'ambiente PATH non è usata se il filename contiene uno slash (/), nel qual caso è trattato come un percorso relativo o assoluto. Daniele Radicioni - Laboratorio di Sistemi Operativi, turno 1 corso A 73 esercizio scrivere un programma in cui viene eseguita una fork(). - il processo figlio esegue una execlp() chiamando un secondo programma ('saluta_persone.c') e un certo numero di nomi propri di persona ('mario', 'ada', etc.) che stampi sullo schermo questi nomi. riscrivere il programma precedente (modificando opportunamente anche saluta_persone.c) eseguendo questa volta execvp() invece di execlp(). Daniele Radicioni - Laboratorio di Sistemi Operativi, turno 1 corso A 74 int execle(const char *pathname, const char *arg,... ); int execlp(const char *filename, const char *arg,... ); int execl(const char *pathname, const char *arg,... ); invece di utilizzare un array per specificare la lista argv per il nuovo programma, execle(), execlp(), e execl() richiedono al programmatore di specificare gli argomenti come una lista di stringhe. la lista di argomenti deve essere terminata da un puntatore a NULL come terminatore della lista. questo formato è indicato dal (char *) NULL commentato nei prototipi riportati sopra. - I nomi delle funzioni che richiedono la lista di argomenti come un array (execve(), execvp(), and execv()) contengono la lettera v (da vector). Daniele Radicioni - Laboratorio di Sistemi Operativi, turno 1 corso A 75 int execle(const char *pathname, const char *arg,... ); int execve(const char *pathname, char *const argv[], char *const envp[]); le funzioni execle() e execve() permettono al programmatore di specificare esplicitamente l'environment per il nuovo programma, utilizzando envp, un array di puntatori a stringhe terminato dal puntatore a NULL. - I nomi di queste funzioni terminano con la lettera e (da environment) per indicare questa caratteristica. Daniele Radicioni - Laboratorio di Sistemi Operativi, turno 1 corso A 76 File Descriptors e exec() per default, tutti i descrittori di file aperti da un programma che chiama exec() restano aperti attraverso la exec() e sono pertanto disponibili per il nuovo programma. - questo è spesso utile, poiché il programma chiamante può aprire file con particolari descrittori, e questi file sono automaticamente disponibili per il nuovo programma, senza che questo debba sapere i loro nomi e/o aprirli. Daniele Radicioni - Laboratorio di Sistemi Operativi, turno 1 corso A 77 esercizio scrivere 2 programmi che implementino le funzionalità della seguente istruzione: cp file1.txt file2.txt il primo programma esegue una fork, e il figlio chiama exec mandando in esecuzione il secondo (prodotto dal sorgente copia.c). Daniele Radicioni - Laboratorio di Sistemi Operativi, turno 1 corso A 78 Eseguire un comando di shell: system() #include int system(const char *command); La funzione system() permette di chiamare un programma per eseguire un comando di shell arbitrario. La funzione system() crea un processo figlio che invoca una shell per eseguire il comando command. Esempio di chiamata di system(): system("ls -lt | wc -l"); Daniele Radicioni - Laboratorio di Sistemi Operativi, turno 1 corso A 79 il valore di ritorno di system() valore di ritorno di system(): - se command è un NULL pointer, system() restituisce un valore diverso da 0 se una shell è disponibile, e 0 se nessuna shell è disponibile. - se non è stato possibile creare un processo figlio o il suo stato di terminazione non è stato ricevuto, system() restituisce –1. - se non è stato possibile eseguire la shell nel processo figlio, system() restituisce un valore come se la shell del processo figlio avesse terminato con la chiamata _exit(127). - se tutte le system calls hanno avuto successo, system() restituisce lo stato di terminazione della shell figlia utilizzata per eseguire il comando: lo status di terminazione di una shell è lo status di terminazione dell'ultimo comando eseguito.Daniele Radicioni - Laboratorio di Sistemi Operativi, turno 1 corso A 80 int main(int argc, char *argv[]) { char str[MAX_CMD_LEN]; int status; for (;;) { printf("Command: "); fflush(stdout); if (fgets(str, MAX_CMD_LEN, stdin) == NULL) break; status = system(str); printf("system() returned: status=0x%04x\n", (unsigned int) status); if (status == -1) errExit("system"); else { if(WIFEXITED(status) && WEXITSTATUS(status) == 127) printf("(Probably) could not invoke shell\n"); else // la shell ha eseguito il comando correttam. print_wait_status(NULL, status); } } exit(EXIT_SUCCESS); } Daniele Radicioni - Laboratorio di Sistemi Operativi, turno 1 corso A 81 int main(int argc, char *argv[]) { char str[MAX_CMD_LEN]; int status; WEXITSTATUS(status) for (;;) { If the value of WIFEXITED(status) printf("Command: "); WIFEXITED(status) is non-zero, Evaluates to a non-zero fflush(stdout); if (fgets(str, MAX_CMD_LEN,this macro== stdin) evaluates NULL) to the low- value if status was break; order 8 bits of the status returned status = forsystem(str); a child argument that the child process process that terminated printf("system() returned: status=0x%04x\n", passed to _exit() or exit(), or the (unsigned int) status); normally. if (status == -1) value the child process returned errExit("system"); from main(). else { if(WIFEXITED(status) && WEXITSTATUS(status) == 127) printf("(Probably) could not invoke shell\n"); else // la shell ha eseguito il comando correttam. print_wait_status(NULL, status); } } exit(EXIT_SUCCESS); } Daniele Radicioni - Laboratorio di Sistemi Operativi, turno 1 corso A 82 Eseguire un comando di shell: system() il vantaggio principale offerto da system() è la semplicità d'uso: - non dobbiamo gestire i dettagli relativi alle chiamate di fork(), exec(), wait(), e exit(). - la gestione degli errori e dei segnali è affidata a system() per nostro conto. - poiché system() utilizza la shell per eseguire un comando, il processamento legato alle sostituzioni, redirezioni è effettuato sul comando prima che esso sia eseguito. Daniele Radicioni - Laboratorio di Sistemi Operativi, turno 1 corso A 83 Eseguire un comando di shell: system() il principale costo della system() è l'inefficienza. - eseguire un comando usando system() richiede la creazione di almeno 2 processi (uno per la shell e uno o più per i comandi eseguiti), ciascuno dei quali esegue una exec(). - se l'efficienza o la velocità sono richiesti, è preferibile utilizzare chiamate fork() e exec() per eseguire il programma desiderato. Daniele Radicioni - Laboratorio di Sistemi Operativi, turno 1 corso A 84 implementazione di system() l'opzione -c del comando sh fornisce un modo semplice per eseguire una stringa che contiene comandi di shell arbitrari: $ sh -c "ls | wc" 38 38 444 per re-implementare system(), dobbiamo utilizzare una fork() per creare un figlio che faccia una execl() con gli argomenti corrispondenti al comando sh: execl("/bin/sh", "sh", "-c", command, (char *) NULL); Daniele Radicioni - Laboratorio di Sistemi Operativi, turno 1 corso A 85 quanti processi genera? for (i=0; i

Use Quizgecko on...
Browser
Browser