Prima di leggere: prova a prevedere
Un programma ignora il comando con cui provi a fermarlo. Lo hai mandato tre volte, e il processo è ancora lì. Secondo te esiste un modo di fermarlo che il programma non può rifiutare? E se esiste, perché non si usa sempre quello? Scrivi le due risposte prima di proseguire.
La seconda domanda è quella che separa chi sa gestire i processi da chi conosce un comando. La prima ha una risposta documentata, la seconda ha una risposta che riguarda i dati del tuo programma, e sbagliarla è il modo in cui si corrompe un database.
Il problema: un processo che non si ferma, o che non dovrebbe fermarsi
Il caso che vedrai più spesso è un programma che occupa la macchina e non risponde. Il caso che fa più danno è il suo opposto: un programma che fermi nel modo sbagliato e che lascia dietro un lavoro a metà. In mezzo c'è un terzo caso, quello che scopri tardi: un programma che continua a girare dopo che chi lo aveva avviato non c'è più.
Tutti e tre si capiscono conoscendo due cose, e non più di due: come il kernel descrive un processo, e che cosa può e non può fare un segnale. Il resto sono comandi che si cercano nel manuale.
Il terzo caso lo prendo da un incidente di questo progetto, documentato nelle sue note operative: uno script di traduzione avviato in sottofondo da un agente sopravvive all'agente, e un SIGTERM non lo ferma; serve un SIGKILL. Quello script, nelle sue condizioni di errore, cancella dei file, quindi il costo del non saperlo fermare si misura in file perduti.
Che cos'è un processo, per il kernel
Un processo è una voce in una tabella, con un numero che lo identifica, un numero che identifica chi
lo ha avviato, uno stato, e una pila di altri attributi. Tutto questo è leggibile come file, sotto
/proc, nella cartella che ha per nome il numero del processo. È da lì che
ps prende i dati: non ha una via privilegiata, legge gli stessi file che puoi leggere tu.
$ sleep 60 &
$ cat /proc/$!/comm
sleep
$ tr '\0' ' ' < /proc/$!/cmdline; echo
sleep 60
$ head -4 /proc/$!/status
Name: sleep
Umask: 0002
State: S (sleeping)
Tgid: 1180031
Due file meritano di essere distinti, perché la differenza salva una diagnosi.
comm contiene il nome breve del programma, troncato; cmdline contiene la
riga di comando completa con gli argomenti, separati da byte nulli invece che da spazi, per questo
serve tr per leggerla. Quando due processi hanno lo stesso nome e devi capire quale
fermare, la risposta sta in cmdline e non in comm.
Lo stato è una lettera, e le lettere hanno un significato preciso elencato nella pagina di manuale di
ps, nella sezione PROCESS STATE CODES. Le quattro che vedrai davvero sono
S, sonno interrompibile, cioè il processo sta aspettando qualcosa; R, in
esecuzione o pronto a esserlo; D, sonno non interrompibile, in genere in attesa di un
disco; e Z, terminato ma non ancora raccolto dal genitore.
La distribuzione reale sorprende la prima volta che la si guarda. Sulla mia macchina, misurata il
2026-10-30 alle 18:17 con ps -eo stat --no-headers | cut -c1 | sort | uniq -c | sort -rn
su 543 processi: 396 in S, 146 in I che sono thread del kernel inattivi,
3 in R, e 1 in Z. Quasi tutto quello che gira sulla tua macchina non sta
facendo niente: sta aspettando.
Lo stato D è quello di cui vale la pena conoscere la conseguenza: un processo in sonno
non interrompibile non riceve segnali, nemmeno quello che non si può rifiutare.
Quindi un processo bloccato su un disco guasto non si uccide, e non perché il comando sia sbagliato.
Si aspetta che il disco risponda o si riavvia la macchina.
Il confronto: il segnale che si può rifiutare e quello che no
Un segnale è una notifica che il kernel consegna a un processo. La maggior parte dei segnali si può
intercettare: il programma dichiara una funzione da eseguire quando arrivano, e decide lui che cosa
fare. Due segnali non si possono intercettare, e la pagina di manuale signal(7) lo dice
in una frase: «The signals SIGKILL and SIGSTOP cannot be caught, blocked, or ignored».
La differenza è misurabile in cinque righe. Questo script intercetta SIGTERM e dichiara di ignorarlo:
#!/bin/bash
trap 'echo " [sordo] ho ricevuto SIGTERM e lo ignoro"' TERM
echo " [sordo] in attesa, PID $$"
while true; do sleep 0.2; done
$ ./sordo.sh &
[sordo] in attesa, PID 1179025
$ kill -TERM 1179025
[sordo] ho ricevuto SIGTERM e lo ignoro
$ ps -o pid,stat,comm -p 1179025
PID STAT COMMAND
1179025 S sordo.sh
$ kill -KILL 1179025
/bin/bash: riga 24: 1179025 Ucciso ./sordo.sh
$ ps -o pid,stat,comm -p 1179025
PID STAT COMMAND
$ echo $?
1
SIGTERM arriva, il programma lo riceve, stampa il suo messaggio e resta vivo. SIGKILL non arriva al
programma: il kernel termina il processo senza consegnare niente, e ps non trova più la
voce, lasciando codice di uscita 1.
Ora la seconda domanda d'apertura, quella che conta. Se SIGKILL funziona sempre, perché non usarlo per primo? Perché la possibilità di intercettare SIGTERM è una funzionalità: è il modo in cui un programma chiude i file, scrive l'ultimo blocco su disco, chiude le connessioni e rilascia i lock. Con SIGKILL non esegue niente di tutto questo. Un database fermato con SIGKILL riparte facendo il recupero del giornale, se è scritto bene; se non lo è, riparte con dei dati incoerenti.
La sequenza che uso, e che è anche quella che systemd applica da sé: prima SIGTERM, poi
si attende un tempo ragionevole, e solo se il processo è ancora lì si manda SIGKILL. L'errore di
classificazione plausibile è pensare che kill -9 sia «il comando che funziona» e gli
altri siano tentativi: sono due operazioni diverse, una chiede e l'altra tronca.
Esempio svolto: la priorità si abbassa e non si rialza
Il problema: un lavoro di compilazione sta rallentando tutto il resto, e vuoi che continui ma occupi la macchina solo quando è libera. Poi vuoi rimettere le cose come stavano.
L'informazione disponibile: ogni processo ha un valore di cortesia, visibile nella colonna
NI di ps, che va da meno venti a diciannove. Più alto il numero, meno il
processo pretende. Il ragionamento: se posso cambiare quel numero su un processo già avviato, non
devo fermare la compilazione, e il comando che lo fa è renice.
$ sleep 60 &
$ ps -o pid,ni -p $!
PID NI
1180026 0
$ renice -n 15 -p 1180026
1180026 (process ID) old priority 0, new priority 15
$ ps -o pid,ni -p 1180026
PID NI
1180026 15
$ renice -n 0 -p 1180026
renice: failed to set priority for 1180026 (process ID): Permesso negato
La discesa funziona, la risalita no, sullo stesso processo, dallo stesso utente, un secondo dopo. Non è un bug: un utente senza privilegi può rendere i propri processi più cortesi, e non può tornare indietro, perché tornare indietro significherebbe prendersi risorse, e questo è riservato a chi ha i privilegi. Lo stesso vale in partenza:
$ nice -n 10 bash -c 'ps -o pid,ni,comm -p $$'
PID NI COMMAND
1180024 10 ps
$ nice -n -5 bash -c 'ps -o pid,ni,comm -p $$'
nice: warning: setpriority: Permission denied
PID NI COMMAND
1180025 0 ps
Con un valore positivo il comando riesce. Con un valore negativo emette un avviso e procede comunque, col valore originale: questo è il dettaglio che conta, perché un avviso non è un errore e il comando lascia codice zero. Uno script che chiede priorità alta e non controlla l'avviso crede di averla ottenuta.
La decisione che prendo: abbasso la cortesia di un lavoro in sottofondo con nice al
momento dell'avvio, e non conto di poterla rialzare. L'alternativa che ho scartato è avviare tutto a
priorità normale e correggere dopo in base al carico: è impraticabile proprio per
l'asimmetria misurata sopra. La verifica è la colonna NI letta dopo il cambio, non il
codice di uscita del comando, per il motivo dell'avviso.
Il caso negativo: un comando che uccide più di quello che credi
Hai un processo da fermare e non ne conosci il numero, solo una parte della riga di comando. La
soluzione comoda è pkill -f, che cerca nella riga di comando completa. Guarda questo
output e dimmi, prima di leggere il seguito, che cosa sarebbe successo se invece di
pgrep avessi scritto pkill.
$ pgrep -af "sleep 30"
1365607 /bin/bash -c ... pgrep -af "sleep 30" ...
1365608 sleep 30
Due corrispondenze, non una. La seconda è il processo che cercavo. La prima è
la shell che sta eseguendo il comando di ricerca, perché la stringa cercata compare
nella sua riga di comando, dato che il comando contiene la stringa. Con pkill -f avrei
ucciso la mia stessa sessione, e in uno script avrei ucciso lo script.
L'errore è plausibile per una ragione sgradevole: il più delle volte non succede, perché il
pattern non compare nella riga di comando di chi cerca. Succede quando il comando viene eseguito
attraverso una shell con -c, che è esattamente il modo in cui lo eseguono gli strumenti
di automazione. Quindi il difetto non si manifesta mentre provi a mano e si manifesta in produzione.
Lo riconosci da un sintomo caratteristico: un lavoro automatico che si interrompe da solo a metà,
senza errori nei log, proprio al passaggio in cui c'è un pkill. La difesa è in tre
mosse. Eseguire sempre prima pgrep -af e leggere l'elenco; usare un pattern che non
possa corrispondere a sé stesso; e dove possibile non cercare affatto, ma conservare il numero del
processo in un file al momento dell'avvio e fermarlo per numero.
Esercizio 1, guidato: un processo che non vuole morire
Scrivi uno script che intercetta SIGTERM, SIGINT e SIGHUP, stampa quale ha ricevuto, e continua a girare. Avvialo in sottofondo e prova a fermarlo con i tre segnali, uno per uno, annotando che cosa stampa e se sopravvive. Poi fermalo per davvero. Criterio: hai una tabella di quattro righe con segnale, numero, cosa è stato stampato, e se il processo è sopravvissuto, e sai dire quale riga non ha stampato niente e perché.
Esercizio 2, autonomo: chi è sopravvissuto alla sua shell
Sulla tua macchina, trova un processo il cui genitore non è più quello che lo ha avviato, cioè un processo adottato dopo la morte del genitore. Poi spiega perché succede e che cosa significa per la sua terminazione. Criterio di successo: hai un numero di processo, il numero del suo genitore attuale, e sai dire chi è quel genitore e per quale motivo quel processo gli è stato affidato.
Soluzioni commentate
Esercizio 1. La riga che non stampa niente è SIGKILL, e il motivo non è che il tuo
trap sia scritto male: è che SIGKILL non viene consegnato al programma. La pagina
signal(7) lo dichiara, e il modo di verificarlo è proprio questo esperimento. Se nella
tua tabella SIGKILL ha stampato qualcosa, hai un difetto nell'esperimento, non una scoperta: quasi
sempre è l'eco del messaggio della shell, che annuncia la terminazione, confuso con l'output dello
script.
Sui tre intercettabili, una cosa che l'esperimento rivela e che non si legge nei manuali: in bash,
una gestione registrata con trap viene eseguita dopo che il comando in
corso è terminato, non durante. Se il ciclo contiene un'attesa lunga, il messaggio compare in
ritardo, e un osservatore distratto conclude che il segnale non è arrivato. Per questo nello script
dell'esempio l'attesa è di due decimi di secondo: con un'attesa di trenta secondi lo stesso script
sembrerebbe sordo. L'ho misurato con uno script che registra l'istante di ogni evento, e il ritardo
è esattamente quello dell'attesa in corso:
[avvio] a 59.94
[mando SIGTERM a 00.94]
[trap] eseguito a 04.95
[sleep finito]
Il segnale è stato mandato un secondo dopo l'avvio e la gestione è partita quattro secondi più tardi, cioè alla fine dell'attesa di cinque secondi che lo script aveva in corso. Non un segnale perso: un segnale in coda.
Una precisazione su SIGHUP, perché è il segnale che spiega il caso dell'incidente citato all'inizio:
è quello che arriva quando il terminale che ti ha avviato scompare. Un programma che lo intercetta,
o che è stato avviato con nohup, non lo riceve e continua. È il motivo per cui un
processo avviato in sottofondo da un'automazione sopravvive all'automazione.
Esercizio 2. Il comando per cercarli è
ps -eo pid,ppid,comm --no-headers | awk '$2==1', cioè i processi il cui genitore ha
numero uno. Quel numero uno è il primo processo avviato dal sistema, che su questa macchina è
systemd, verificabile con ps -o comm= -p 1. Quando un processo muore
lasciando figli vivi, i figli non muoiono: vengono affidati al primo processo, che diventa il loro
genitore. Su questa macchina ce ne sono 44, contati con quel comando il 2026-10-30, e la maggior parte sono servizi avviati
correttamente, non orfani per incidente.
La conseguenza per la terminazione, che è la parte che l'esercizio chiede, è doppia. Primo: quel processo non riceverà SIGHUP quando chiudi il tuo terminale, perché il tuo terminale non c'entra più niente con lui. Secondo: quando termina, il suo stato di uscita viene raccolto dal primo processo, che lo raccoglie sempre, e questo è il motivo per cui un processo adottato non diventa mai uno zombie permanente. Uno zombie permanente richiede un genitore vivo che non raccoglie i figli, e quindi non si risolve uccidendo lo zombie, che è già morto: si risolve agendo sul genitore.
Che cosa adesso sai fare
Sai leggere i dati di un processo dalla sua cartella sotto /proc, e sai distinguere il
nome breve dalla riga di comando completa quando due processi si chiamano uguale. Sai interpretare la
lettera dello stato, e sai che un processo in sonno non interrompibile non si uccide. Sai che SIGTERM
si può intercettare e SIGKILL no, e sai perché non vuoi cominciare dal secondo.
Sai che la priorità di un processo si abbassa e non si rialza senza privilegi, e che il comando che
fallisce nel rialzarla ti avvisa invece di fermarsi. Sai perché pkill -f può uccidere
chi lo invoca. La verifica concreta: prendi un servizio che gira sulla tua macchina, trova il suo
numero di processo, leggi il suo stato e la sua riga di comando completa, e scrivi la sequenza con
cui lo fermeresti in sicurezza.