SSD Nodes Learn 🎉 VPS dès $5.50/mois
Guides Matt ConnorPar Matt Connor

Pourquoi systemd s’est imposé face à SysV init

Découvrez ce que SysV init ne pouvait pas gérer, les apports d’Upstart et launchd, et pourquoi les distributions ont adopté systemd en quatre ans.

Pourquoi systemd s’est imposé

L’histoire de systemd commence avec deux problèmes que SysV init ne pouvait pas résoudre. SysV init (System V init, le système de démarrage Linux hérité d’AT&T Unix) ne permettait pas de décrire les dépendances d’un service ni de savoir quels processus lui appartenaient une fois lancé. systemd a résolu ces deux problèmes grâce à des fonctions du noyau qu’un script shell ne peut pas utiliser : les control groups pour suivre les processus et les sockets d’écoute ouverts à l’avance pour gérer l’ordre de démarrage. La suite explique comment ces deux solutions se sont étendues au reste de l’espace utilisateur. C’est à ce moment que les objections ont commencé, et plusieurs d’entre elles étaient fondées.

Ce que faisait réellement SysV init

Sur un système SysV, le PID 1 (l’identifiant de processus 1, le premier processus lancé par le kernel) lisait /etc/inittab, sélectionnait un runlevel, puis exécutait les scripts associés à ce runlevel. Les scripts se trouvaient dans /etc/init.d/. Les liens symboliques de /etc/rc3.d/ déterminaient lesquels étaient exécutés et dans quel ordre. Ainsi, /etc/rc3.d/S20nginx pointait vers /etc/init.d/nginx et était appelé avec l’argument 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

Le 20 dans S20nginx indique une position, pas une dépendance. Il indique que ce script s’exécute après S19 et avant S21. Il n’indique pas pourquoi. Rien ne peut donc le vérifier, et rien ne peut exécuter deux scripts sans lien entre eux en parallèle sans qu’un administrateur ait préalablement déterminé que c’était sûr.

Le programme rc exécutait chaque script à tour de rôle et attendait sa fin. Un script bloqué pendant trente secondes en attendant une adresse réseau bloquait tout le démarrage pendant trente secondes, y compris pour les services qui n’utilisent jamais le réseau.

L’en-tête LSB (Linux Standard Base) placé au début de ce script visait à corriger ce problème de l’intérieur. En 2011, Debian 6.0 a fait de insserv la valeur par défaut : il lisait Required-Start dans chaque script, construisait un graphe et renumérotait les liens symboliques. Debian pouvait alors exécuter simultanément les scripts indépendants avec startpar. Cela a amélioré la situation, sans résoudre le problème de fond. La dépendance portait toujours sur la fin d’exécution d’un script. Le code de retour S20nginx égal à 0 signifie qu’une fonction shell s’est terminée correctement. Il ne signifie pas que nginx accepte des connexions.

Les cinq problèmes qu’aucun script init ne pouvait résoudre

  • Démarrage en parallèle. Le classement par nom de fichier impose un ordre total entre tous les services de la machine. Le démarrage prend donc au minimum la somme des durées de chaque service.
  • État prêt. Un script de démarrage se termine dès qu’il a forké le daemon, et non lorsque celui-ci peut traiter une requête. Le script suivant démarre donc souvent trop tôt.
  • Supervision. Un daemon effectue deux fork, puis son processus parent se termine. Le daemon n’est alors plus rattaché au terminal et est réadopté par le PID 1. init voit un processus fils se terminer, mais ne dispose d’aucun lien fiable avec le processus qui a survécu.
  • Démarrage à la demande. inetd (le super-serveur Internet) pouvait lancer un daemon lorsqu’une connexion arrivait, mais il s’agissait d’un système séparé avec son propre fichier de configuration. Il ne résolvait pas le problème de l’ordre de démarrage des autres services.
  • Contrôle des ressources. Aucun élément d’un script init ne pouvait limiter la mémoire d’un service ni sa part de CPU. ulimit s’appliquait à un seul processus, et nice ne concernait que le scheduler. Un processus fils qui consommait excessivement des ressources était donc traité comme n’importe quel autre processus de la machine.

Le manque de supervision était le problème le plus pénalisant au quotidien. Le fichier PID servait de contournement : le daemon écrivait son identifiant de processus dans /run/nginx.pid, puis la fonction d’arrêt relisait ce fichier. Si le daemon était tué de force, le fichier restait présent. Le kernel réutilisait ensuite ce numéro pour un autre processus, et start-stop-daemon --stop --pidfile envoyait un signal au processus qui le possédait désormais. Un fichier PID obsolète permet à un script init de tuer le mauvais processus.

launchd a d’abord résolu le problème des sockets

Apple a publié launchd dans Mac OS X 10.4 en 2005. Le programme a été écrit par Dave Zarzycki. Un seul processus a remplacé init, rc, xinetd, crond et watchdogd.

L’idée à reprendre était l’activation par socket. launchd crée d’abord tous les sockets en écoute, puis démarre les daemons. Un client qui se connecte à un daemon qui n’a pas encore démarré ne reçoit pas de refus de connexion, car le kernel conserve la connexion dans la file d’attente backlog de ce socket jusqu’à ce que le daemon appelle accept(). L’ordre de démarrage entre deux daemons ne dépend plus d’une déclaration humaine. Le socket le gère.

launchd repose sur Mach IPC (communication interprocessus), qui appartient au kernel XNU d’Apple et n’a pas d’équivalent sous Linux. Porter le code n’a jamais été réaliste. L’idée s’est tout de même propagée.

Upstart a fait des événements l’unité de travail

Upstart, le système de Canonical écrit par Scott James Remnant, a été publié avec Ubuntu 6.10 en octobre 2006. Fedora 9 à Fedora 14 l’ont utilisé, tout comme RHEL 6 et Chrome OS. Il a remplacé le runlevel par un événement. Un job indiquait quels événements devaient le démarrer et l’arrêter.

# /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

Deux problèmes sont apparus lorsque le nombre de jobs a augmenté. Le premier concerne le sens de la dépendance. Un job indique « démarrez-moi lorsque cela se produit ». La connaissance des dépendances se trouve donc dans le mauvais fichier : un service connaît ce dont il a besoin, mais il ne peut pas savoir quel autre service en aura besoin l’année prochaine. Ajouter un service impliquait souvent de modifier un job existant pour qu’il émette un nouvel événement.

Le second concerne le suivi des processus. Upstart suivait un daemon qui se mettait en arrière-plan en comptant les appels fork() avec ptrace, que vous configuriez comme expect fork ou expect daemon. Si le nombre de forks était incorrect, Upstart supervisait un processus qui s’était déjà arrêté ou attendait un fork qui avait déjà eu lieu. Le symptôme était une commande initctl start bloquée sans erreur. Le fichier du job ne permettait pas d’en expliquer la cause.

Upstart exigeait également des contributeurs qu’ils signent l’accord de contribution de Canonical. Ce n’était pas un défaut d’ingénierie, mais cela a bien influencé les personnes qui travaillaient sur le projet.

Repenser PID 1, avril 2010

Le 30 avril 2010, Lennart Poettering a publié un article intitulé « Repenser PID 1 ». Kay Sievers a travaillé avec lui sur le projet. L’argumentaire comportait quatre volets.

  • Démarrer moins de services. De nombreux services peuvent attendre qu’une requête les sollicite réellement.
  • Éviter de déclarer l’ordre lorsqu’un socket peut l’induire. Ouvrir tous les sockets en une seule passe, puis démarrer tous les services en même temps.
  • Suivre les processus avec des control groups plutôt qu’avec des fichiers PID.
  • Décrire un service dans un fichier déclaratif, afin qu’une même description fonctionne sur toutes les distributions.

La première release est sortie cette même année. Fedora 14 a proposé systemd en option en novembre 2010, puis Fedora 15 en a fait le système par défaut en mai 2011.

Pourquoi les cgroups ont rendu la supervision fiable

Un cgroup (control group) est une fonctionnalité du kernel qui permet de regrouper des processus. Elle a été intégrée à Linux 2.6.24 en 2008. systemd place chaque service dans son propre cgroup. Un processus enfant hérite du cgroup de son parent. Un processus non privilégié ne peut pas s’en extraire. Le double fork ne dissimule donc rien : PID 1 conserve à tout moment l’ensemble exact des processus appartenant à une unité. Arrêter un service consiste à tuer tous les processus de son cgroup. C’est ce que fait le KillMode=control-group par défaut.

systemctl status affiche ce groupe :

● 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"

Ce bloc répond entièrement au problème du fichier PID obsolète. Aucun fichier ne peut devenir obsolète, car la liste correspond à l’état du kernel.

Le même arbre porte aussi les limites, car les cgroups ont d’abord été conçus pour la comptabilisation, avant d’être utilisés pour le suivi des processus. MemoryMax=, CPUQuota= et TasksMax= tiennent chacun sur une ligne. Définir une limite stricte de mémoire et de CPU pour un service consiste aujourd’hui à créer un fichier drop-in ; en 2009, il fallait modifier un script shell que personne n’avait écrit.

Pourquoi toutes les distributions ont changé entre 2011 et 2015

  • Fedora 15, mai 2011.
  • openSUSE 12.1, novembre 2011.
  • Mageia 2, mai 2012.
  • Arch Linux, par défaut pour les nouvelles installations à partir d’octobre 2012.
  • RHEL 7, juin 2014.
  • SLES 12, octobre 2014.
  • Debian 8, avril 2015.
  • Ubuntu 15.04, avril 2015.

Les raisons étaient généralement peu passionnantes, ce qui explique la rapidité de la transition.

  • Un même fichier d’unité fonctionne sur toutes les distributions. Les projets upstream ont donc commencé à fournir un fichier .service, et les distributions ont cessé de maintenir un script shell par paquet et par version.
  • Le suivi des sessions graphiques est passé à systemd-logind après l’abandon de ConsoleKit vers 2012. GNOME avait besoin de logind. Une distribution sans systemd devait donc trouver un remplacement. Ce remplacement, elogind, est le logind de systemd extrait et maintenu séparément.
  • udev, le gestionnaire de périphériques, a été fusionné dans l’arbre source de systemd en avril 2012. Les distributions qui fournissaient udev suivaient désormais le dépôt de systemd. Gentoo a créé un fork de eudev en réaction.
  • Les conteneurs ont accru l’importance du suivi fiable des processus et des limites par service, car ces deux fonctions reposent sur les cgroups. La question de savoir quel superviseur gère le processus d’un conteneur reste d’actualité lorsque vous faites redémarrer une stack Docker Compose après un reboot.

La décision de Debian a été la plus retentissante. Le Comité technique a voté en février 2014. Le vote s’est soldé par une égalité et le président, Bdale Garbee, a voté en faveur de systemd pour départager les voix. Quelques jours plus tard, Ubuntu a annoncé qu’il suivrait Debian plutôt que de continuer avec Upstart. Un groupe de développeurs Debian a créé un fork de la distribution sous le nom de Devuan en novembre 2014, puis a publié Devuan 1.0 en mai 2017.

Les objections, formulées équitablement

Périmètre. Un seul projet fournit désormais PID 1, le daemon de journalisation, la gestion des sessions de connexion, le gestionnaire de périphériques, un daemon de configuration réseau, un resolver DNS (domain name system), un client NTP (network time protocol), un lanceur de conteneurs et un boot loader. La défense habituelle — il s’agit de binaires distincts que vous n’êtes pas obligé d’installer — est exacte, mais elle ne répond pas à l’objection. Dès qu’un environnement de bureau a besoin de logind et que logind est fourni par l’arborescence systemd, le choix n’est plus libre. C’est ce que l’argument entendait par couplage, et c’est bien ce qui s’est produit.

Le journal binaire. journald écrit dans un format binaire indexé plutôt qu’en texte brut. Vous obtenez des fonctions que le texte ne permettait pas : filtrage par unité et par priorité, champs structurés et métadonnées que le programme émetteur ne peut pas falsifier, car journald enregistre lui-même l’unité et le cgroup. journalctl -u nginx -p err --since "-1h" remplace un grep par une expression régulière correspondant à une date. Le coût est réel lui aussi. Sur une machine qui ne démarre pas, vous ne pouvez pas lire le journal avec less depuis un shell de secours. Vous indiquez plutôt à journalctl le disque monté :

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

Il existe ici un deuxième piège, qui surprend souvent une première fois. journald conserve les journaux dans /run/log/journal, qui se trouve en mémoire, sauf si /var/log/journal existe. Sur une machine où ce répertoire n’existe pas, journalctl -b -1 n’a rien à afficher après un redémarrage, précisément au moment où vous en avez besoin. Vérifiez et corrigez le problème :

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 doit maintenant afficher des journaux archivés sous /var/log/journal. Si vous voulez aussi du texte brut, définissez ForwardToSyslog=yes dans /etc/systemd/journald.conf et laissez rsyslog installé.

Débogage du boot. Lorsqu’une unité reste bloquée, la console affiche une ligne et rien d’autre :

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

Les outils nécessaires pour aller plus loin existent bien : systemctl list-jobs pendant le blocage, systemd-analyze blame et systemd-analyze critical-chain ensuite, ainsi que systemd.log_level=debug sur la ligne de commande du kernel. La formulation équitable de l’objection est la suivante : n’importe qui connaissant sh pouvait lire un script init de haut en bas, tandis qu’une unité bloquée exige de savoir laquelle d’une douzaine de commandes utiliser. C’est un coût réel. Chaque administrateur ne le paie qu’une fois, mais beaucoup d’administrateurs l’ont payé en même temps.

Un défaut qui change pour tout le monde. systemd 230, en 2016, a modifié le comportement par défaut de logind afin que les processus utilisateur restants soient tués à la déconnexion. Les sessions tmux et screen détachées étaient terminées lorsque la session qui les avait lancées se terminait. Les distributions ont fourni KillUserProcesses=no dans /etc/systemd/logind.conf, et la réponse officiellement prise en charge est loginctl enable-linger <user>. Un seul défaut, dans un seul projet, a modifié une habitude sur laquelle des millions de personnes comptaient. C’est ce que signifie concrètement « trop de composants de userland au même endroit ».

Une dépendance par défaut constitue une surface d’attaque. En mars 2024, la backdoor présente dans xz-utils ciblait sshd sur Debian et Ubuntu. OpenSSH en amont ne se lie pas à libsystemd. Ces distributions l’ont modifié pour que sshd puisse signaler son état prêt à systemd, et libsystemd entraînait liblzma, qui contenait la backdoor. Le protocole de notification d’état prêt consiste lui-même en un datagramme envoyé à la socket indiquée dans $NOTIFY_SOCKET ; aucune bibliothèque n’a donc jamais été nécessaire. systemd a réagi en chargeant les bibliothèques de compression avec dlopen, afin qu’elles ne soient plus liées par défaut. Une catégorie connexe de bug présente la même structure : en 2017, une valeur User= commençant par un chiffre était considérée comme invalide et l’unité s’exécutait avec root au lieu d’échouer. Une faute de frappe devenait ainsi une élévation de privilèges. Les versions ultérieures refusent de démarrer l’unité.

Historique directement depuis l’invite systemctl

Chaque problème présenté ci-dessus correspond désormais à une directive dans un fichier que vous pouvez consulter.

  • Le démarrage séquentiel est devenu After= et Wants=, tandis que systemd-analyze critical-chain indique ce qui a réellement bloqué le démarrage.
  • La gestion de l’état prêt est devenue Type=notify : le service écrit READY=1 dans $NOTIFY_SOCKET lorsqu’il peut traiter les requêtes. Type=forking avec PIDFile= existe toujours pour les anciens daemons, et c’est ce type qui échoue avec start operation timed out. Terminating. lorsque le fichier PID n’apparaît jamais.
  • La supervision repose désormais sur le cgroup. Restart=on-failure avec RestartSec= remplace un script wrapper, et StartLimitBurst= empêche une boucle de crash de s’exécuter indéfiniment.
  • inetd est devenu une unité .socket placée à côté de l’unité .service.
  • ulimit est devenu MemoryMax=, CPUQuota= et TasksMax=.
  • La ligne su - appuser -c d’un script init est devenue User=, NoNewPrivileges=yes et ProtectSystem=strict. Exécuter un service avec un utilisateur non privilégié constitue donc la configuration par défaut d’une unité, et non une tâche supplémentaire.
[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

Une ligne de ce fichier correspond à l’erreur que tout le monde commet une fois. Requires=postgresql.service exprime une dépendance, pas un ordre de démarrage : cette directive indique que votre unité échoue si Postgres échoue, mais elle n’indique pas de démarrer Postgres en premier. Sans After=postgresql.service, les deux services démarrent en même temps et votre service se connecte à un port sur lequel aucun processus n’écoute encore. Ces deux directives sont volontairement distinctes, car vous pouvez parfois avoir besoin de l’une sans l’autre. ProtectSystem=strict monte le système de fichiers en lecture seule pour ce service. C’est pourquoi StateDirectory= est présent : il fournit au service un chemin accessible en écriture sous /var/lib.

L’exemple le plus clair de ce qui a changé depuis 2005 sur un serveur 2026 est SSH sur Ubuntu 24.04, qui fournit systemd 255 en août 2026. ssh.service est activé par socket par défaut : ssh.socket possède le socket en écoute, et sshd démarre lorsqu’une connexion arrive. Port 2222 dans /etc/ssh/sshd_config n’a donc aucun effet, car sshd n’est pas le processus qui a ouvert le port. La modification doit être effectuée dans l’unité socket.

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

La directive ListenStream= vide la valeur héritée de l’unité fournie par le paquet. Si vous l’omettez, les deux ports sont utilisés, car systemd ajoute les valeurs à une liste au lieu de les remplacer. Appliquez ensuite la configuration et vérifiez le résultat, en gardant une deuxième session SSH ouverte pendant toute l’opération :

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

ss doit afficher un seul socket sur le port 2222, possédé par systemd et non par sshd. C’est la conception de launchd, vingt ans plus tard, sur votre VPS. Si vous préférez l’ancien comportement, sudo systemctl disable --now ssh.socket suivi de sudo systemctl enable --now ssh.service vous fournit un sshd exécuté en continu, qui lit de nouveau Port dans sa propre configuration.

Les détails que vous rencontrerez dépendent de la release utilisée. Il est donc utile de connaître la différence entre une release LTS et une release intermédiaire d’Ubuntu avant de planifier une mise à niveau. Sur plusieurs machines, le fait qu’un fichier d’unité soit identique partout explique pourquoi gérer plusieurs serveurs depuis un même emplacement relève désormais de la configuration et non des scripts shell. Lorsque vous écrivez vos propres unités, le couple service et timer remplit le rôle que vous auriez réparti entre un script init et une ligne cron en 2009.

FAQ

Pourquoi les distributions Linux ont-elles remplacé SysV init par systemd ?

Pour deux raisons d’ingénierie et une raison de maintenance. SysV init ordonnait les services selon leur nom de fichier, ce qui définit une position et non une dépendance. Il perdait aussi la trace de tout daemon qui se détachait de son parent. C’est pourquoi des fichiers PID obsolètes pouvaient arrêter le mauvais processus. systemd a résolu l’ordonnancement avec l’activation par socket et les directives de dépendance. Il a résolu le suivi avec les groupes de contrôle. La raison liée à la maintenance a accéléré l’adoption : un seul fichier d’unité fonctionne sur toutes les distributions. Les projets upstream fournissaient donc un fichier .service, et les mainteneurs des distributions ont cessé d’écrire un script shell par paquet. Fedora 15 est passée à systemd en mai 2011, et Ubuntu 15.04 a été le dernier grand réfractaire, en avril 2015.

systemd est-il un binaire monolithique ?

Non. L’arborescence des sources construit de nombreux programmes distincts. Le PID 1 est /usr/lib/systemd/systemd, tandis que journald, logind et udevd sont des processus distincts avec leurs propres binaires. Exécutez ls /usr/lib/systemd/ pour les afficher sur votre propre machine. La critique qui subsiste concerne le couplage des versions plutôt que la taille du binaire : ces programmes sont publiés ensemble et partagent des interfaces privées. Les distributions ont donc tendance à les adopter comme un ensemble, et des logiciels comme GNOME ont fini par attendre spécifiquement logind.

Puis-je encore utiliser Linux sans systemd ?

Oui. Devuan fournit sysvinit, Gentoo utilise OpenRC par défaut, Void utilise runit, Alpine utilise busybox init avec OpenRC, et Slackware conserve des scripts de style BSD. Le coût est le travail de compatibilité. Les logiciels de bureau qui attendent logind ont besoin de elogind, c’est-à-dire du logind de systemd maintenu comme paquet autonome. De plus en plus de logiciels serveur ne fournissent désormais qu’un fichier .service. Vous devez donc écrire et maintenir vous-même le script de démarrage.

Pourquoi le journal utilise-t-il un format binaire plutôt qu’un simple fichier texte ?

Parce que journald stocke des champs structurés avec un index. Cela permet le filtrage par unité, le filtrage par priorité et l’enregistrement de métadonnées que le programme émetteur ne peut pas falsifier. journald enregistre lui-même l’unité, le cgroup et l’UID réel, au lieu de faire confiance à la ligne de journal. En contrepartie, vous avez besoin de journalctl pour le lire, y compris depuis un système de secours. Dans ce cas, indiquez-lui le disque monté avec journalctl --directory /mnt/var/log/journal. Si vous voulez également du texte, définissez ForwardToSyslog=yes dans /etc/systemd/journald.conf.

Qu’est-ce qui a remplacé la modification de mon script /etc/init.d ?

Les fichiers drop-in. Ne modifiez pas l’unité dans /usr/lib/systemd/system/, car une mise à niveau du paquet l’écraserait. Exécutez sudo systemctl edit nginx.service : systemd crée alors /etc/systemd/system/nginx.service.d/override.conf, qui est fusionné avec l’unité fournie par le paquet. systemctl cat nginx.service affiche le résultat fusionné, et systemd-delta répertorie tous les overrides présents sur la machine. Après toute modification manuelle, exécutez sudo systemctl daemon-reload. Sinon, la commande suivante affiche Warning: The unit file, source configuration file or drop-ins of nginx.service changed on disk..

#systemd#linux#init#sysvinit#history