SSD Nodes Learn 🎉 VPS da $5.50/mese
Guide Matt ConnorDi Matt Connor · Aggiornato 2026-08-13

Perché systemd ha vinto la sfida con SysV init

Come systemd ha risolto dipendenze e processi con cgroup e socket, perché Upstart e launchd non bastarono e quali obiezioni erano fondate.

Perché systemd ha prevalso

La storia di systemd inizia con due problemi che SysV init non poteva risolvere. SysV init (System V init, il sistema di avvio che Linux ha ereditato da AT&T Unix) non aveva modo di descrivere le dipendenze di un servizio né di sapere quali processi appartenessero a un servizio già avviato. systemd ha risolto entrambi i problemi usando funzionalità del kernel che uno script shell non può utilizzare: i control group per tenere traccia dei processi e i socket di ascolto aperti in anticipo per definire l'ordine di avvio. Il resto della storia riguarda la diffusione di queste due soluzioni nel resto dello userland. È qui che iniziano le obiezioni, e alcune erano fondate.

Che cosa faceva realmente SysV init

Su un sistema SysV, PID 1 (l'ID di processo 1, il primo processo avviato dal kernel) leggeva /etc/inittab, selezionava un runlevel ed eseguiva gli script associati a quel runlevel. Gli script si trovavano in /etc/init.d/. I collegamenti simbolici in /etc/rc3.d/ stabilivano quali script eseguire e in quale ordine; per questo /etc/rc3.d/S20nginx puntava a /etc/init.d/nginx e veniva chiamato con l'argomento start.

#!/bin/sh
### BEGIN INIT INFO
# Provides:          nginx
# Required-Start:    $local_fs $remote_fs $network $syslog
# Required-Stop:     $local_fs $remote_fs $network $syslog
# Default-Start:     2 3 4 5
# Default-Stop:      0 1 6
### END INIT INFO

case "$1" in
  start)
    start-stop-daemon --start --quiet --pidfile /run/nginx.pid \
      --exec /usr/sbin/nginx
    ;;
  stop)
    start-stop-daemon --stop --quiet --retry TERM/30/KILL/5 \
      --pidfile /run/nginx.pid
    ;;
esac

Il 20 in S20nginx indica una posizione, non una dipendenza. Specifica che questo script viene eseguito dopo S19 e prima di S21. Non specifica il motivo, quindi nessun componente può verificarlo e non è possibile eseguire in sicurezza due script indipendenti contemporaneamente senza che una persona stabilisca che l'operazione è sicura.

Il programma rc eseguiva ogni script in sequenza e attendeva che terminasse. Se uno script restava bloccato per trenta secondi in attesa di un indirizzo di rete, l'intero avvio rimaneva bloccato per trenta secondi, anche per i servizi che non usavano mai la rete.

L'header LSB (Linux Standard Base) all'inizio dello script era un tentativo di risolvere il problema dall'interno. Nel 2011, Debian 6.0 impostava insserv come comportamento predefinito: leggeva Required-Start da ogni script, costruiva un grafo e riassegnava la numerazione dei collegamenti simbolici. Debian poteva quindi eseguire contemporaneamente gli script indipendenti tramite startpar. Questo migliorava la situazione, ma non risolveva il problema più profondo. La dipendenza era ancora basata sulla terminazione di uno script. Il codice S20nginx pari a 0 indica che una funzione della shell è terminata correttamente. Non indica che nginx accetta connessioni.

Le cinque cose che nessuno script init poteva risolvere

  • Avvio parallelo. L'ordinamento basato sul nome file impone un ordine totale a tutti i servizi della macchina, quindi l'avvio richiede almeno la somma dei tempi delle singole operazioni.
  • Verifica della disponibilità. Uno script di avvio termina quando ha eseguito il fork del daemon, non quando il daemon è in grado di gestire una richiesta. Per questo lo script successivo spesso parte troppo presto.
  • Supervisione. Un daemon esegue il fork due volte e il processo padre termina. Il daemon viene così scollegato dal terminale e riassegnato a PID 1. init rileva la terminazione di un processo figlio, ma non dispone di un collegamento affidabile con il processo sopravvissuto.
  • Avvio su richiesta. inetd (il super-server Internet) poteva avviare un daemon quando arrivava una connessione, ma era un sistema separato con un proprio file di configurazione. Inoltre non gestiva l'ordine di avvio degli altri servizi.
  • Controllo delle risorse. Nessuna direttiva in uno script init poteva limitare la memoria di un servizio o la sua quota di CPU. ulimit si applicava a un solo processo e nice interveniva soltanto sullo scheduler, quindi un processo figlio fuori controllo appariva come qualsiasi altro processo del sistema.

La mancanza di supervisione era il problema più evidente nell'attività quotidiana. Il file PID era la soluzione adottata: il daemon scriveva il proprio ID di processo in /run/nginx.pid e la funzione di arresto rileggeva quel file. Se il daemon veniva terminato forzatamente, il file rimaneva. Il kernel riutilizzava quindi quel numero per un altro processo e start-stop-daemon --stop --pidfile inviava un segnale al processo che nel frattempo ne era diventato proprietario. Un file PID obsoleto può quindi portare uno script init a terminare il processo sbagliato.

launchd ha risolto per primo il problema dei socket

Apple ha incluso launchd in Mac OS X 10.4 nel 2005. Il programma è stato scritto da Dave Zarzycki. Un unico processo ha sostituito init, rc, xinetd, crond e watchdogd.

L'idea da riutilizzare era l'attivazione tramite socket. launchd crea prima tutti i socket in ascolto e avvia in seguito i daemon. Un client che si connette a un daemon non ancora avviato non riceve un rifiuto della connessione, perché il kernel mantiene la connessione nella coda backlog del socket finché il daemon non chiama accept(). L'ordine di avvio tra due daemon non deve più essere dichiarato manualmente. Se ne occupa il socket.

launchd è stato costruito su Mach IPC (comunicazione tra processi), che appartiene al kernel XNU di Apple e non ha un equivalente in Linux. Portare il codice su un'altra piattaforma non è mai stato realistico. L'idea, però, si è diffusa comunque.

Upstart faceva degli eventi l'unità di lavoro

Upstart, il sistema di Canonical scritto da Scott James Remnant, è stato rilasciato con Ubuntu 6.10 nell'ottobre 2006. Lo hanno utilizzato Fedora 9 fino a Fedora 14, RHEL 6 e Chrome OS. Ha sostituito il runlevel con un evento; un job dichiarava quali eventi dovevano avviarlo e arrestarlo.

# /etc/init/example.conf
start on filesystem and net-device-up IFACE!=lo
stop on runlevel [!2345]
respawn
respawn limit 10 5
exec /usr/local/bin/exampled

Con l'aumento del numero di job sono emersi due problemi. Il primo riguardava la direzione delle dipendenze. Un job dichiara «avviami quando accade questo evento», quindi le informazioni sulle dipendenze si trovano nel file sbagliato: un servizio conosce ciò di cui ha bisogno, ma non può sapere chi ne avrà bisogno l'anno successivo. Aggiungere un servizio spesso significava modificare un job esistente affinché emettesse un nuovo evento.

Il secondo problema riguardava il monitoraggio. Upstart seguiva un daemon che eseguiva il fork contando le chiamate fork() tramite ptrace, configurato come expect fork o expect daemon. Se il numero di fork era errato, Upstart supervisionava un processo già terminato oppure attendeva un fork già eseguito. Il sintomo era initctl start bloccato senza errori; il file del job non forniva alcun modo per spiegarne la causa.

Upstart richiedeva inoltre ai contributori di firmare l'accordo di contribuzione di Canonical. Non era un problema tecnico, ma influiva sulle persone che vi lavoravano.

Ripensare il PID 1, aprile 2010

Il 30 aprile 2010 Lennart Poettering pubblicò un articolo intitolato "Ripensare il PID 1". Kay Sievers lavorò al progetto insieme a lui. La proposta si articolava in quattro punti.

  • Avviare meno servizi. Molti servizi possono attendere fino a quando un client non ne richiede effettivamente l'utilizzo.
  • Evitare di dichiarare l'ordine quando questo può essere implicato da un socket. Aprire tutti i socket in un'unica fase, quindi avviare tutto contemporaneamente.
  • Tracciare i processi con i control group invece dei file PID.
  • Descrivere un servizio in un file dichiarativo, in modo che una descrizione funzioni su ogni distribuzione.

La prima release arrivò nello stesso anno. Fedora 14 rese disponibile systemd come opzione nel novembre 2010, mentre Fedora 15 lo impostò come predefinito nel maggio 2011.

Perché i cgroup hanno reso affidabile il controllo dei servizi

Un cgroup (control group) è una funzionalità del kernel che raggruppa i processi, integrata in Linux 2.6.24 nel 2008. systemd inserisce ogni servizio nel proprio cgroup. Un processo figlio eredita il cgroup del processo padre e un processo senza privilegi non può spostarsi al di fuori di quello assegnato. Il doppio forking quindi non nasconde nulla: PID 1 mantiene in ogni momento l’insieme esatto dei processi appartenenti a un’unità. Arrestare un servizio significa terminare tutti i processi del relativo cgroup, operazione eseguita dal valore predefinito di KillMode=control-group.

systemctl status stampa quel gruppo:

● nginx.service - A high performance web server and a reverse proxy server
     Loaded: loaded (/usr/lib/systemd/system/nginx.service; enabled; preset: enabled)
     Active: active (running) since Tue 2026-08-11 09:14:22 UTC; 3min ago
   Main PID: 1042 (nginx)
      Tasks: 3 (limit: 4653)
     Memory: 6.1M (peak: 7.4M)
     CGroup: /system.slice/nginx.service
             ├─1042 "nginx: master process /usr/sbin/nginx"
             ├─1043 "nginx: worker process"
             └─1044 "nginx: worker process"

Questo blocco risolve completamente il problema del file PID obsoleto. Non esiste alcun file che possa diventare obsoleto, perché l’elenco corrisponde allo stato del kernel.

Lo stesso albero contiene anche i limiti, perché i cgroup sono stati progettati per la contabilizzazione prima di essere usati per il controllo dei processi. MemoryMax=, CPUQuota= e TasksMax= occupano una riga ciascuno. Impostare un limite rigido per memoria e CPU su un servizio oggi richiede un file drop-in; nel 2009 avrebbe richiesto una patch a uno script shell che nessuno aveva scritto.

Perché tutte le distribuzioni sono passate a systemd tra il 2011 e il 2015

  • Fedora 15, maggio 2011.
  • openSUSE 12.1, novembre 2011.
  • Mageia 2, maggio 2012.
  • Arch Linux, impostazione predefinita per le nuove installazioni da ottobre 2012.
  • RHEL 7, giugno 2014.
  • SLES 12, ottobre 2014.
  • Debian 8, aprile 2015.
  • Ubuntu 15.04, aprile 2015.

I motivi erano per lo più poco interessanti, quindi il passaggio è stato rapido.

  • Un unico file di unità funziona su ogni distribuzione. Per questo i progetti upstream hanno iniziato a distribuire un file .service e le distribuzioni hanno smesso di mantenere uno script shell per ogni pacchetto e per ogni release.
  • Il tracciamento delle sessioni desktop è passato a systemd-logind dopo che ConsoleKit ha smesso di essere mantenuto intorno al 2012. GNOME aveva bisogno di logind, quindi una distribuzione senza systemd doveva trovare un'alternativa. Quell'alternativa, elogind, è logind di systemd estratto e mantenuto separatamente.
  • udev, il gestore dei dispositivi, è stato integrato nell'albero dei sorgenti di systemd nell'aprile 2012. Le distribuzioni che distribuivano udev ora seguivano il repository di systemd. Gentoo ha creato un fork di eudev in risposta.
  • I container hanno reso più importante un tracciamento affidabile dei processi e l'applicazione di limiti per servizio, perché entrambe sono funzionalità dei cgroup. La questione di quale supervisor debba gestire un processo in un container è ancora attuale ogni volta che fai ripartire uno stack Docker Compose dopo un riavvio.

La decisione di Debian è stata quella che ha suscitato più attenzione. Il Technical Committee ha votato nel febbraio 2014, il voto è terminato in parità e il presidente, Bdale Garbee, ha espresso il voto decisivo a favore di systemd. Pochi giorni dopo Ubuntu ha annunciato che avrebbe seguito Debian invece di continuare con Upstart. Un gruppo di sviluppatori Debian ha creato la distribuzione derivata Devuan nel novembre 2014 e ha pubblicato Devuan 1.0 nel maggio 2017.

Le obiezioni, esposte correttamente

Ambito. Un unico progetto ora fornisce PID 1, il demone di logging, la gestione delle sessioni di accesso, il device manager, un demone per la configurazione di rete, un resolver DNS (domain name system), un client NTP (network time protocol), un container runner e un boot loader. La difesa abituale, secondo cui si tratta di binari separati che non è necessario installare, è corretta, ma non risponde all'obiezione. Quando un desktop richiede logind e logind viene distribuito separatamente dall'albero di systemd, la scelta non è più libera. Questo è il significato di accoppiamento nell'argomentazione, e questo accoppiamento si è verificato.

Il journal binario. journald scrive in un formato binario indicizzato invece che in testo normale. Offre funzioni che il testo non consentiva: filtraggio per unità e priorità, campi strutturati e metadati che il programma mittente non può falsificare, perché journald registra autonomamente l'unità e il cgroup. journalctl -u nginx -p err --since "-1h" sostituisce grep con un'espressione regolare per la data. Anche il costo è reale. Su una macchina che non si avvia, non è possibile leggere il log con less da una shell di ripristino. È invece necessario indicare a journalctl il disco montato:

sudo journalctl --directory /mnt/var/log/journal --boot -1 --priority err

Esiste anche una seconda trappola, che prima o poi coinvolge molti amministratori. journald conserva i log in /run/log/journal, che risiede in memoria, a meno che non esista /var/log/journal. Su un sistema in cui non esiste, journalctl -b -1 non ha nulla da mostrare dopo un riavvio, cioè proprio nel momento in cui il log serve. Verificare e correggere la configurazione:

journalctl --disk-usage
sudo mkdir -p /var/log/journal
sudo systemd-tmpfiles --create --prefix /var/log/journal
sudo systemctl restart systemd-journald

journalctl --disk-usage dovrebbe ora mostrare i journal archiviati in /var/log/journal. Per mantenere anche testo normale, impostare ForwardToSyslog=yes in /etc/systemd/journald.conf e lasciare installato rsyslog.

Debuggabilità dell'avvio. Quando un'unità si blocca, la console mostra una riga e nient'altro:

[  *** ] A start job is running for Wait for Network to be Configured (1min 32s / no limit)

Gli strumenti per proseguire l'analisi esistono: systemctl list-jobs mentre l'unità è bloccata, systemd-analyze blame e systemd-analyze critical-chain in seguito, e systemd.log_level=debug nella riga di comando del kernel. La formulazione corretta dell'obiezione è che uno script init poteva essere letto dall'inizio alla fine da chiunque conoscesse sh, mentre per analizzare un'unità bloccata è necessario sapere quale usare tra una dozzina di comandi. Questo è un costo reale. Ogni amministratore lo sostiene una volta, e molti amministratori lo hanno sostenuto nello stesso periodo.

Un'impostazione predefinita che cambia per tutti. Nel 2016 systemd 230 ha modificato l'impostazione predefinita di logind, terminando alla disconnessione i processi utente rimasti attivi. Le sessioni tmux e screen scollegate terminavano quando finiva la sessione che le aveva avviate. Le distribuzioni fornivano KillUserProcesses=no in /etc/systemd/logind.conf e la soluzione supportata è loginctl enable-linger <user>. Un'impostazione predefinita in un singolo progetto ha modificato un comportamento su cui facevano affidamento milioni di persone. Questo è il significato pratico di "una parte eccessiva dello userland concentrata in un unico punto".

Una dipendenza predefinita è una superficie di sicurezza. Nel marzo 2024 la backdoor in xz-utils prendeva di mira sshd su Debian e Ubuntu. OpenSSH upstream non effettua il linking con libsystemd. Queste distribuzioni lo hanno modificato affinché sshd potesse segnalare la propria disponibilità a systemd, e libsystemd introduceva liblzma, dove risiedeva la backdoor. Il protocollo di disponibilità consiste in un singolo datagramma inviato al socket indicato in $NOTIFY_SOCKET, quindi non è mai stata necessaria alcuna libreria. La risposta di systemd è stata caricare le librerie di compressione con dlopen, così non vengono più collegate per impostazione predefinita. Una classe di bug correlata presenta la stessa struttura: nel 2017 un valore User= che iniziava con una cifra veniva considerato non valido e l'unità veniva eseguita come root invece di fallire, trasformando un errore di battitura in un'escalation dei privilegi. Le versioni successive rifiutano di avviare l'unità.

La cronologia nel prompt systemctl del proprio sistema

Ora ogni problema precedente corrisponde a una direttiva in un file che si può leggere.

  • L'avvio sequenziale è diventato After= e Wants=, mentre systemd-analyze critical-chain mostra cosa ha effettivamente bloccato l'avvio.
  • La verifica della disponibilità è diventata Type=notify: quando il servizio può accettare richieste, scrive READY=1 in $NOTIFY_SOCKET. Type=forking con PIDFile= esiste ancora per i daemon meno recenti ed è il tipo che fallisce con start operation timed out. Terminating. quando il file PID non compare.
  • La supervisione è affidata al cgroup. Per questo Restart=on-failure con RestartSec= sostituisce uno script wrapper e StartLimitBurst= impedisce che un ciclo di crash continui indefinitamente.
  • inetd è diventato un'unità .socket accanto all'unità .service.
  • ulimit è diventato MemoryMax=, CPUQuota= e TasksMax=.
  • La direttiva su - appuser -c in uno script init è diventata User=, NoNewPrivileges=yes e ProtectSystem=strict. Per questo eseguire un servizio con un utente senza privilegi è la configurazione predefinita di un'unità, non un'attività aggiuntiva.
[Unit]
Description=Example API
Wants=network-online.target
After=network-online.target postgresql.service
Requires=postgresql.service

[Service]
Type=notify
ExecStart=/usr/local/bin/exampled
Restart=on-failure
RestartSec=2
MemoryMax=512M
CPUQuota=50%
TasksMax=128
User=exampled
NoNewPrivileges=yes
PrivateTmp=yes
ProtectSystem=strict
StateDirectory=exampled

[Install]
WantedBy=multi-user.target

Una riga di quel file rappresenta l'errore che prima o poi commettono tutti. Requires=postgresql.service è un requisito, non un vincolo di ordinamento: indica che l'unità fallisce se Postgres fallisce, ma non indica di avviare prima Postgres. Senza After=postgresql.service, entrambi vengono avviati nello stesso momento e il servizio tenta di connettersi a una porta su cui non è ancora in ascolto alcun processo. La separazione è intenzionale, perché a volte si vuole uno dei due comportamenti senza l'altro. ProtectSystem=strict monta il file system in sola lettura per questo servizio. Per questo è presente StateDirectory=: fornisce al servizio un unico percorso scrivibile sotto /var/lib.

Il modo più chiaro per vedere il modello del 2005 su un server del 2026 è usare SSH su Ubuntu 24.04, che ad agosto 2026 include systemd 255. ssh.service viene attivato tramite socket per impostazione predefinita: ssh.socket contiene il socket in ascolto e sshd viene avviato quando arriva una connessione. Di conseguenza, Port 2222 in /etc/ssh/sshd_config non ha effetto, perché non è sshd ad aver aperto la porta. La modifica deve essere applicata all'unità socket.

sudo systemctl edit ssh.socket
[Socket]
ListenStream=
ListenStream=2222

Il valore vuoto di ListenStream= cancella il valore ereditato dall'unità fornita dal pacchetto. Se lo si omette, si ottengono entrambe le porte, perché systemd aggiunge elementi a un elenco invece di sostituirlo. Applicare quindi la modifica e verificare il risultato, mantenendo aperta per tutta la procedura una seconda sessione SSH:

sudo systemctl daemon-reload
sudo systemctl restart ssh.socket
sudo ss -lntp | grep 2222

ss dovrebbe elencare un solo socket sulla porta 2222, di proprietà di systemd e non di sshd. Questo è il modello di avvio di launchd, vent'anni dopo, sul proprio VPS. Se si preferisce il comportamento precedente, sudo systemctl disable --now ssh.socket seguito da sudo systemctl enable --now ssh.service fornisce un processo sshd a esecuzione prolungata che legge nuovamente Port dalla propria configurazione.

I dettagli che si incontrano dipendono dalla release utilizzata. Prima di pianificare un aggiornamento, è quindi utile conoscere la differenza tra una release LTS e una release interim di Ubuntu. Quando si gestiscono più macchine, il fatto che un file di unità sia identico ovunque è il motivo per cui gestire più server da un unico punto è oggi un problema di configurazione, non di scripting della shell. Quando si scrivono unità personalizzate, inoltre, la coppia servizio e timer svolge il lavoro che nel 2009 si sarebbe diviso tra uno script init e una riga di cron.

FAQ

Perché le distribuzioni Linux hanno sostituito SysV init con systemd?

Per due motivi tecnici e uno di manutenzione. SysV init ordinava i servizi in base al nome del file, che indica una posizione e non una dipendenza, e perdeva il controllo dei demoni che eseguivano un fork separandosi dal processo padre. Per questo i file PID obsoleti potevano terminare il processo sbagliato. systemd ha risolto l'ordinamento con l'attivazione tramite socket e le direttive sulle dipendenze, e il tracciamento con i control group. Il motivo legato alla manutenzione ha determinato la rapidità della transizione: un unico file unità funziona su ogni distribuzione, quindi i progetti upstream hanno distribuito un file .service e i manutentori delle distribuzioni hanno smesso di scrivere uno script shell per ogni pacchetto. Fedora 15 è passata a systemd a maggio 2011 e Ubuntu 15.04 è stata l'ultima grande distribuzione a farlo, ad aprile 2015.

systemd è un unico binario di grandi dimensioni?

No. L'albero dei sorgenti compila molti programmi separati. PID 1 è /usr/lib/systemd/systemd, mentre journald, logind e udevd sono processi distinti con binari propri; esegui ls /usr/lib/systemd/ per visualizzarli sul tuo sistema. La critica che rimane riguarda l'accoppiamento delle release, non le dimensioni dei binari: questi programmi vengono rilasciati insieme e condividono interfacce private, quindi le distribuzioni tendono ad adottarli come un insieme, e software come GNOME ha iniziato ad aspettarsi specificamente logind.

Posso ancora eseguire Linux senza systemd?

Sì. Devuan distribuisce sysvinit, Gentoo usa OpenRC come impostazione predefinita, Void usa runit, Alpine usa busybox init con OpenRC e Slackware mantiene gli script in stile BSD. Il costo è il lavoro necessario per la compatibilità. Il software desktop che si aspetta logind richiede elogind, cioè logind di systemd mantenuto come pacchetto autonomo, e una quantità crescente di software server distribuisce ormai soltanto un file .service, quindi devi scrivere e mantenere personalmente lo script di avvio.

Perché il journal usa un formato binario invece di un semplice file di testo?

Perché journald memorizza campi strutturati con un indice. Questo consente il filtraggio per unità e priorità e fornisce metadati che il programma che invia il messaggio non può falsificare: journald registra autonomamente l'unità, il cgroup e l'UID reale invece di fidarsi della riga di log. Lo svantaggio è che per leggerlo devi usare journalctl, anche da un sistema di ripristino, dove indichi il disco montato con journalctl --directory /mnt/var/log/journal. Se vuoi anche un formato testuale, imposta ForwardToSyslog=yes in /etc/systemd/journald.conf.

Che cosa ha sostituito la modifica dello script in /etc/init.d?

I file drop-in. Non modificare l'unità in /usr/lib/systemd/system/, perché un aggiornamento del pacchetto la sovrascrive. Esegui sudo systemctl edit nginx.service e systemd crea /etc/systemd/system/nginx.service.d/override.conf, che viene applicato sopra l'unità fornita dal pacchetto. systemctl cat nginx.service mostra il risultato combinato e systemd-delta elenca tutti gli override presenti sul sistema. Dopo ogni modifica manuale, esegui sudo systemctl daemon-reload, altrimenti il comando successivo stampa Warning: The unit file, source configuration file or drop-ins of nginx.service changed on disk.

#systemd#linux#init#sysvinit#history