In un sistema operativo, la terminazione di un processo Γ¨ lβevento in cui un processo smette di essere eseguito e le sue risorse vengono rilasciate dal sistema.
1 - Tipi di terminazione di un processo
La terminazione di un processo puΓ² essere volontaria o forzata.
1.1 - Terminazione volontaria di un processo
In una terminazione volontaria (o normale), il processo completa lβesecuzione del suo codice (ad esempio, lβutente chiude un programma).
Ogni processo puΓ² chiamare esplicitamente la funzione di terminazione, come exit()
in UNIX o ExitProcess()
in Windows.
1.2 - Terminazione forzata di un processo
In una terminazione forzata (o anomala), il processo viene terminato a causa di un errore o unβeccezione non gestita, come una divisione per zero o un accesso in memoria non valido.
Il sistema operativo o un altro processo puΓ² terminare forzatamente un processo tramite chiamate di sistema (ad esempio, tramite lβinvio di un segnale con kill()
in UNIX o TerminateProcess()
in Windows). Generalmente solo il processo padre del processo che si vuole terminare puΓ² invocare una chiamata di sistema di questo tipo, altrimenti gli utenti potrebbero causare arbitrariamente la terminazione forzata di processi di chiunque.
2 - Fasi principali della terminazione di un processo
Le fasi principali della terminazione di un processo sono:
- Ricezione della richiesta di terminazione: il processo che deve terminare chiama una funzione di uscita (
exit()
,_exit()
oabort()
) nel caso della terminazione volontaria o riceve un segnale di terminazione da un altro processo tramitekill()
(es.SIGTERM
,SIGKILL
) nel caso della terminazione forzata. - Esecuzione di funzioni allβuscita: se il processo terminazione volontariamente tramite
exit()
, vengono eseguite prima le funzioni registrate tramiteatexit()
, in ordine inverso rispetto alla loro registrazione. - Chiusura e liberazione delle risorse: il sistema operativo chiude automaticamente tutti i file aperti dal processo. Anche altri tipi di risorse come socket, canali di comunicazione, semafori o altre strutture di dati vengono rilasciate, così come lo spazio di memoria assegnato al processo (sia quella per il codice che per i dati) viene liberato, in modo che possa essere utilizzato da altri processi.
- Aggiornamento dello stato del processo: nel PCB, lo stato del processo viene aggiornato in
terminated
e viene salvato il codice di uscita. FinchΓ© il processo padre non recupera lo stato del processo figlio tramite la funzionewait()
, il processo figlio rimane nello statozombie
. - Liberazione del PCB dalle code di schedulazione: il processo viene rimosso dalla coda dei processi pronti o da altre code di schedulazione. Il PCB del processo viene marcato per la rimozione o, nel caso di un processo nello stato
zombie
, il PCB viene mantenuto in memoria fino a che il processo padre recupera il suo stato. - Notifica del processo padre: se il processo Γ¨ stato generato da un processo padre, questβultimo viene notificato della terminazione del figlio tramite un segnale
SIGCHLD
. In UNIX, se il processo padre termina prima del figlio, tutti i figli vengono adottati dal processoinit
, che diventa il loro nuovo padre e sarΓ responsabile della loro gestione. - Rimozione del processo dal sistema operativo: una volta che tutte le risorse sono state rilasciate e, se necessario, il processo padre (se presente) ha recuperato il codice di uscita del figlio, il sistema operativo rimuove il PCB e tutte le strutture associate al processo dalla memoria. A questo punto, il processo Γ¨ completamente terminato.
2.1 - Codice di uscita
Quando un processo termina, spesso restituisce un codice di uscita (in inglese exit code) al sistema operativo o al processo padre. Questo codice Γ¨ un valore che indica se il processo Γ¨ terminato correttamente o se si Γ¨ verificato un errore. I valori, contenuti in un intero di cui vengono considerati solo gli 8 piΓΉ significativi bit, sono:
- Valore
0
: indica che il processo Γ¨ terminato con successo. - Un valore diverso dallo
0
: indica un errore o una condizione specifica che ha causato la terminazione del processo (ogni numero viene solitamente assegnato a un errore specifico che lo identifica).
Nella libreria stdlib.h
sono definite le macro:
EXIT_SUCCESS
: di solito0
.EXIT_FAILURE
: di solito1
.
Γ bene usare queste macro anzichΓ© direttamente i valori 0
e 1
perchΓ© puΓ² capitare che un sistema operativo possa utilizzare valori diversi da questi due per indicare il successo e il fallimento.
3 - Terminazione di un processo in C
In C, Γ¨ possibile terminare un processo in diversi modi:
- Tramite la funzione
_exit()
. - Tramite la funzione
_exit()
. - Tramite la funzione
abort()
.
3.1 - Terminazione volontaria di un processo tramite _exit()
La funzione _exit()
Γ¨ una chiamata di sistema utilizzata per terminare volontariamente un processo.
Il suo prototipo Γ¨ il seguente:
#include <unistd.h>
void _exit(int status);
dove:
status
: valore del codice di uscita della terminazione del processo.
3.2 - Terminazione volontaria di un processo tramite exit()
Di solito, il codice dei programmi non contengono la funzione _exit()
, ma contengono invece la funzione exit()
, che costituisce anchβessa una chiamata di sistema utile a terminare volontariamente un processo.
Il suo prototipo Γ¨ il seguente:
#include <unistd.h>
void exit(int status);
dove:
status
: valore del codice di uscita della terminazione del processo.
Possiamo notare che il prototipo Γ¨ in tutto e per tutto identico a quello della funzione _exit()
, ma la exit()
, oltre alla terminazione del processo, compie anche altre azioni:
- Esegue i gestori di uscita: se sono stati registrati dei gestori di uscita (exit handlers) tramite
atexit()
, vengono eseguiti prima della chiusura del processo. - Scarica e svuota i buffer di output: se ci sono dati ancora presenti nei buffer di output (es.
stdout
,stderr
, ecc.), questi vengono scritti effettivamente nel file o sullo schermo. - Chiude tutti i file aperti: i file aperti con
fopen()
vengono chiusi automaticamente. - Rilascia risorse di librerie standard: alcune librerie standard possono eseguire operazioni di pulizia della memoria.
- Restituisce il codice di uscita: il valore passato alla funzione
exit()
nel parametrostatus
viene restituito come codice di uscita del processo.
3.3 - Terminazione anomala di un processo tramite abort()
La funzione abort()
Γ¨ una chiamata di sistema utilizzata per terminare un processo in modo anomalo. In particolare, quel che fa Γ¨:
- Invia un segnale
SIGABRT
al processo corrente, che di default lo termina generando un core dump (se abilitato). - Non esegue i gestori registrati tramite
atexit()
, ignorandoli completamente. - Non garantisce la scrittura dei buffer di output
stdout
, i cui dati potrebbero essere persi se non sono stati svuotati manualmente. - Non chiude i file aperti.
Il suo prototipo Γ¨ il seguente:
#include <stdlib.h>
void abort();
3.3.1 - Uso di abort()
con assert()
La funzione abort()
viene usata allβinterno della chiamata della funzione assert()
(della libreria assert.h
): nel caso in cui lβasserzione fallisca, infatti, il programma viene terminato tramite abort()
.
3.4 - Registrazione di funzioni da eseguire alla terminazione volontaria di un processo tramite atexit()
La funzione atexit()
Γ¨ una funzione della libreria standard C che permette di registrare funzioni da eseguire automaticamente alla normale terminazione del processo, quando viene chiamata la funzione exit()
, in ordine inverso di registrazione (ossia secondo una politica LIFO).
Il suo prototipo Γ¨ il seguente:
#include <stdlib.h>
int atexit(void (*func)(void));
dove:
func
: puntatore a una funzione senza argomenti e senza valore di ritorno.- valore di ritorno
int
:0
: la registrazione ha successo.- Valore diverso da
0
: cβΓ¨ stato un errore nella registrazione.
Attenzione: limite di funzioni registrabili
Il numero massimo di funzioni registrabili tramite
atexit()
dipende dallβimplementazione, ma in genere Γ¨ 32.
3.5 - Monitoraggio della terminazione di un processo figlio qualsiasi tramite wait()
La funzione wait()
Γ¨ una chiamata di sistema usata per sospendere un processo padre (ossia metterlo nello stato waiting
) fino alla terminazione di un qualsiasi processo figlio. Γ utile per evitare processi zombie e per recuperare il codice di uscita di un figlio.
Il suo prototipo Γ¨ il seguente:
#include <sys/wait.h>
pid_t wait(int *status);
dove:
status
: puntatore a un intero dove verrΓ memorizzato il codice di uscita del figlio.pid_t
restituito:-1
: cβΓ¨ stato un errore (es. nessun figlio esistente, indicato dal valoreECHILD
dierrno
).- Valore diverso da
-1
: PID del figlio terminato con successo.
3.6 - Monitoraggio della terminazione di un particolare processo figlio tramite waitpid()
La funzione waitpid()
Γ¨ una chiamata di sistema usata per sospendere un processo padre (ossia metterlo nello stato waiting
) fino alla terminazione di un particolare processo figlio (a differenza della funzione wait()
che aspetta la terminazione di un figlio qualsiasi).
Il suo prototipo Γ¨ il seguente:
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *status, int options);
dove:
pid
: PID del processo figlio che si vuole monitorare, in particolare:pid > 0
: monitora il processo figlio specifico con PID uguale apid
.pid == 0
: monitora tutti i processi figli nel gruppo di processi del chiamante (padre).pid == -1
: monitora tutti i processi figli (equivalente allβuso della funzionewait()
).pid < -1
: monitora tutti i processi figli nel gruppo di processi specificato dal valore assolutopid
(cioè-pid
).
status
: puntatore a un intero dove verrΓ memorizzato il codice di uscita del figlio.options
: bit mask che puΓ² includere (in OR) zero o piΓΉ dei seguenti flag:WUNTRACED
: oltre a restituire informazioni quando un figlio termina, restituisce informazioni quando il figlio viene bloccato da un segnale.WCONTINUED
: restituisce informazioni anche nel caso il figlio sia nello statostopped
e venga risvegliato da un segnaleSIGCONT
.WNOHANG
: se nessun figlio specificato dapid
ha cambiato stato, restituisci immediatamente, invece di bloccare il chiamante. In questo caso, il valore di ritorno diwaitpid()
Γ¨0
. Se il processo chiamante non ha figli con il PID richiesto,waitpid()
fallisce con lβerroreECHILD
.
pid_t
restituito:-1
: cβΓ¨ stato un errore (es. nessun figlio esistente, indicato dal valoreECHILD
dierrno
).- Valore diverso da
-1
: PID del figlio terminato con successo.
3.7 - Estrazione del codice di uscita tramite la macro WEXITSTATUS(status)
La macro WEXITSTATUS(status)
in C Γ¨ usata per estrarre il codice di uscita di un processo figlio terminato normalmente. Questa macro viene utilizzata in combinazione con la funzione wait()
o waitpid()
, che restituiscono un valore di stato che contiene informazioni sulla terminazione del figlio.
Γ definita nel seguente modo:
#define WEXITSTATUS(x) ((x) >> 8)
dove:
x
: codice di uscita ottenuto dallβinvocazione di unawait()
owaitpid()
.- Valore restituito: primi bit piΓΉ significativi di
x
.
3.8 - Gestione dei processi orfani
In generale, puΓ² succedere uno dei seguenti due casi:
- Il processo figlio viene terminato prima del processo padre.
- Viceversa, il processo padre viene terminato prima di suo figlio e questβultimo viene detto orfano.
Chi diventa il padre di un processo figlio orfano? In UNIX, il figlio orfano viene automaticamente adottato dal processo init
, il progenitore di tutti i processi, il cui PID Γ¨ uguale a 1
.
CiΓ² significa che, dopo che il genitore di un processo figlio termina, una chiamata alla funzione getppid()
restituirΓ il valore 1
, ossia il PID del processo init
(il valore potrebbe differire in base al sistema operativo): ciΓ² puΓ² essere utile per capire se il vero padre di un processo figlio Γ¨ ancora vivo.
3.9 - Gestione dei processi zombie
Cosa capita a un figlio che termina prima che il padre abbia avuto modo di controllare il suo stato tramite la funzione wait()
?
Il kernel garantisce che il padre possa avere la possibilitΓ di eseguire una wait()
in un momento successivo alla sua terminazione trasformando il figlio in uno zombie. Gran parte delle risorse gestite da un figlio sono rilasciate al sistema per essere assegnate ad altri processi e lβunica parte del processo che resta Γ¨ unβentry nella tabella dei processi che registra il PID del figlio, il suo codice di uscita, e le statistiche sullβutilizzo delle risorse. Inoltre, 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.
Se un processo padre crea un processo 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 zombie possono essere adottati dal processo init
e automaticamente rimossi.
4 - Terminazione a cascata
La terminazione a cascata Γ¨ un meccanismo che si verifica in un sistema operativo quando un processo termina e i suoi processi figli vengono automaticamente terminati insieme ad esso. Questo comportamento Γ¨ spesso associato ai processi di tipo daemon o ai servizi che, quando vengono interrotti, devono anche interrompere i processi che hanno creato.
4.1 - Funzionamento della terminazione a cascata
Quando un processo padre termina, i processi figli possono essere influenzati in diversi modi a seconda della loro relazione con il processo padre e della gestione dei segnali. Alcuni punti chiave della terminazione a cascata sono:
- Segnale di terminazione: quando un processo padre termina, puΓ² inviare segnali ai suoi processi figli. Se i processi figli non sono in grado di gestire il segnale di terminazione (come
SIGTERM
oSIGKILL
), verranno terminati dal sistema operativo. - Processi orfani: se un processo padre termina, i suoi processi figli diventano orfani. In UNIX, i processi orfani vengono adottati dal processo
init
, che si occupa di gestire la loro terminazione. I processi orfani non vengono necessariamente terminati a meno che non vengano esplicitamente chiesti di farlo. - Comportamento di
wait()
: quando un processo padre utilizza la funzionewait()
, attende la terminazione di uno dei suoi processi figli. Se il padre termina prima dei figli, il sistema operativo puΓ² terminare i figli se la relazione di dipendenza Γ¨ tale che non ha senso che continuino a vivere.
4.2 - Esempio di terminazione a cascata in linguaggio C
Ecco un esempio in linguaggio C, in cui un processo padre crea un processo figlio e poi termina. Se il processo padre termina, il comportamento dei processi figli puΓ² essere osservato.
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
int main() {
pid_t pid = fork();
if (pid < 0) {
perror("fork failed");
exit(EXIT_FAILURE);
} else if (pid == 0) {
// Questo Γ¨ il processo figlio
while (1) {
printf("Processo figlio in esecuzione (PID: %d).\n", getpid());
sleep(1);
}
} else {
// Questo Γ¨ il processo padre
printf("Processo padre (PID: %d) in attesa di 5 secondi prima di terminare.\n", getpid());
sleep(5);
printf("Processo padre sta per terminare.\n");
exit(0); // Termina il processo padre
}
return 0;
}
Durante lβesecuzione di questo programma, si puΓ² notare che il processo figlio continua a stampare i messaggi anche dopo la terminazione del processo padre, poichΓ© diventa un processo orfano e viene adottato dal processo init
. Se invece il processo padre inviasse un segnale di terminazione, come SIGKILL
o SIGTERM
, anche il processo figlio verrebbe terminato, mostrando un comportamento di terminazione a cascata.