In Linux un processo può trovarsi nei seguenti stati:
- RUNNING & RUNNABLE
- INTERRRUPTABLE_SLEEP
- UNINTERRUPTABLE_SLEEP
- STOPPED
- ZOMBIE
Una volta lanciato, un processo cambierà il proprio stato diverse volte fino a quando non terminerà la sua esecuzione, finendo così nello stato ZOMBIE.
Quando scriviamo un programma che richiede la generazione (spawn) di sottoprocessi dobbiamo sicuramente eseguire la sequenza di fork/exec
. Inoltre, visto che gestiamo più sottoprocessi, è buona prassi registrare un signal handler per il segnale SIGCHILD. Questo segnale viene inviato dal sistema operativo al Parent Process ID (PPID) quando il processo figlio è terminato e si trova adesso nello stato zombie.
Quando un processo muore, la sua struttura dati all’interno del kernel (il Process Control Block – PCB) viene mantenuta in memoria – occupando quindi una piccola quantità di risorse – finché il processo padre non esegue la systemcall di wait
.
Se i programmi in esecuzione nel nostro sistema sono scritti correttamente, dovrebbe essere molto difficile trovare processi zombie attivi. Nel signal handler per la SIGCHILD, infatti, il processo padre dovrebbe chiamare wait() sul PID del figlio in modo da collezionarne il codice di uscita.
Per vedere la lista di PID e il loro stato, possiamo utilizzare il comando ps
:
ps axo pid=,stat=
E filtrare l’output per cercare processi nello stato “Z”:
ps axo pid=,stat= | awk '$2~/^Z/ { print }'
Come rimuovere gli zombie?
Come detto poco sopra, è responsabilità del PPID rilasciare le risorse occupate dai processi figli ormai zombie, utilizzando la funzione di wait.
E se il padre non dovesse fare il proprio dovere? Oppure, se il padre morisse prima di rilasciare le risorse dei figli?
In Linux esiste un processo “speciale” chiamato processo di init a cui viene solitamente assegnato il PID 1. Questo processo è il primo programma avviato dal sistema operativo una volta che l’avvio del sistema è completato.
Le principali caratteristiche di questo programma sono le seguenti:
- Non deve mai morire
- Deve rilasciare le risorse occupate dai programmi.
Essendo il primo processo ad essere eseguito, init è l’unico ad avere PPID 0 (in Linux, il pid 0 è assegnato al kernel scheduler). Tutti gli altri processi in esecuzione nel nostro sistema sono quindi in qualche modo discendenti di questo processo. E’ possibile vedere una rappresentazione grafica ad albero della gerarchia dei processi utilizzando il comando pstree
.
Prendiamo ad esempio un processo padre P che spawna un processo figlio F.
F muore e P dovrebbe quindi reclamare le sue risorse. Prima di farlo però P incontra un errore fatale e termina la propria esecuzione in maniera imprevista. Il PPID di P, a questo punto, reclama le risorse di P.
E cosa accade alle risorse di F? Sembrerebbe trattarsi di un leak di risorse.
(Attenzione: un discorso simile potrebbe essere fatto anche nel caso in cui il processo P morisse prima di F, lasciandolo orfano!)
Per risolvere questo problema, Linux rimappa i processi orfani assegnandogli come PPID quello di init – ovvero 1 – che, tra i propri compiti principali, ha appunto quello di rilasciare le risorse dei processi zombie.
Per rispondere quindi alla domanda riguardo le risorse di F, sarà init a rilasciare a questo punto le risorse occupate da F.
Che problemi potrebbero portare i processi zombie?
Come detto in precedenza, di un processo zombie rimane solamente il PCB. Per poter reclamare le risorse di un processo, la chiamata alla syscall wait ritornerà al chiamante una struttura dati contenente alcune informazioni del figlio defunto, fra cui il suo PID.
Il Process ID è in Linux una risorsa limitata. Solitamente il numero massimo di PID in Linux è 32768. Questo significa che abbiamo un limite sulla quantità di programmi che possiamo avere in esecuzione in un dato momento.
Nonostante i processi in stato Zombie non siano in esecuzione, il loro PID non può essere riutilizzato finchè il loro PCB non viene distrutto.
E cosa succederebbe se finissimo i PID? Non potremmo aprire nuovi programmi! In tal caso, il modo più rapido (e sicuro) per risolvere il problema sarebbe effettuare un riavvio del sistema. [1]
Questo è ovviamente problematico, sopratutto per sistemi che non possono permettersi un riavvio.
Non a caso, alcuni virus puntano proprio alla PID exhaustion, il cui esempio più famoso è la forkbomb in bash:
:(){: |:&};:
Nel caso delle fork bomb, o se comunque non si riesce a capire chi rappresenti la causa del problema, il riavvio del sistema è il modo più rapido e indolore per pulire i PID.
E se non possiamo riavviare il sistema?
Se esiste un processo in particolare che ha un grande numero di processi zombie da reclamare che per qualche motivo non lo sta facendo, una soluzione è terminarlo, in modo che il processo di init
possa svolgere il proprio compito ed eseguire la pulizia del sistema.
Conclusione
Gli zombie sono dei processi morti le cui meta-risorse sono ancora mantenute nelle strutture del kernel. Il padre (o il processo di init per lui) deve eseguire una chiamata alla syscall wait
per permettere al kernel di eliminare definitivamente il processo defunto.
Che sia a causa di un problema di programmazione (bug) o perpetrato in maniera intenzionale (virus), la generazione di un elevato numero di processi zombie può rapidamente portare il sistema a terminare i Process IDs disponibili, causando quindi una negazione del servizio (DOS – Denial Of Service).
Se siamo fortunati, tramite programmi di monitoraggio (ps
, o pstree
ad esempio) è possibile risalire al processo padre che ha prodotto i processi zombie e non li sta reclamando: ucciderlo permetterà ad init di rilasciare le risorse occupate.
Salvo per la PID exhaustion, avere dei processi Zombie in un dato momento non è per forza evidenza di problemi – ma è comunque bene conoscerne i rischi e pericoli.
[1] E’ interessante notare come alcune funzionalità fondamentali, come ad esempio kill
o reboot, siano integrate direttamente nella bash. Questo perché se “almeno” dovessimo riuscire ad aprire un terminale bash, avremmo poi la possibilità di inviare segnali ad altri programmi (ad esempio SIGTERM per terminare), senza la necessità di eseguire un nuovo programma – che a causa della PID exhaustion sarebbe impossibile!