Prima di leggere: una domanda sulle lezioni precedenti
Nella lezione sui processi hai misurato che un programma può intercettare SIGTERM e sopravvivergli, e
che SIGKILL non si può rifiutare. Adesso la domanda: quando chiedi a systemd di fermare
un servizio, quale dei due manda? E se il servizio non si ferma, chi decide che cosa succede dopo?
Scrivi la risposta. Il punto di questa lezione è che systemd non è un altro modo di
lanciare programmi: è un contratto scritto su chi avvia un servizio, con quali limiti, che cosa fa se
cade, e quanto aspetta prima di troncare. Tutto quello che in uno script avresti dovuto programmare
a mano, qui si dichiara in una riga. E una riga sbagliata si nota molto meno di dieci righe sbagliate.
Il problema: il tuo programma e la macchina che lo ospita
Hai un programma che funziona e devi farlo girare su un server. Serve che parta all'accensione, che riparta se cade, che scriva i suoi log in un posto dove li ritrovi, che non possa toccare parti del sistema che non gli servono, e che si fermi in modo ordinato quando lo chiedi. Scritto a mano, con uno script e un lavoro periodico, è un lavoro che si fa male: ho visto più volte un servizio «gestito» da un lavoro che ogni minuto controlla se è vivo e nel dubbio ne avvia un secondo.
Qui scriverai un file di unità vero, lo avvierai, lo farai fallire, leggerai il suo log, e lo farai
riavviare da solo. Lo farai senza privilegi di amministratore, perché
systemd ha una istanza per utente, e per imparare va benissimo: le chiavi sono le
stesse, cambia solo chi la gestisce e dove stanno i file.
Un file di unità, riga per riga
Un file di unità è un file di testo diviso in sezioni fra parentesi quadre, con chiavi e valori.
Questo è il minimo che funziona, e sta in ~/.config/systemd/user/ per un servizio
dell'utente oppure in /etc/systemd/system/ per uno di sistema.
[Unit]
Description=Demo per la lezione su systemd
After=default.target
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/usr/bin/bash -c 'echo "avviato alle $(date -Is)"'
ExecStop=/usr/bin/bash -c 'echo "fermato alle $(date -Is)"'
[Install]
WantedBy=default.target
[Unit] descrive la cosa e le sue relazioni con le altre: After dichiara un
ordine, non una dipendenza. [Service] dice che cosa eseguire e come trattarlo.
[Install] serve soltanto a systemctl enable: dice a quale bersaglio
agganciarsi per partire da sé. Una unità senza [Install] si può avviare a mano e non si
può abilitare.
Type è la chiave che si sbaglia più spesso, e ne bastano tre valori per cominciare.
simple significa che il processo avviato è il servizio, e
systemd lo considera attivo appena lo ha lanciato. oneshot significa che il
comando finisce e va bene così, e con RemainAfterExit=yes l'unità resta marcata attiva
anche dopo. notify significa che il programma dirà lui quando è pronto, ed è il valore
corretto per un servizio che ha un tempo di avvio e di cui vuoi sapere quando è davvero pronto.
Dopo aver scritto il file serve systemctl --user daemon-reload, perché
systemd tiene in memoria la propria vista delle unità e non rilegge il disco da sé.
Dimenticarlo produce il sintomo più frustrante possibile: modifichi il file, riavvii il servizio, e
il comportamento non cambia.
$ systemctl --user daemon-reload
$ systemctl --user is-active lezione-demo.service
inactive
$ systemctl --user start lezione-demo.service
$ systemctl --user is-active lezione-demo.service
active
Lo stato, e il log che gli sta attaccato
Il comando systemctl status non è un riassunto decorativo: contiene il codice di uscita
dell'ultimo tentativo, il numero di processo, il consumo, e le ultime righe di log. Letto per intero
risponde a quasi tutte le domande.
$ systemctl --user status lezione-demo.service
● lezione-demo.service - Demo per la lezione su systemd
Loaded: loaded (/home/federicocalo/.config/systemd/user/lezione-demo.service; disabled; preset: enabled)
Active: active (exited) since Fri 2026-10-30 17:47:20 CET; 13ms ago
Process: 1181027 ExecStart=/usr/bin/bash -c echo "avviato alle $(date -Is)" (code=exited, status=0/SUCCESS)
Main PID: 1181027 (code=exited, status=0/SUCCESS)
Mem peak: 2.6M
CPU: 11ms
Due parole su quella riga Loaded: disabled significa che l'unità non
partirà all'accensione, indipendentemente dal fatto che adesso sia attiva. Confondere
attivo con abilitato produce il servizio che funziona finché nessuno riavvia la
macchina, che è il guasto più imbarazzante da spiegare.
I log vanno in journalctl, e il filtro per unità è l'opzione che userai sempre. Due
opzioni rendono l'uscita leggibile e riusabile: -o short-iso mette le date in formato
ISO, e --no-hostname toglie una colonna che su una macchina sola è rumore.
$ journalctl --user -u lezione-demo.service -o short-iso --no-hostname
2026-10-30T17:47:20+01:00 systemd[2047]: Starting lezione-demo.service - Demo per la lezione su systemd...
2026-10-30T17:47:20+01:00 bash[1181027]: avviato alle 2026-10-30T17:47:20+01:00
2026-10-30T17:47:20+01:00 systemd[2047]: Finished lezione-demo.service - Demo per la lezione su systemd.
Il programma non ha fatto niente per scrivere nel log: ha scritto sull'uscita standard, e
systemd l'ha raccolta. È una delle ragioni per cui un servizio gestito così non ha
bisogno di gestire da sé la rotazione dei propri file di log.
Esempio svolto: far fallire un servizio e leggere il verdetto
Il problema: capire come systemd riporta un guasto, perché la prima volta che un
servizio vero non parte vorrai saperlo già.
L'informazione disponibile: il codice di uscita del programma arriva a systemd, che lo
interpreta. Il ragionamento: se scrivo un'unità il cui comando esce con un codice diverso da zero,
vedo l'intero percorso dell'informazione, dal programma al log, e imparo a leggerlo su un caso che ho
costruito io invece che su un incidente.
[Unit]
Description=Unit con un errore tipico
[Service]
Type=simple
ExecStart=/usr/bin/bash -c 'echo "$HOME e la mia casa"; exit 3'
$ systemctl --user start lezione-rotta.service
$ systemctl --user is-active lezione-rotta.service
active
Qui c'è la prima cosa che vale la pena notare, e che fa perdere tempo a tutti: lo stato letto subito
dopo l'avvio dice active. Non è un errore, è un fatto sul tempo: il comando
start con Type=simple ritorna appena il processo è stato lanciato, non
quando ha finito. Lo stesso comando, un secondo dopo:
$ systemctl --user is-active lezione-rotta.service
failed
$ systemctl --user is-failed lezione-rotta.service
failed
$ systemctl --user show -p Result --value lezione-rotta.service
exit-code
$ journalctl --user -u lezione-rotta.service -o short-iso --no-hostname
... systemd[2047]: Started lezione-rotta.service - Unit con un errore tipico.
... bash[1182191]: /home/federicocalo e la mia casa
... systemd[2047]: lezione-rotta.service: Main process exited, code=exited, status=3/NOTIMPLEMENTED
... systemd[2047]: lezione-rotta.service: Failed with result 'exit-code'.
Il codice 3 compare nel log con un'etichetta, NOTIMPLEMENTED, che
systemd prende da una convenzione sui codici di uscita dei servizi. L'etichetta è
fuorviante se il tuo programma usa il 3 per un'altra cosa, come succede nello script
della lezione sul bash scripting, dove i codici li hai scelti tu. Il numero è il dato,
l'etichetta è un'interpretazione.
La decisione che prendo: per sapere se un servizio è partito bene, non leggo
is-active subito dopo start. Leggo systemctl show -p Result
dopo un'attesa, oppure uso Type=notify e lascio che sia il programma a dichiararsi
pronto. L'alternativa che ho scartato è aggiungere un'attesa fissa negli script di deploy: funziona
finché la macchina non è sotto carico, e quando sotto carico il servizio parte più lentamente, il
deploy dichiara un successo che non c'è.
La verifica che chiude il ragionamento è Result, che è un dato strutturato e non un
testo da interpretare: vale success, exit-code, timeout,
signal e pochi altri valori, e distingue le famiglie di guasto. L'errore che avrei
potuto commettere è cercare la parola «fail» nell'output di status: funziona, fino al
giorno in cui cambia la formulazione del messaggio o la lingua del sistema.
Il caso negativo: una lettera sbagliata che spegne una protezione
Questa unità vuole impedire al servizio di vedere la cartella dei file temporanei del sistema, che è una misura di isolamento elementare. Contiene un errore di battitura. Trovalo, e di' che cosa accade quando avvii il servizio.
[Unit]
Description=Errore di battitura in una chiave di sicurezza
[Service]
Type=oneshot
ExecStart=/usr/bin/bash -c 'echo pronto'
PrivateTemp=true
La chiave giusta è PrivateTmp, non PrivateTemp. E il servizio
parte senza nessun errore. Il log dice Finished, lo stato è quello
atteso, e l'isolamento non c'è. Misurato:
$ systemctl --user show -p PrivateTmp --value lezione-typo.service
no
$ sed -i 's/PrivateTemp=true/PrivateTmp=true/' lezione-typo.service
$ systemctl --user daemon-reload
$ systemctl --user show -p PrivateTmp --value lezione-typo.service
yes
Una lettera, e la protezione passa da yes a no senza che niente si lamenti
al momento dell'avvio. L'errore è plausibile perché PrivateTemp è esattamente la parola
che scriverebbe chi ragiona in italiano o in inglese corrente: la chiave vera usa l'abbreviazione del
nome della cartella.
Lo strumento che lo trova esiste, e si chiama systemd-analyze verify. Ma leggilo bene,
perché contiene una trappola che vale la pena conoscere:
$ systemd-analyze --user verify lezione-typo.service
lezione-typo.service:7: Unknown key 'PrivateTemp' in section [Service], ignoring.
$ echo $?
0
Lo strumento trova l'errore, lo stampa, e lascia codice di uscita zero. Chi lo mette
in una catena di controllo guardando solo il codice di uscita ottiene un verde falso: il controllo
gira, non segnala niente, e l'errore è passato. Il controllo giusto non è il codice di uscita, è la
presenza della stringa Unknown key nell'uscita, filtrata sul nome del tuo file perché
nell'uscita compaiono anche avvisi su unità di sistema che non hai scritto tu.
Un errore in un valore invece si comporta in modo diverso, e la differenza è utile:
Type=oneshoot produce Failed to parse Type=oneshoot, ignoring, cioè
systemd riconosce la chiave e rifiuta il valore. Quindi la regola è: un valore sbagliato
viene nominato, una chiave sbagliata viene ignorata, e le chiavi sbagliate sono le
più pericolose perché quasi tutte le chiavi che si sbagliano riguardano i limiti e l'isolamento.
Esercizio 1, guidato: un servizio che si rialza
Scrivi un'unità con Type=simple il cui comando stampa qualcosa, attende un secondo, e
poi esce con un codice diverso da zero. Aggiungi Restart=always e
RestartSec=1. Avviala, attendi sette secondi, e misura quante volte è stata riavviata e
quante righe ha scritto nel log. Criterio: hai due numeri letti dal sistema, non contati a mente, e
sai dire con quale comando hai ottenuto ciascuno.
Esercizio 2, progetto: metti in servizio un programma tuo
Prendi uno script che hai scritto nella lezione sul bash scripting, quello con i codici di uscita distinti, e mettilo in servizio come unità utente. I requisiti sono questi, e sono il contratto da soddisfare.
- Parte da sé quando accedi, e questo si verifica con
systemctl --user is-enabled. - Scrive nel giornale, e il suo output si legge con un solo comando filtrato sull'unità.
- Non vede la cartella temporanea del sistema, e la proprietà effettiva si legge con
systemctl --user show. - Se esce con un codice di errore, lo stato riporta la famiglia di guasto corretta.
- Il file passa il controllo delle chiavi sconosciute, verificato nel modo giusto e non col codice di uscita.
Criterio di successo: per ciascuno dei cinque requisiti hai il comando e il suo output come prova, e almeno uno dei cinque ti ha costretto a correggere il file.
Esercizio 3, trasferimento
La domanda d'apertura di questa lezione aveva una seconda parte: se il servizio non si ferma, chi decide che cosa succede dopo? Trova la risposta nella documentazione del sistema, non in rete, e di' quale chiave governa quel comportamento e quanto vale per default sulla tua macchina.
Soluzioni commentate
Esercizio 1. I due comandi sono
systemctl --user show -p NRestarts --value per i riavvii e
journalctl --user -u ... -o cat | grep -c per le righe. Sulla mia prova, dopo sette
secondi, i valori sono 3 riavvii e 4 righe scritte, e la differenza di uno
è la cosa da capire: la prima esecuzione non è un riavvio. Quattro esecuzioni, tre riavvii.
Perché contare a mente sarebbe sbagliato. Con RestartSec=1 e un comando che dura un
secondo, il calcolo a mente dà tre o quattro esecuzioni in sette secondi a seconda di come arrotondi,
e non hai modo di sapere quale delle due. Il contatore che systemd tiene è un dato, e
questo è il motivo per cui il criterio dell'esercizio chiedeva numeri letti e non calcolati. In più,
systemd ha un limite di riavvii in una finestra di tempo, governato da
StartLimitBurst e StartLimitIntervalSec: oltre quello non riavvia più e
marca l'unità come fallita, e un servizio che «ha smesso di riavviarsi» di solito ha incontrato quel
limite e non un guasto nuovo.
Esercizio 2. Il requisito che costringe quasi tutti a correggere il file è il primo,
e per due motivi distinti. Primo: senza la sezione [Install] con
WantedBy=default.target, systemctl --user enable non ha un bersaglio a cui
agganciare l'unità e il comando fallisce. Secondo: anche con la sezione corretta,
is-enabled risponde disabled finché non esegui enable, e
start non abilita niente. Sono due passaggi, e chi ne fa uno solo ha un servizio che
funziona fino al primo riavvio.
Sul terzo requisito, la prova che vale è il valore effettivo letto con show, non la
presenza della riga nel file. È la lezione del caso negativo applicata: il file dice quello che hai
scritto, show dice quello che systemd ha capito, e quando le due cose
divergono è la seconda che governa il comportamento. Sul quarto, la famiglia di guasto si legge da
Result: se il tuo script esce con 2 per argomento non valido, il valore
atteso è exit-code; se vedi timeout hai un problema diverso, cioè il
programma non è terminato affatto.
Esercizio 3. La risposta è la chiave TimeoutStopSec, documentata in
systemd.service(5). systemd manda prima il segnale dichiarato in
KillSignal, che per default è SIGTERM, attende quel tempo, e poi manda SIGKILL se il
processo è ancora vivo: è esattamente la sequenza che nella lezione sui processi ho descritto come
quella corretta, e qui è un valore configurabile invece di una buona abitudine. Il valore in vigore
sulla tua macchina si legge con systemctl show -p TimeoutStopUSec su un'unità qualsiasi,
ed è il modo giusto di rispondere: il default documentato può essere stato cambiato in un file di
configurazione del sistema, e il valore che conta è quello che la tua macchina riporta.
Che cosa adesso sai fare
Sai scrivere un file di unità con le tre sezioni, sai che daemon-reload serve perché
systemd non rilegge il disco da sé, e sai distinguere un servizio attivo da un servizio
abilitato. Sai leggere l'esito di un avvio da Result invece di cercare una parola in un
messaggio, e sai perché lo stato letto subito dopo start non è ancora informativo.
Sai che una chiave sbagliata viene ignorata in silenzio mentre un valore sbagliato viene nominato, e
sai che lo strumento di controllo riporta il problema lasciando codice di uscita zero: quindi sai
come costruire un controllo che non dia un verde falso. La verifica concreta: prendi un'unità di
sistema che gira sulla tua macchina e confronta tre chiavi del suo file con i valori che
systemctl show riporta davvero.