Il concetto di processo Un sistema operativo può eseguire una grande varietà di attività diverse: sistemi batch – jobs; sistemi time-shared – programmi utente o task. Molti libri di testo utilizzano i termini job e processo in modo quasi interscambiabile. Processo – un programma in esecuzione; l’esecuzione di un processo deve progredire in modo sequenziale. Un processo comprende: codice; stack (es: variabili temporanee, parametri di subroutine); sezione dati (es: variabili globali); program counter; un identificatore di processo univoco (PID). 1.1 Il concetto di processo 1.2 Modelli di esecuzione dei processi Multiprogrammazione di quattro programmi. Modello concettuale di 4 processi indipendenti e sequenziali. Un solo programma è attivo in ogni istante. 1.3 Creazione e terminazione di un processo Eventi principali che comportano la creazione di un processo: 1. 2. 3. 4. boot del sistema; esecuzione di una chiamata di sistema per la creazione di un processo; l’utente richiede la creazione di un nuovo processo; inizio di un job batch. Condizioni per la terminazione di un processo: 1. normale uscita, esecuzione dell’ultima istruzione del codice (volontaria); chiamata esplicita di un’apposita system call (volontaria); fatal error, tentativi di operazioni illegali (involontaria); interruzione causata dalla ricezione di segnali (involontaria). 2. 3. 4. 1.4 Stato del processo Mentre un processo è in esecuzione, subisce un cambiamento di stato: new: il processo viene creato; running: le istruzioni vengono eseguite; waiting: il processo è in attesa che accada qualche evento; ready: il processo è in attesa di essere assegnato a un processore; terminated: il processo ha terminato l’esecuzione. 1.5 Diagramma degli stati di un processo 1.6 Processi UNIX UNIX e Linux sono sistemi multitasking, pertanto ci possono essere più contemporaneamente. processi in esecuzione Come si fa a visualizzare la lista dei processi in esecuzione? $ ps -A 1.7 ps utility ps è un’abbreviazione di Process Status. Il comando può essere usato per mostrare i processi attualmente in esecuzione su sistemi Unix/Linux. $ ps PID TTY 24467 pts/0 24504 pts/0 TIME CMD 00:00:00 bash 00:00:00 ps $ ps -u USER PID %CPU %MEM dellungo 24467 0.0 0.7 dellungo 24510 0.0 0.3 VSZ RSS TTY 2740 1632 pts/0 2504 688 pts/0 STAT START S 11:02 R 11:39 TIME COMMAND 0:00 -bash 0:00 ps –u $ ps -l F S 100 S 000 R UID PID PPID 501 24467 24466 501 24511 24467 C PRI 0 75 0 76 NI ADDR SZ WCHAN 0 - 685 wait4 0 - 746 - TTY pts/0 pts/0 TIME CMD 00:00:00 bash 00:00:00 ps Digitando ps --help viene mostrato un sommario delle opzioni e dell’utilizzo in generale del comando ps. 1.8 Eseguire processi in background Se a un singolo comando, a una pipeline, o a un gruppo di comandi viene fatto seguire il metacarattere &, viene creata una sottoshell per eseguire i comandi come processo in background. Il processo in background viene eseguito concorrentemente con la shell padre, non è controllato da tastiera e non pregiudica il controllo della tastiera. Eseguire processi in background è molto utile per eseguire task in parallelo che non richiedono controllo da tastiera. $ xemacs & [1] 25330 $ ps PID TTY 25298 pts/1 25330 pts/1 25336 pts/1 TIME 00:00:00 00:00:00 00:00:00 CMD bash xemacs ps 1.9 Eseguire processi in background Per sospendere il processo foreground: Ctrl+Z Per inviare il processo sospeso in: foreground: fg background: bg $ xemacs Ctrl+Z [2]+ Stopped $ ps -l F S TTY 000 S 501 000 T 501 000 R 501 $ bg %2 [2]+ xemacs & $ ps -l F S TTY 000 S 501 000 S 501 000 R 501 xemacs TIME 00:00:00 00:00:00 00:00:00 CMD bash xemacs ps TIME 00:00:00 00:00:00 00:00:00 CMD bash xemacs ps 1.10 Terminazione di un processo È possibile terminare un processo digitando: Ctrl+C. Se vogliamo terminare un processo background prima che si completi, possiamo usare il comando kill. Naturalmente, abbiamo i permessi per terminare esclusivamente i nostri processi. La sintassi per il comando kill è: kill [-signalId] PID. Esempio: $ ps PID TTY TIME CMD 25298 pts/1 00:00:00 bash 25330 pts/1 00:00:00 xemacs 25353 pts/1 00:00:00 ps $ kill 25330 [1]+ Terminated xemacs kill invia la signal con relativo codice signalId alla lista dei processi elencati. Di default, kill invia una signal TERM (numero 15). Per ottenere la lista delle signals, si può usare kill –l. Alcuni processi resistono ad essere terminati facilmente, per essi si può usare l’opzione "-9" per forzare l’eliminazione del processo. 1.11 Creazione dei processi Il solo modo di creare un nuovo processo in UNIX e Linux è quello di duplicare un processo esistente. Processi padri creano processi figli, che, a loro volta, creano altri processi, dando vita così a un albero di processi. 1.12 Creazione dei processi Nel momento in cui un processo viene duplicato (fork()), i processi padre e figlio sono totalmente identici, eccetto che per il loro PID; codice, dati e stack del figlio sono una copia di quelli del padre, pertanto i due processi continuano ad eseguire lo stesso codice dall’istruzione che segue la fork. Tuttavia, un processo figlio può sostituire (exec()) il suo codice con un altro file eseguibile, differenziandosi in tal modo dal proprio padre. Condivisione di risorse Padre e figlio condividono tutte le risorse. Il figlio condivide un sottoinsieme delle risorse del padre. Padre e figlio non condividono alcuna risorsa. Esecuzione Padre e figlio sono in esecuzione concorrente. Il padre attende che il figlio termini per riprendere l’esecuzione. 1.13 Terminazione di un processo Il processo esegue l’ultima istruzione e chiede al sistema operativo di essere cancellato (exit()). Possone essere restituiti dai dati (output) al padre da parte di un figlio che viene terminato (via wait()). Tutte le risorse del processo vengono deallocate dal sistema operativo. Il padre può terminare l’esecuzione dei processi figli (abort()). Il figlio ha ecceduto nell’uso di alcune delle risorse che gli sono state allocate. Il compito assegnato al figlio non è più necessario. Il padre termina. 1.14 Esempio Parent Process PID 34 Running shell Duplicate: fork () Child Process PID 35 Running shell Parent Process PID 34 Running shell Waiting for child Differentiate: exec () Child Process PID 35 Running utility Wait for child: wait () Terminate: exit () Parent Process PID 34 Running shell awakens signal 1.15 Child Process PID 35 Terminates Creare un nuovo processo: fork System call: int fork() causa la duplicazione di un processo. Il processo figlio condivide il codice con il padre ed eredita una copia delle aree dati globali e dello stack, ma riceve un differente PID. Se fork() ha successo, restituisce il PID del figlio al processo padre, e restituisce 0 al processo figlio. Se fallisce, restituisce –1 al processo padre e non viene creato alcun figlio. Entrambi i processi continuano ad eseguire il medesimo codice in concorrenza, ma mantengono uno spazio di indirizzamento dei dati completamente separato. 1.16 Esempio con fork #include<stdio.h> main() { int pid; printf("I am the original process with PID %d and PPID %d \n",getpid(),getppid()); pid=fork(); if (pid!=0) {printf("I am the parent process with PID %d and PPID %d\n",getpid(),getppid()); printf("My child's PID is %d\n",pid);} else {printf("I am the child process with PID %d and PPID %d\n",getpid(),getppid());} printf("PID %d terminates \n",getpid()); } 1.17 Esempio con fork $ gcc fork1.c -o fork1.out $ ls -l fork1.* -rw-r--r-- 1 lferrari lferrari 421 apr 28 12:35 fork1.c -rwxrwxr-x 1 lferrari lferrari 12043 apr 28 12:36 fork1.out* $ ./fork1.out I am the original process with PID 24629 and PPID 24467 I am the parent process with PID 24629 and PPID 24467 I am the child process with PID 24630 and PPID 24629 PID 24630 terminates My child's PID is 24630 PID 24629 terminates 1.18 Processi orfani Cosa accade se un padre termina senza attendere la morte del figlio? Ad esempio, si potrebbe inserire un istruzione di sleep nel codice del figlio del programma precedente: else{ sleep (5); printf(“I am the child process …); } In tal modo, mandando in esecuzione orphan.out (il programma nuovo) otteniamo: $ ./orphan.out I am the original process with PID 24638 and PPID 24467 I am the parent process with PID 24638 and PPID 24467 My child's PID is 24639 PID 24638 terminates I am the child process with PID 24639 and PPID 1 PID 24639 terminates 1.19 Processi orfani Ciò significa che se un padre muore prima del proprio figlio, il figlio viene automaticamente adottato dal processo originario init (PID 1). Il kernel assicura che tutti i figli di un processo in terminazione vengono adottati da init impostando il loro PPID a 1. init Parent Process dies first Adopt child Child Process Survives the parent 1.20 Terminare un processo: exit System call: int exit (int status) chiude tutti i descrittori del processo, provvede a deallocare il suo codice, i suoi dati e il suo stack, e termina il processo. Quando un processo figlio termina, invia al proprio padre una signal SIGCHLD e rimane in attesa che venga accettato il proprio valore d’uscita status. Un processo in attesa che il proprio padre raccolga le informazioni sul suo stato viene chiamato un processo zombie. Un padre raccoglie le informazioni relative alla terminazione di un figlio eseguendo la system call wait(). Se il processo padre è vivo ma non esegue mai una wait(), il codice di uscita del figlio non verrà mai accettato e il processo rimane uno zombie. 1.21 Esempio con exit $ cat exit.c #include<stdio.h> main() {printf (“I am going to exit with return code 42”); exit(42);} $ exit.out I am going to exit with return code 42 $ echo $? 42 1.22 Processo zombie $ cat zombie.c #include<stdio.h> main() { int pid; pid=fork(); if (pid!=0) {while(1) sleep(1000);} else {exit(42);} } $ ./zombie.out & [1] 25363 $ ps PID TTY TIME CMD 24467 pts/0 00:00:00 bash 25363 pts/0 00:00:00 zombie.out 25364 pts/0 00:00:00 zombie.out <defunct> 25365 pts/0 00:00:00 ps $ kill 25363 [1]+ Terminated ./zombie.out 1.23 Attendere un figlio: wait System call: int wait (int status) fa sì che un processo venga sospeso finchè uno dei suoi figli termina. Una chiamata wait() che ha successo restituisce il PID del figlio che è stato terminato e memorizza nella variabile status lo stato di terminazione del figlio nel modo seguente: se il byte meno significativo (più a destra) di status è zero, la terminazione del figlio è avvenuta volontariamente e nel byte più significativo (più a sinistra) è memorizzato lo stato di terminazione (il valore eventualmente passato dalla exit() al figlio); se il byte meno significativo è diverso da zero, la terminazione è avvenuta involontariamente e il suo valore è uguale al numero della signal che ha provocato la terminazione del figlio. se un processo esegue una wait() e non ha figli, wait() restituisce immediatamente –1. 1.24 Esempio con wait #include<stdio.h> main() { int pid, status, childPID; printf(“I am the original process with PID %d getpid(),getppid()); pid=fork(); if (pid!=0) {printf("I am the parent process with PID %d and PPID %d\n",getpid(),getppid()); childPID = wait(&status); printf(“A child with PID %d terminated with exit code %d \n",childPID, status>>8);} else {printf("I am the child process with PID %d and PPID %d\n",getpid(),getppid()); exit(42);} printf("PID %d terminates \n",getpid()); } 1.25 \n”, Esempio con wait $ gcc wait.c -o wait.out $ ls -l wait.* -rw-r--r-- 1 lferrari lferrari 421 apr 28 12:35 wait.c -rwxrwxr-x 1 lferrari lferrari 12043 apr 28 12:36 wait.out* $ ./wait.out I am the parent process with PID 24629 I am the parent process with PID 24629 and PPID 24467 I am the child process with PID 24630 and PPID 24629 A child with PID 24630 terminated with exit code 42 PID 24629 terminates 1.26 Esempio con wait #include<stdio.h> main() { int pid, status, childPID; printf(“I am the original process with PID %d getpid(),getppid()); pid=fork(); if (pid!=0) {printf("I am the parent process with PID %d and PPID %d\n",getpid(),getppid()); childPID = wait(&status); printf(“A child with PID %d terminated with exit code %d \n",childPID, status);} else {printf("I am the child process with PID %d and PPID %d\n",getpid(),getppid()); sleep(10000); exit(42);} printf("PID %d terminates \n",getpid()); 1.27 } \n”, Esempio con wait $ xterm & (con xterm apriamo una nuova finestra) $ ./waitsleep.out I am the parent process with PID 2000 I am the parent process with PID 2000 and PPID 1806 I am the child process with PID 2001 and PPID 2000 Nel nuovo terminale aperto digitiamo $ ps –a PID 1999 2000 2001 2002 TTY pst/1 pst/1 pst/1 pst/2 CDM xterm.real wait.out wait.out ps $ kill –kill 2001 Nel finestra originaria si vedrà A child with PID 2001 terminated with exit code 9 PID 2001 terminates 1.28 Signal I processi a volte si trovano ad avere a che fare con eventi imprevisti (un errore di floating point, un’interruzione dell’alimentazione elettrica, la morte di un processo figlio, una richiesta di terminazione da parte di un utente (cioè Ctrl-C) …). Tali eventi vengono detti interrupts; essi devono interrompere il regolare flusso del processo per essere opportunamente presi in consierazione. Quando Linux riconosce che un tale evento si è verificato, manda una signal al processo corrispondente. C’è un’unica signal, opportunamente numerata con interi positivi da 1 a 63, per ogni possibile evento. Ad esempio, se un processo causa un errore di floating point, il kernel manda al processo in questione la signal SIGFPE (vale a dire, la signal numero 8). Il kernel non è l’unico che può mandare signal. Ogni processo può inviare ad ogni altro processo una signal, se ha il permesso. 1.29 La lista predefinita delle signal $ kill -l 1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP 6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1 11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP 21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ 26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR 31) SIGSYS 32) SIGRTMIN 33) SIGRTMIN+1 34) SIGRTMIN+2 35) SIGRTMIN+3 36) SIGRTMIN+4 37) SIGRTMIN+5 38) SIGRTMIN+6 39) SIGRTMIN+7 40) SIGRTMIN+8 41) SIGRTMIN+9 42) SIGRTMIN+10 43) SIGRTMIN+11 44) SIGRTMIN+12 45) SIGRTMIN+13 46) SIGRTMIN+14 47) SIGRTMIN+15 48) SIGRTMAX-15 49) SIGRTMAX-14 50) SIGRTMAX-13 51) SIGRTMAX-12 52) SIGRTMAX-11 53) SIGRTMAX-10 54) SIGRTMAX-9 55) SIGRTMAX-8 56) SIGRTMAX-7 57) SIGRTMAX-6 58) SIGRTMAX-5 59) SIGRTMAX-4 60) SIGRTMAX-3 61) SIGRTMAX-2 62) SIGRTMAX-1 63) SIGRTMAX man signal mostra un riassunto della descrizione della signal. 1.30 Sospensione e ripresa dei processi #include<stdio.h> #include<signal.h> main() { int pid1, pid2; pid1=fork(); if (pid ==0) /* first child */ {while(1) {printf(“pid1 is alive\n”); sleep(1);}} pid2=fork(); if (pid ==0) /* second child */ {while(1) {printf(“pid2 is alive\n”); sleep(1);}} sleep(3); kill(pid1,SIGSTOP); /* suspend first child */ sleep(3); kill(pid1,SIGCONT); /* resume first child */ sleep(3); kill(pid1,SIGINT); /* kill first child */ kill(pid1,SIGINT); /* kill second child */ 1.31 } Esempio con kill() $ ./susres.out pid1 pid2 pid1 pid2 pid1 pid2 pid2 pid2 pid2 pid1 pid2 pid1 pid2 pid1 pid2 $ is is is is is is is is is is is is is is is alive alive alive alive alive alive alive alive alive alive alive alive alive alive alive … entrambi in esecuzione nei primi tre secondi … ora è in esecuzione soltanto il secondo figlio … il primo figlio viene ripreso 1.32 Sostituzione di codice: exec La system call: int execl (char path, char arg0, …, char argn, NULL) sostituisce codice, dati globali e stack del processo chiamante prendendo i nuovi dal file con pathname path. Se l’eseguibile non viene trovato, viene restituito -1; altrimenti, il processo chiamante sostituisce i propri codice, dati e stack con quelli dell’eseguibile e comincia ad eseguire il nuovo codice. Una chiamata exec() eseguita con successo è “senza ritorno”, in quanto il processo chiamante perde ogni riferimento al vecchio codice. C’è un’intera famiglia di system call di tipo exec() in Unix per la sostituzione del codice: execv(), execlp(), execvp() … 1.33 Esempio con execl $ cat exec.c #include<stdio.h> main() { printf(“I am process with PID %d and I am about to exec an ls –l \n”, getpid()); execl (“/bin/ls”,“ls”, “-l”, NULL); printf(“this line should never be executed \n”); } $./exec.out I am process with PID 2031 and I am about to exec ls -l -rw-r--r-- 1 lferrari lferrari 421 apr 28 12:35 exec.c -rwxrwxr-x 1 lferrari lferrari 12043 apr 28 12:36 exec.out* 1.34 Una shell semplificata #include<stdio.h> main() { while (1) { type_prompt( ); read_command (path, command, parameters) pid=fork(); if (pid < 0) { printf(“Unable to fork”);} else if (pid != 0) {wait(&status);} else {execl (path, command, parameters, NULL);} } 1.35 Gestione dei processi della shell Parent Process PID 501 Running shell Duplicate: fork () Child Process PID 748 Running shell Parent Process PID 501 Running shell Waiting for child Differentiate: exec () Child Process PID 748 Running utility Wait for child: wait () Terminate: exit () Parent Process PID 501 Running shell awakens signal 1.36 Child Process PID 748 Terminates Passi effettuati durante l’esecuzione del comando di shell ls 1.37 Scheduling Con questo termine si indica l’attività di allocare il tempo della CPU a compiti diversi all’interno di un sistema operativo. Sebbene si pensi normalmente allo scheduling come all’esecuzione e interruzione dei processi, in Linux lo scheduling comprende anche l’esecuzione dei vari task del kernel. L’esecuzione dei task del kernel comprende sia i task che sono richiesti da un processo in esecuzione sia i task che vengono eseguiti internamente per conto dei driver delle periferiche. 1.38 La sincronizzazione del kernel Una richiesta di esecuzione in modalità kernel può avvenire in due modi distinti: un programma in esecuzione può richiedere un servizio del sistema operativo, o esplicitamente tramite una system call, o implicitamente, ad esempio, quando si verifica un page fault; un driver di periferica può scatenare un interrupt hardware che provoca l’esecuzione da parte della CPU dell’handler definito dal kernel per quel particolare interrupt. La sincronizzazione del kernel richiede una struttura che consenta l’esecuzione di una sezione critica del kernel, senza violare l’integrità dei dati condivisi. 1.39 Scheduler di UNIX Lo scheduler di UNIX è basato su una struttura a code multiple. 1.40