SSD Nodes Learn 🎉 VPS da $5.50/mese
Guide Matt ConnorDi Matt Connor

systemd Type=: simple, forking o notify: quale scegliere

L'unità è active ma il demone è terminato? Scegli Type= tra simple, exec, forking, oneshot e notify e individua il vero PID principale.

Perché systemd indica un'unità come attiva quando il processo è terminato

Un'unità di servizio systemd resta active finché è in esecuzione l'unico processo che systemd considera il processo principale, e Type= nella sezione [Service] determina quale processo sia. Se si sceglie il valore errato, systemd finisce per monitorare un wrapper shell o un processo padre di breve durata, mentre il demone che interessa termina all'interno della stessa unità. L'unità descrive correttamente lo stato del processo che le è stato indicato di monitorare.

Cambiare la policy di riavvio non risolve il problema. Restart= interviene quando il processo principale termina, quindi Restart=always non viene mai eseguito finché il PID principale, cioè l'identificatore del processo, appartiene a qualcosa che è ancora in esecuzione. Correggere prima Type=. Il comportamento di systemd dopo la reale terminazione del processo principale è una decisione separata, descritta nella guida a Restart= e RestartSec=.

Che cosa determina realmente Type=

Ogni valore di Type= risponde contemporaneamente a due domande. Quando può systemd considerare avviata questa unità e quale processo è quello principale.

La prima risposta controlla l'ordine di avvio. Un'unità che indica la tua in After= attende che systemd consideri avviata la tua unità. Un Type= che segnala «avviato» troppo presto consente alle unità dipendenti di essere eseguite prima che il tuo servizio possa rispondere.

La seconda risposta controlla il monitoraggio. systemd inserisce ogni processo avviato da un'unità in un cgroup (control group), una funzionalità del kernel che raggruppa i processi per limitarli e terminarli insieme. Il cgroup è il meccanismo con cui systemctl stop esegue la pulizia: KillMode= ha come valore predefinito control-group, quindi l'arresto di un'unità invia un segnale a ogni processo al suo interno. Il PID principale è un concetto più ristretto. È l'unico processo la cui terminazione conclude l'unità e il cui codice di uscita diventa il risultato dell'unità. La confusione nasce quando si interpreta il cgroup come se fosse il PID principale.

Type=simple segnala l’avvio prima dell’esecuzione del binario

Type=simple è il valore predefinito quando è impostato ExecStart= e non è presente né Type=BusName=. systemd crea il processo, considera immediatamente avviata l’unità e tratta quel processo come PID principale. Le unità dipendenti iniziano subito, prima ancora che il binario del servizio venga eseguito.

Questo ultimo dettaglio spiega un problema comune. Un errore nel percorso ExecStart= produce comunque un job di avvio completato correttamente; il fallimento arriva un momento dopo, quando l’esecuzione non riesce. systemd registra questo caso con il codice di uscita 203, che nella propria tabella è indicato come EXEC e definito come errore nell’esecuzione del binario del servizio. Pertanto, il completamento senza errori di systemctl start non dimostra che il binario esista.

Usare simple per un programma che rimane in foreground e non passa autonomamente in background. Questa configurazione copre la maggior parte dei daemon moderni e quasi tutti i programmi scritti autonomamente.

Type=exec attende che il programma venga effettivamente avviato

Type=exec è simple con un passaggio aggiuntivo. systemd considera l'unità avviata solo dopo che sia il fork sia l'esecuzione del binario sono riusciti. Un binario mancante o un User= che non può essere risolto ora fanno fallire direttamente il job di avvio, invece di segnalare il successo e fallire silenziosamente un momento dopo.

Type=exec è stato introdotto in systemd 240, quindi è disponibile in tutte le distribuzioni server attuali. Ubuntu 24.04 include systemd 255 e Debian 13 include systemd 257, ad agosto 2026. Verifica la versione installata con systemctl --version.

Il costo è un passaggio di sincronizzazione aggiuntivo durante l'avvio. Il vantaggio è un codice di uscita affidabile restituito da systemctl start. Per un programma in primo piano, preferisci exec a simple.

Type=forking e come si perde il PID principale

Type=forking indica a systemd che il processo in ExecStart= creerà un processo figlio e poi terminerà intenzionalmente. systemd attende la terminazione del primo processo e solo dopo considera avviata l'unità. Il processo figlio rimasto è il daemon.

La difficoltà riguarda l'identità del processo. Il processo avviato da systemd non esiste più, quindi systemd deve determinare quale processo rimasto sia quello principale. Impostare PIDFile= sul file scritto dal daemon, normalmente un percorso sotto /run, consente a systemd di leggere il PID dal file. systemd verifica inoltre che il PID contenuto nel file si riferisca a un processo già appartenente a questo servizio. In questo modo un file obsoleto che indica un processo non correlato viene rifiutato, anziché essere considerato attendibile.

Senza PIDFile= si applica GuessMainPID=, il cui valore predefinito è yes. Questa deduzione è affidabile solo quando il servizio si stabilizza in un singolo processo. Il manuale indica chiaramente il limite: se il daemon è composto da più di un processo, la deduzione può essere errata e il rilevamento degli errori smette di funzionare. Un'unità può anche ritrovarsi con un PID principale pari a 0; in tal caso systemd non ha alcun processo da supervisionare.

La maggior parte dei daemon che creano processi figlio dispone anche di un'opzione per restare in primo piano. Usare questa opzione con Type=exec e rimuovere la riga PIDFile=. Meno componenti significano meno possibilità di perdere il PID.

Type=oneshot per le operazioni che terminano

Type=oneshot presuppone che il processo venga eseguito e termini. systemd considera l'unità avviata solo dopo la sua terminazione, quindi oneshot è la scelta corretta per qualsiasi operazione che un'altra unità debba attendere. È anche il valore predefinito implicito quando un'unità non specifica né Type=ExecStart=.

A oneshot sono associati due comportamenti specifici. È l'unico tipo che accetta più di una riga ExecStart=, che viene eseguita in ordine. Inoltre, per impostazione predefinita, il timeout di avvio è disabilitato. Di conseguenza, un oneshot che resta bloccato attende indefinitamente, a meno che non si imposti esplicitamente TimeoutStartSec=.

Dopo la terminazione del processo, l'unità torna allo stato inactive. RemainAfterExit=yes la mantiene active senza alcun processo in esecuzione. Questo è il comportamento intenzionale descritto dal sintomo all'inizio di questa pagina. È corretto quando il compito dell'unità consiste nel lasciare uno stato persistente invece di mantenere un processo in esecuzione, ad esempio caricando un ruleset del firewall o avviando uno stack di container. È il modello usato da uno stack Docker Compose che torna attivo dopo un riavvio, in cui l'unità esegue il comando compose, termina e resta active perché i container avviati continuano a funzionare dopo la sua terminazione. Un'unità oneshot è anche il tipo di unità attivato da una pianificazione; questo è l'altro aspetto di eseguire un'operazione con un timer systemd invece di cron.

Type=notify consente al servizio di indicare quando è pronto

Type=notify sposta la decisione al servizio. systemd mantiene aperto il job di avvio finché il processo non invia READY=1 tramite un socket Unix il cui percorso viene ricevuto nella variabile d’ambiente NOTIFY_SOCKET. L’interfaccia C è sd_notify(3) e molti server la supportano già.

Questa è la risposta corretta alla domanda «è stato avviato?». simple e exec segnalano l’avvio prima che il servizio abbia letto la configurazione o aperto il socket in ascolto. Un’unità dipendente può quindi avviarsi troppo presto e fallire la prima connessione. notify segnala l’avvio nel momento in cui il servizio stesso dichiara di essere pronto.

systemd accetta il messaggio soltanto dal processo principale. Questo è il significato di NotifyAccess=main, che è implicato da Type=notify. Se il messaggio proviene da un processo figlio o da un processo di supporto, impostare NotifyAccess=all. Uno script shell può chiamare systemd-notify --ready, ma il comando viene eseguito come processo separato di breve durata. Sono quindi necessari NotifyAccess=all e systemd potrebbe non riuscire ad attribuire un messaggio il cui processo mittente è già terminato. Un servizio che implementa direttamente il protocollo è più affidabile.

È utile conoscere anche due impostazioni correlate. Type=notify-reload, disponibile a partire da systemd 253, estende lo stesso handshake alle operazioni di ricaricamento. In questo modo systemctl reload restituisce il controllo quando il servizio segnala che il ricaricamento è terminato, invece di restituirlo quando viene inviato il segnale. WatchdogSec= chiede a un servizio che supporta le notifiche di inviare un messaggio di keep-alive a intervalli regolari. systemd considera una scadenza non rispettata come un errore.

Type=dbus e Type=idle

Type=dbus attende che il servizio registri un nome su D-Bus, il bus di messaggi che i servizi di sistema e desktop usano per comunicare tra loro. Richiede BusName= e diventa il valore predefinito non appena viene impostato BusName=. Usatelo solo per un servizio che registra effettivamente un nome sul bus.

Type=idle si comporta come simple, ma ritarda l'avvio del programma finché i job accodati non sono stati distribuiti, con un limite massimo di cinque secondi. Serve a evitare che l'output della console durante il boot si sovrapponga ai messaggi di stato. Non è uno strumento di ordinamento e non va usato per un servizio normale.

Perché uno script wrapper lascia systemd sul PID errato

Ecco la struttura che produce il sintomo originale.

[Service]
Type=simple
ExecStart=/opt/app/run.sh
#!/bin/bash
export APP_ENV=production
/opt/app/bin/server --config /etc/app.yaml &
/opt/app/bin/exporter --port 9101

systemd registra la shell come PID principale. La shell rimane attiva mentre exporter viene eseguito in primo piano. Se server termina, la shell non se ne accorge: il PID principale è ancora attivo, l'unità è ancora active e Restart= non ha nulla su cui intervenire. Entrambi i processi rimangono per tutto il tempo nel cgroup dell'unità, quindi systemctl stop continua a eseguire correttamente la pulizia. Il problema riguarda la supervisione, non la pulizia.

La correzione dipende dal numero effettivo di processi a esecuzione prolungata dell'unità.

Se ce n'è uno, sostituisci la shell con quel processo.

#!/bin/bash
export APP_ENV=production
exec /opt/app/bin/server --config /etc/app.yaml

exec sostituisce la shell con il programma indicato e mantiene lo stesso PID. Il PID registrato da systemd appartiene quindi al demone. Ancora meglio, rimuovi il wrapper. Environment= e EnvironmentFile= trasmettono le variabili, mentre ExecStartPre= esegue il passaggio di configurazione. In questo modo systemd può avviare direttamente il demone e conoscerne il PID per costruzione.

Se sono due, nessun singolo PID rappresenta l'unità. Separali in due unità e definisci l'ordine con After= e Wants=. Un'unità per processo è la struttura che systemd supervisiona meglio ed è l'unico modo per assegnare a ciascun processo un comportamento di riavvio indipendente.

Cosa cambia con ExitType=cgroup

ExitType= è stato aggiunto in systemd 250. Il valore predefinito è main: l'unità viene considerata arrestata quando termina il processo principale. Con ExitType=cgroup, l'unità viene considerata in esecuzione finché resta attivo un processo nel relativo cgroup.

[Service]
Type=simple
ExitType=cgroup
ExecStart=/opt/app/launcher

Questo risolve un problema specifico. Un launcher che avvia il lavoro effettivo e poi termina farebbe sì che, con ExitType=main, systemd consideri l'unità arrestata e termini i processi rimasti. Con ExitType=cgroup, invece, l'unità segue l'intero gruppo.

È importante chiarire cosa non risolve. ExitType=cgroup mantiene attiva un'unità finché è in esecuzione almeno un processo. Un'unità che contiene due daemon rimane quindi attiva dopo la terminazione di uno dei due. Questo risolve il caso del launcher. Non trasforma un'unità in un supervisore di più processi indipendenti. Inoltre, ExitType= non può essere combinato con Type=oneshot.

Il cgroup è anche il livello a cui viene applicata la contabilizzazione delle risorse. Di conseguenza, limiti come MemoryMax= e CPUQuota= si applicano a ogni processo avviato dall'unità, indipendentemente da ciò che Type= indica sul PID principale. Questo aspetto è trattato in limitare memoria e CPU di un servizio con systemd.

Come individuare il processo effettivamente monitorato da systemd

Eseguire questi controlli, nell'ordine indicato, sull'unità in fase di debug. Prima verificare ciò che systemd ha caricato, quindi ciò che sta monitorando e infine confrontare i risultati con la tabella dei processi.

systemctl cat app.service
systemctl show -p Type,ExitType,GuessMainPID,PIDFile,MainPID,RemainAfterExit app.service

systemctl cat stampa il file dell'unità insieme a tutti i drop-in applicabili, così si verifica ciò che systemd ha caricato invece del file che si ricorda di aver modificato. systemctl show stampa i valori effettivi, inclusi i valori predefiniti che non sono stati annotati. Prima di procedere, prendere nota del valore di MainPID.

systemd-cgls --unit=app.service
ps -o pid,ppid,stat,etime,args -p "$(systemctl show -p MainPID --value app.service)"

systemd-cgls elenca tutti i processi nel cgroup dell'unità. La riga ps descrive l'unico processo supervisionato da systemd. Leggere insieme queste due informazioni. Un MainPID pari a 0 significa che systemd non ha alcun processo da monitorare. Un MainPID che risolve in una shell mentre il cgroup contiene anche il daemon indica il caso del wrapper descritto sopra. Un cgroup con più processi del previsto indica la presenza di un launcher o di un daemon che esegue il fork.

systemctl status app.service
journalctl -u app.service -b

systemctl status stampa insieme la riga dello stato e l'albero dei cgroup, fornendo spesso entrambe le risposte in un'unica operazione. journalctl -u limitato a questo boot con -b mostra gli eventi di avvio e arresto registrati da systemd per l'unità, inclusi i codici di uscita rilevati. Se il daemon scrive in un proprio file di log invece che nel journal, leggere anche quel file, perché systemd può registrare soltanto ciò che gli arriva.

Quando si modifica Type=, eseguire il reload e il restart.

systemd-analyze verify /etc/systemd/system/app.service
sudo systemctl daemon-reload
sudo systemctl restart app.service

systemd-analyze verify analizza il file e segnala le impostazioni che non può accettare. daemon-reload fa rileggere a systemd i file delle unità dal disco. Una modifica a Type= non viene applicata a un'unità già in esecuzione, quindi il restart è necessario.

Quindi verificare la modifica. Prendere il PID del processo effettivamente interessato da systemd-cgls e terminarlo. Eseguire subito dopo systemctl is-active app.service. Se Type= è corretto, l'unità esce dallo stato active. Se rimane active, systemd sta ancora monitorando qualcos'altro.

Quale Type= di systemd usare

  • Un programma che resta in primo piano: Type=exec.
  • Un programma che supporta la notifica di disponibilità: Type=notify, e notify-reload se conferma anche i reload.
  • Un daemon che deve necessariamente passare in background: Type=forking con PIDFile=, oppure l'opzione per restare in primo piano con Type=exec.
  • Uno script che esegue un'attività e termina: Type=oneshot, oltre a RemainAfterExit=yes quando l'obiettivo è lasciare uno stato persistente.
  • Un launcher che termina mentre i processi figli continuano a essere eseguiti: Type=simple con ExitType=cgroup.

Se non sai quale usare per un daemon di terze parti, leggi prima il relativo unit file fornito dal pacchetto. Eseguendo systemctl cat su un'unità distribuita insieme alla distribuzione puoi vedere la scelta Type= fatta dal progetto upstream. Questa configurazione è stata verificata da più persone rispetto alla tua.

FAQ

Perché la mia unità systemd resta attiva quando il processo è terminato?

Perché il processo che systemd considera principale è ancora in esecuzione. systemd controlla un solo PID per servizio, scelto in base a Type=, invece di controllare ogni processo nel cgroup dell'unità. La causa più comune è uno script wrapper avviato con Type=simple: la shell è il PID principale, quindi l'unità resta attiva quando termina un demone avviato in background dalla shell. Esegui systemctl show -p MainPID app.service, quindi elenca il cgroup dell'unità con systemd-cgls --unit=app.service e confronta i due risultati.

Qual è la differenza tra Type=simple e Type=exec?

Type=simple considera l'unità avviata non appena systemd ha creato il processo, prima che il binario venga eseguito. Di conseguenza, un percorso errato in ExecStart= produce comunque un job di avvio completato correttamente, seguito da un errore. Type=exec attende che l'esecuzione abbia avuto esito positivo, quindi l'errore viene segnalato direttamente dal job di avvio. Entrambi considerano lo stesso processo come PID principale. Type=exec richiede systemd 240 o versione successiva.

Serve ancora PIDFile= con Type=forking?

Sì, ogni volta che il demone scrive un file PID. In sua assenza, systemd ricorre a GuessMainPID=, che è una stima ed è affidabile solo per un servizio che si stabilizza in un singolo processo. Quando la stima è errata o non è possibile formularla, il rilevamento degli errori e il riavvio automatico smettono di funzionare per quell'unità. Imposta PIDFile= sul percorso esatto in cui il demone scrive il file, normalmente sotto /run.

Quando devo usare RemainAfterExit=yes?

Quando lo scopo dell'unità è modificare lo stato del sistema invece di mantenere in esecuzione un processo. Un'unità Type=oneshot che carica le regole del firewall o avvia uno stack di container termina non appena completa il proprio lavoro. Senza RemainAfterExit=yes, l'unità diventa inattiva e systemctl stop non ha nulla da arrestare né un modo per eseguire una pulizia ExecStop=. Con questa impostazione, l'unità resta attiva senza processi. In questo caso è il comportamento previsto.

La modifica di Type= richiede un daemon-reload?

Sì, e richiede anche il riavvio dell'unità. systemctl daemon-reload fa rileggere a systemd i file delle unità presenti su disco, ma un'istanza già in esecuzione conserva il valore di Type= con cui è stata avviata. Esegui sudo systemctl daemon-reload e poi sudo systemctl restart app.service prima di eseguire i test. In caso contrario, stai ancora osservando il comportamento di supervisione precedente.