1 26/04/2026 10 min

Quando un processo zombie è un sintomo, non il guasto

Su Debian 10 un processo zombie non sta consumando CPU, non sta eseguendo istruzioni e non si “uccide” nel senso classico del termine. È già morto: è rimasto solo l’entry nella tabella dei processi perché il processo padre non ha ancora letto il suo stato di uscita con una chiamata di attesa, in pratica wait() o una delle sue varianti. Per questo motivo il punto non è “terminare lo zombie”, ma capire perché il padre non lo sta raccogliendo e, se serve, sistemare il padre o il servizio che lo ha generato.

La distinzione conta molto in produzione. Se vedi più zombie del normale, il problema vero può essere un demone che non gestisce bene i figli, uno script che fa fork senza pulizia, un supervisore mal configurato, oppure un’applicazione che perde il controllo dei processi figlio. Lo zombie è solo il residuo visibile. La correzione efficace è quasi sempre a monte.

Come riconoscere uno zombie su Debian 10

Il segnale più semplice è lo stato Z o defunct nei tool standard. Con ps puoi verificare subito se il processo è davvero zombie e chi è il padre che dovrebbe raccoglierlo.

ps -eo pid,ppid,stat,cmd | awk '$3 ~ /Z/ {print}'

Output atteso: una o più righe con STAT che contiene Z oppure defunct nel comando. Se il campo PPID è 1, significa che il padre originario è già terminato e il processo è stato adottato da systemd o dall’init, che in teoria dovrebbe raccoglierlo. Se invece il padre è un demone applicativo ancora vivo, il problema è lì.

Un controllo utile è questo:

ps -o pid,ppid,stat,etime,cmd -p <PID_ZOMBIE>
ps -o pid,ppid,stat,etime,cmd -p <PPID>

Il primo comando conferma lo stato del figlio; il secondo ti dice se il padre è ancora in esecuzione, da quanto tempo, e con quale comando. Se il padre è un processo noto del servizio, hai già la pista giusta. Se è un wrapper o uno script custom, spesso la causa è un bug di gestione dei segnali o dei figli.

Perché non puoi “killare” uno zombie

Uno zombie non risponde a SIGKILL, SIGTERM o ad altri segnali, perché non è più in esecuzione. La sua voce nella tabella dei processi esiste solo per conservare il codice di uscita finché il padre non lo legge. Se provi a fare kill -9 su un PID zombie, il comando può anche sembrare accettato, ma non cambierà nulla: il processo è già terminato. Questo è uno dei punti che genera più confusione quando si lavora al volo su una macchina con carico alto.

La soluzione reale è sempre una di queste: fare in modo che il padre chiami correttamente wait(), riavviare il padre se è bloccato o buggato, oppure correggere il servizio che genera figli non gestiti. Solo in casi limite, quando il padre è irrecuperabile e non vuoi aspettare, lo si termina per forzare l’adozione da parte di PID 1 e la successiva raccolta degli zombie. Anche lì, però, stai agendo sul padre, non sullo zombie.

Verificare il layer giusto: processo, servizio o applicazione

Prima di toccare anything, classifica il problema. Se gli zombie sono pochi e sporadici, può essere un effetto collaterale innocuo. Se aumentano nel tempo, o compaiono in coincidenza con errori applicativi, il rischio è che il processo padre stia degradando. Su Debian 10 conviene partire dal servizio systemd, poi passare al log dell’applicazione, poi ai figli.

Un flusso pratico è questo:

systemctl status <servizio>
journalctl -u <servizio> -n 100 --no-pager
ps -eo pid,ppid,stat,cmd | awk '$3 ~ /Z/ {print}'

Se il servizio è attivo ma i log mostrano crash, riavvii, timeout o errori di fork, il padre potrebbe non riuscire a gestire correttamente i figli. Se invece il padre è un worker pool, cerca saturazione, deadlock o file descriptor esauriti. In molti casi gli zombie sono la punta dell’iceberg di un problema di limiti di sistema o di gestione impropria dei segnali.

Diagnosi rapida: tre ipotesi ordinate per probabilità

1) Il processo padre non chiama wait() o lo fa male. È la causa più comune. La falsifichi in pochi minuti controllando il tipo di servizio, i log e la persistenza degli zombie dopo un riavvio del padre. Se il padre resta vivo e gli zombie continuano a comparire, l’ipotesi regge.

2) Il padre è bloccato o in stato degradato. Un processo può restare vivo ma incapace di raccogliere i figli se è bloccato su I/O, in attesa di rete, o saturo di richieste. Lo falsifichi guardando top, htop, strace o i log: se il padre è in sleep prolungato, in attesa su syscall o con errori ripetuti, il problema non è lo zombie ma il blocco del padre.

3) Il servizio genera figli effimeri in modo aggressivo. Tipico di script, wrapper, agent o plugin. Si falsifica osservando un aumento rapido di PID in breve tempo e una correlazione con specifici eventi o job schedulati. Se il pattern è ripetibile, non è rumore: c’è un comportamento di fork non governato.

Strumenti utili su Debian 10

Per un controllo veloce, ps e top bastano. Per una diagnosi più precisa, pstree mostra la relazione padre-figlio in modo leggibile, mentre lsof e strace aiutano a capire se il padre è bloccato. Ecco una sequenza pratica.

pstree -ap | less
ps -eo pid,ppid,stat,cmd | grep ' Z '
strace -p <PPID> -f -tt -s 128

pstree -ap ti fa vedere chi ha generato chi. Se trovi un padre con decine di figli zombie, hai una firma chiara. strace è più invasivo, quindi usalo con criterio: non è il primo passo su un sistema già sotto stress, ma è molto utile per capire se il padre è fermo su wait4, read, poll o altro. Se non puoi usare strace in produzione, passa almeno da journalctl e dalle metriche del servizio.

Se vuoi vedere se il numero di zombie sta crescendo, una misura grezza ma efficace è questa:

watch -n 2 "ps -eo stat | grep -c '^Z'"

Non è una metrica da dashboard, ma dà subito il trend. Se il conteggio sale con il tempo, non stai osservando un residuo innocuo. Stai vedendo un meccanismo che non smaltisce i figli.

Intervento minimo e reversibile

La regola è semplice: non agire sullo zombie, agisci sul padre con il minimo impatto possibile. Se il padre è un servizio systemd, il primo tentativo ragionevole è un riavvio controllato del servizio, dopo aver verificato dipendenze e finestra di impatto. Se il servizio è critico, valuta prima un failover o un nodo secondario. Il blast radius può essere ampio: riavviare il padre può interrompere sessioni, code, socket aperti o job in corso.

Flusso consigliato:

  • Identifica il padre con ps -o pid,ppid,stat,cmd -p <PID_ZOMBIE>.
  • Controlla il servizio associato con systemctl status <servizio> e i log recenti con journalctl -u <servizio> -n 50 --no-pager.
  • Se il padre è degradato ma ancora sano, esegui un riavvio del solo servizio interessato: systemctl restart <servizio>.
  • Rivisita ps -eo pid,ppid,stat,cmd | awk '$3 ~ /Z/ {print}' e verifica che gli zombie spariscano o non aumentino più.
  • Se il servizio non recupera, passa a un rollback della release recente o a una correzione del codice/configurazione che gestisce i figli.
  • Se il padre è uno script lanciato a mano, la correzione va fatta nel modo in cui lo script gestisce i processi figli: trap dei segnali, attesa dei PID, chiusura ordinata. Se il padre è un demone legacy, controlla se esistono opzioni per il foreground mode o per integrare correttamente il servizio in systemd. Su Debian 10, far girare un processo come servizio nativo è spesso più affidabile che affidarsi a wrapper artigianali.

    Quando il problema è nel codice o nello script

    Se gestisci un’applicazione propria, la causa probabile è nella logica di fork/exec. Un padre che crea figli deve sempre raccoglierne l’uscita. In C, questo significa gestire correttamente SIGCHLD e chiamare waitpid(). In shell, significa non lasciare processi in background senza controllo. In PHP, Python, Node o altri runtime, la questione cambia sintassi ma non sostanza: ogni child va seguito o delegato a un supervisore che lo faccia al posto tuo.

    Esempio C minimale, giusto per fissare il punto:

    signal(SIGCHLD, SIG_IGN);

    Non è una soluzione universale e non va usata alla cieca, ma mostra il principio: il padre deve occuparsi dei figli. In molti casi è più corretto usare waitpid() in un loop controllato, soprattutto se devi leggere stato e codice di uscita. Se non hai il codice sorgente sotto mano, il controllo si sposta sul supervisore, sui parametri del servizio e sui log dell’applicazione.

    Segnali che indicano un problema più serio

    Gli zombie occasionali, uno o due, in un sistema con molti job brevi non sono automaticamente un allarme rosso. Diventano un problema quando noti uno di questi segnali: crescita progressiva del numero, correlazione con errori applicativi, saturazione di PID, ritardi nel servizio, riavvii frequenti del padre, oppure comparsa insieme ad altri sintomi come memoria esaurita, file descriptor pieni o I/O bloccato.

    Su Debian 10 puoi incrociare i dati con:

    uptime
    free -h
    df -h
    ulimit -a
    journalctl -p err -b --no-pager

    Se il sistema è sotto pressione su memoria o disco, il padre può comportarsi male e accumulare figli non gestiti. Se i log mostrano ENOMEM, Too many open files, timeout o crash ripetuti, la correzione deve coprire la causa primaria, non limitarsi a ripulire gli zombie presenti. Altrimenti il fenomeno si ripresenterà appena il carico torna normale.

    Prevenzione operativa su Debian 10

    La prevenzione migliore è far girare i processi nel modo giusto fin dall’inizio. Se amministri servizi custom, preferisci unità systemd ben definite, con Type appropriato, gestione dei segnali chiara e logging su journal. Evita wrapper che avviano processi in background senza un controllo esplicito del ciclo di vita. Se usi applicazioni già pronte, verifica che il vendor supporti il modello di esecuzione che stai usando.

    In ambito operativo, tieni d’occhio almeno tre cose: numero di processi zombie, frequenza di riavvio del servizio e anomalie nei log. Anche un controllo periodico via cron o monitoring può bastare per intercettare il problema prima che diventi rumore di fondo. Una soglia ragionevole dipende dal ruolo della macchina, ma la tendenza conta più del valore assoluto.

    Un esempio di check leggero da integrare in monitoring:

    #!/bin/sh
    count=$(ps -eo stat | grep -c '^Z')
    [ "$count" -gt 0 ] && echo "ZOMBIE_COUNT=$count"

    Non è un sistema di osservabilità completo, ma è sufficiente per far scattare un alert iniziale. Da lì puoi mettere correlazione con il servizio, il carico e gli errori recenti. La parte importante è non aspettare che gli zombie diventino la causa apparente di un problema che in realtà nasce altrove.

    Checklist operativa finale

    Se devi intervenire adesso, segui questa sequenza corta e sicura:

  • Conferma lo stato zombie con ps -eo pid,ppid,stat,cmd | awk '$3 ~ /Z/ {print}'.
  • Individua il padre con PPID e verifica il servizio con systemctl status.
  • Leggi gli ultimi log con journalctl -u <servizio> -n 100 --no-pager.
  • Se il padre è degradato, riavvia il servizio in modo controllato e verifica che il numero di zombie non cresca più.
  • Se il problema torna, correggi gestione dei figli, supervisione o codice applicativo; non inseguire il singolo zombie.
  • Assunzione: Debian 10 con systemd attivo, accesso root o sudo, e un servizio applicativo identificabile dal PPID dello zombie.