Salve amici di ElectroYou. Poichè molti di voi mi stanno aiutando a comprendere meglio il mondo dell'elettronica, tra componenti e circuiti che a me non stanno molto simpatici, ho deciso di "ripagare" questo fantastico sito con delle "lezioni" riguardo la gestione dei processi e dei thread in un sistema Linux, argomento che a me piace parecchio. Non so quanti di voi ne faranno uso, però qualora ne abbiate bisogno, posso garantirvi che a me sono servite parecchio. Buona Lettura.
Indice |
I PROCESSI
Un programma in esecuzione è chiamato processo. Quando viene invocato un comando in una shell (durante quindi un processo), il programma corrispondente viene eseguito in un nuovo processo.
Molte delle funzioni che manipolano processi sono dichiarate negli header file <sys/types.h> e <unistd.h>.
Ogni programma in esecuzione usa uno o più processi. Ogni processo in un sistema Linux è identificato dal suo unico Process ID, cioè il pid. Il pid viene assegnato sequenzialmente da Linux quando viene creato un nuovo processo. Ogni processo inoltre ha un processo genitore, identificato dal suo Parent Process ID, o ppid.
Quando vogliamo riferirci ai pid in un programma C, bisogna usare il tipo identificativo pid_t, che è definito in <sys/types.h>. Un programma può ottenere il pid del processo che sta eseguendo con la chiamata di sistema getpid(), e può ottenere quello del processo genitore con la chiamata di sistema getppid(). Vediamo il programma che stampa il suo pid e quello del suo genitore:
#include <stdio.h> #include <stdlib.h> int main() { char esci; pid_t a,b; a=getpid(); b=getppid(); printf ("Il pid è : %d\n",a); printf ("Il pid del padre è : %d\n",b); printf ("Premi un tasto per uscire: "); scanf ("%c",&esci); }
Si può osservare che se si invoca questo programma diverse volte, appare un differente Process ID perchè ogni invocazione è un nuovo processo. Comunque, se ogni volta viene invocato dalla stessa shell, il pid del genitore (ovvero il pid del processo shell) è sempre lo stesso.
IL COMANDO PS
Per vedere i processi che sono in esecuzione sul nostro sistema bisogna invocare dalla shell il comando ps. Per avere molti più dettagli su ciò che è in esecuzione, invochiamo questo comando:
$ps -e -o pid,ppid,command
- l'opzione -e ordina a ps di mostrare tutti i processi in esecuzione sul sistema;
- l'opzione -o pid,ppid,command dice a ps quali informazioni mostrare su ogni processo, in questo caso il pid, il ppid e il comando in esecuzione in questo processo.
UCCIDERE UN PROCESSO
Un processo può essere eliminato con il comando kill, specificando sulla linea di comando il Process ID del processo che si vuole eliminare. Questo comando invierà al processo il segnale SIGTERM, o terminazione, il quale porterà il processo a terminare, senza che il programma in esecuzione agisca esplicitamente o mascheri il segnale inviato.
CREAZIONE DI PROCESSI
Per creare nuovi processi ci sono due tecniche comuni: utilizzando la funzione system (relativamente semplice, ma inefficiente perchè ha considerevoli rischi di sicurezza) o utilizzando le funzioni fork e exec (molto più complesse, ma più flessibili, veloci e sicure). Vediamole nel dettaglio:
- La funzione system rappresenta un modo semplice di eseguire un comando dall'interno di un programma, come se il comando fosse inserito dentro la shell. Infatti, questa funzione crea un sottoprocesso che esegue la shell Bourne e inserisce manualmente il comando a quella shell per l'esecuzione. La funzione system ritorna lo stato d'uscita del comando shell.
- La funzione fork crea un processo figlio che è un'esatta copia del suo processo genitore. Quando viene eseguita la fork e viene creato un nuovo processo, il sistema operativo inizializza le strutture dati necessarie a gestirlo. In particolare crea un nuovo PCB (Blocco per il Controllo dei Processi) per quel processo. Il processo genitore continua ad eseguire il programma dal punto in cui la fork è stata chiamata. Il processo figlio esegue lo stesso programma dallo stesso punto. La differenza tra i due processi è che il processo figlio è un nuovo processo e quindi ha un nuovo Process ID, diverso da quello del genitore. La fork restituisce un intero: -1 se non è andata a buon fine, 0 al processo figlio, mentre al processo padre viene dato come valore di ritorno il pid del figlio. Ecco il codice della funzione fork:
#include <stdio.h> #include <stdlib.h> #include <sys/types.h> #include <unistd.h> int main() { int n; n=fork(); if (n==-1) { fprintf(stderr,"Fork fallita\n"); exit(1); } else if(n==0) { /*processo figlio*/ printf ("\nSono il processo figlio; risultato della fork= %d\n",n); printf ("\nIl mio Process ID è: %d\n",getpid()); printf ("\nIl Process ID di mio padre è: %d\n",getppid()); exit(0); } else { /* processo padre */ printf ("\nSono il processo padre; risultato della fork= %d\n",n); printf ("\nIl mio Process ID è: %d\n",getpid()); exit(0); } }
- La famiglia exec fa sì che un particolare processo termini di essere l'istanza di un programma e diventi quella di un altro programma. Questa funzione sostituisce il programma in esecuzione in un processo con un altro programma che viene eseguito dall'inizio, assumento che la chiamata exec non abbia riscontrato errori. All'interno della famiglia exec ci sono diverse funzioni: execv,execvp,execve prendono come parametro l'array di puntatori agli argomenti da passare al file da eseguire; exeeclp,execvp prendono come primo argomento un file e non un pathname, quindi il file da eseguire è ricercato in una delle directory specificate in PATH; execle,execve passano al file da eseguire la environment list; un processo che invece chiama le altre exec copia la sua variabile per il nuovo file (programma); execl,execlp,execle prendono come parametro la lista degli argomenti da passare al file da eseguire.
NOTA: Per generare un nuovo processo, bisogna prima usare la fork per fare una copia del processo corrente. Dopo si usa la exec per trasformare uno di questi processi in un'istanza del programma che si vuole generare.
TERMINAZIONE DI UN PROCESSO
In alcune situazioni, per il processo genitore è consigliabile aspettare che uno o più processi figli terminino. Questo può avvenire con la chiamata di sistema wait: questa permette di aspettare che un processo finisca l'esecuzione e abilita il processo genitore a recuperare l'informazione sulla terminazione del figlio. Possono esserci tre casi:
- Se il processo figlio termina mentre il genitore sta chiamando una funzione wait, il processo figlio termina e il suo stato di terminazione passa al genitore grazie alla chiamata wait;
- Se un padre termina prima del figlio, allora il processo init (primo processo dopo l'avvio del calcolatore) diventa il nuovo padre e cancella tutti i processi figli che ha ereditato;
- Se un processo figlio termina, ma il padre non sta chiamando la wait, il processo figlio non svanisce (poichè le informazioni andrebbero perse), ma diventa un processo zombie.
Un processo zombie è un processo che ha terminato ma non è stato ancora cancellato. Deve essere il padre a cancellare il figlio zombie, tramite la chiamata wait. Supponiamo che un programma crei un processo figlio, compie altre computazioni, e dopo chiami il wait. Se il processo figlio non ha terminato a quel punto, il processo genitore si bloccherà nel wait finchè il processo figlio finisce. Se il processo figlio termina prima che il genitore chiami il wait, il processo figlio diventa uno zombie. Quando il processo genitore chiama il wait, lo stato di terminazione del figlio zombie sarà estratto, il processo figlio cancellato e la chiamata wait ritorna immediatamente. Ecco il codice per la creazione di un processo zombie:
#include <stdlib.h> #include <stdio.h> #include <sys/types.h> #include <unistd.h> int main() { pid_t child_pid; child_pid =fork(); /*'''Crea un processo figlio'''*/ if (child_pid>0) { /*'''Questo è il processo genitore. Aspetta un minuto.'''*/ sleep(60); } else { /*'''Questo è il processo figlio. Esci immediatamente.'''*/ exit(0); } return 0; }
Dopo averlo compilato, lo eseguiamo come make_zombie e mentre sta eseguendo inseriamo il seguente comando:
$ps -e -o pid,ppid,stat,command
Osserveremo che, oltre al processo genitore make_zombie, c'è un altro processo make_zombie nella lista. E' il processo figlio. Inoltre il Process ID del genitore è il Process ID del più importante processo make_zombie. Il processo figlio è segnalato come <defunct>, e il codice del suo stato è Z, cioè Zombie.
Nel prossimo intervento tratteremo i Threads, un argomento molto ma molto importante per la comprensione del funzionamento di un sistema operativo. Mi raccomando non mancate ;)