systemd 260 supprime SysV. Et après ?
systemd 260 supprime le sysv-generator, rc-local.service et systemd-sysv-install. Chaque script /etc/init.d et chaque /etc/rc.local de votre parc est désormais en sursis. Voici comment les recenser et les convertir proprement.
- systemd
- Linux
- Administration système
- Migration
Pendant une quinzaine d’années, systemd a traduit discrètement vos scripts /etc/init.d en services, à chaque démarrage, sans jamais vous demander d’y prêter attention. En version 260, publiée en mars 2026, il a cessé de le faire. Le systemd-sysv-generator, le systemd-rc-local-generator avec son rc-local.service, et le point d’accroche systemd-sysv-install qui se cachait derrière systemctl enable ont été purement et simplement supprimés. Rien n’a changé dans vos scripts. Ce qui a changé, c’est que plus personne ne les lit.
![Image de couverture en deux panneaux : à gauche, un panneau intitulé « DELETED IN systemd 260 » listant systemd-sysv-generator, systemd-rc-local-generator, rc-local.service et systemd-sysv-install ; à droite, un panneau intitulé « WHAT REPLACES WHAT » avec quatre correspondances avant/après : start-stop-daemon --background devient Type=exec, --make-pidfile devient aucun fichier PID du tout, --chuid acme devient User=acme, et /etc/rc[2-5].d/S20name devient WantedBy=multi-user.target.](/og/migrate-sysv-init-to-systemd-units.png)
Voici la migration, écrite pour quelqu’un qui doit la mener sur de vrais serveurs plutôt que la contempler de loin : comment recenser tous les scripts encapsulés sur un parc entier grâce à un champ de journal que presque personne ne connaît, ce que le générateur écrivait exactement pour que vous puissiez le recopier au lieu de le deviner, la conversion champ par champ d’un script init réaliste, l’erreur de Type= qui fait rapporter un succès à une unit dont le démon est mort, et les commandes qui ont remplacé init 3. Chaque numéro de version et chaque directive cités ici ont été vérifiés dans le fichier NEWS de systemd et dans le code source, aux tags où le code supprimé existait encore — pas de mémoire.
L’unit n’est pas en échec. Elle n’existe pas
Le symptôme est inhabituellement déroutant, parce que c’est une absence et non une erreur. Votre service n’apparaît pas en échec. Il n’apparaît pas du tout. systemctl status répond Unit could not be found alors que le script est bien sur le disque, exécutable, inchangé depuis 2015. Et c’est le comportement correct : le script n’a jamais été une unit. Un générateur en fabriquait une dans /run/systemd/generator.late à chaque démarrage, et ce répertoire est désormais vide.[v260]
| Ce que vous voyez | Ce que cela signifie | Où c’est traité |
|---|---|---|
Unit x.service could not be found, alors que /etc/init.d/x existe et est exécutable | Le générateur qui inventait cette unit a été supprimé. Rien n’est cassé ; plus rien ne traduit. | Ce que le générateur écrivait |
| Un service qui démarrait à chaque boot depuis des années ne démarre simplement plus, et rien n’apparaît dans le journal | Même cause, vue depuis le démarrage plutôt que depuis la ligne de commande. Il n’y a pas d’unit en échec parce qu’il n’y a pas d’unit. | Convertir un script |
/etc/rc.local est toujours exécutable et ne s’exécute plus | systemd-rc-local-generator et rc-local.service ont été supprimés en même temps que le générateur SysV. | Remplacer /etc/rc.local |
systemctl enable sur un ancien service ne fait plus rien d’utile | systemd-sysv-install, le point d’accroche qui transmettait enable/disable à chkconfig ou update-rc.d, a disparu. | Ce qu’un paquet doit livrer |
telinit: command not found, ou init 3 ne fait rien | Supprimé en 258, deux versions plus tôt et pour une autre raison : c’est la notion de runlevel qui a été abandonnée, pas seulement les scripts. | init 3, telinit et runlevel |
| Une unit convertie se déclare active, mais le processus a disparu — ou se déclare en échec alors que le démon tourne | Le Type= est faux pour un démon qui fait un fork. C’est la conversion ratée la plus fréquente. | Type=forking et fichiers PID |
# After the upgrade, the service that has started at boot for eleven years
# is simply not there. Not failed - not there.
systemctl status acme-collector
# Unit acme-collector.service could not be found.
ls -l /etc/init.d/acme-collector
# -rwxr-xr-x 1 root root 1284 Mar 14 2015 /etc/init.d/acme-collector
# ^ the script is still on disk, still executable, still correct.
# The script was never a service. A generator turned it into one on every
# boot, and the generator is gone. Confirm which systemd you are on:
systemctl --version | head -1
# systemd 260 (260.2-1)
# And confirm the generator really is absent rather than just failing:
ls /usr/lib/systemd/system-generators/ | grep -E 'sysv|rc-local'
# (no output on 260 and later; on 259 and earlier you get one or two lines)
# The same disappearance, seen from the other end - nothing was generated:
ls /run/systemd/generator.late/
# (empty, or missing your unit)Les générateurs s’exécutent avant que le gestionnaire ne charge la moindre unit, à chaque démarrage et à chaque daemon-reload, et leur production vit en tmpfs. C’est pour cela que rien ne semble différent sur le disque après la mise à niveau, et qu’il n’y a rien à réparer : aucun fichier n’est cassé nulle part, il manque seulement une étape de traduction qui n’a plus lieu. Tout ce qui suit suppose que vous avez un shell sur la machine et que vous pouvez la redémarrer une fois à la fin. Rien dans cet article ne modifie l’état du système tant que vous ne l’avez pas décidé ; les commandes d’inventaire ne font que lire.[generator]
Ce qui a été supprimé, et exactement quand
Quatre versions distinctes sont en jeu, et les confondre explique pourquoi tant de conseils sur le sujet sont faux. Les scripts de service ont été déclarés dépréciés dès la version 255. Ce que la version 258 a fait, en septembre 2025, c’est supprimer les interfaces System V d’état système — /dev/initctl, les commandes initctl, runlevel et telinit, le pilotage de l’état par init 3, les units runlevel[0-6].target, et l’enregistrement des transitions de runlevel dans utmp et wtmp. La version 259 est revenue partiellement sur l’une de ces suppressions. La version 260 a supprimé les scripts de service eux-mêmes.[news][v255][v258]
| Version | Date | Ce qui s’est passé |
|---|---|---|
| 258 | Septembre 2025 | Les notes de version rappellent la dépréciation des scripts de service System V, avec suppression alors prévue pour la 259 — plus tard repoussée à la 260. Séparément et immédiatement : /dev/initctl, les commandes initctl, runlevel et telinit, les changements d’état à la init 3 et les units runlevel[0-6].target sont tous supprimés, et les transitions de runlevel cessent d’être enregistrées dans utmp/wtmp. La prise en charge de cgroup v1 est abandonnée dans la même version. |
| 259 | Décembre 2025 | runlevel[0-6].target restaurées, mais seulement si la distribution compile avec -Dcompat-sysv-interfaces=yes. Les commandes supprimées ne reviennent pas. C’est la version qu’embarque Ubuntu 26.04 LTS. |
| 260 | Mars 2026 | Prise en charge des scripts de service System V supprimée. Supprimés : systemd-sysv-generator ; systemd-rc-local-generator et rc-local.service ; systemd-sysv-install. Les options de compilation -Drc-local=, -Dsysvinit-path= et -Dsysvrcnd-path= deviennent dépréciées. Le noyau minimum passe à 5.10, la glibc à 2.34, OpenSSL à 3.0. |
| 261 et suivantes | 2026 | Plus aucun changement lié à SysV ; la couche de compatibilité est simplement absente. Les options de compilation dépréciées devraient disparaître dans une version future. |
Ce retour en arrière partiel de la 259 compte si vous administrez un parc hétérogène : les units runlevel[0-6].target sont revenues, mais uniquement comme une option que la distribution doit activer à la compilation, et les commandes, elles, ne sont pas revenues du tout. Donc systemctl isolate runlevel3.target peut fonctionner sur l’une de vos machines et pas sur la suivante, selon la façon dont chaque distribution a compilé son paquet. N’en faites pas une habitude. Là où vos serveurs se situent réellement dépend de la version de systemd qu’ils embarquent :[v259][v260]
| Où vous en êtes | systemd | Scripts SysV et rc.local |
|---|---|---|
| Debian 12 bookworm | 252 | Les deux fonctionnent. Aucun avertissement par script dans le journal. |
| Debian 13 trixie | 257 | Les deux fonctionnent, et le journal avertit déjà pour chaque script encapsulé. |
| Ubuntu 24.04 LTS | 255 | Les deux fonctionnent, et le journal avertit déjà pour chaque script encapsulé. |
| Ubuntu 26.04 LTS | 259 | Les deux fonctionnent, et le journal avertit pour chaque script encapsulé — comme depuis la 24.04. Canonical indique que c’est la dernière Ubuntu avec la compatibilité SysV. |
| Ubuntu 26.10 | 260 ou ultérieure | Supprimés. Les scripts restent sur le disque et ne sont jamais lus. |
| RHEL 9 / RHEL 10 | 252 / 257 | Les deux fonctionnent pendant toute la vie de la version. RHEL 9 reste silencieux ; RHEL 10 avertit pour chaque script encapsulé et ses notes de version portent déjà la dépréciation. Le changement arrive avec la prochaine version majeure. |
| Distributions en rolling release | 260 ou ultérieure | Déjà supprimés. Vérifiez avec systemctl --version plutôt que de le supposer. |
Les dates rendent la chose concrète plutôt que théorique. Les notes de version de Canonical indiquent qu’Ubuntu 26.04 LTS — qui embarque systemd 259 — est la dernière version d’Ubuntu offrant la compatibilité System V, et que le changement prend effet en 26.10. Cette version est prévue pour octobre 2026. Si vous suivez les versions intermédiaires, ou si vous avez quoi que ce soit sur une distribution en rolling release, l’échéance se compte en semaines et non en années. Si vous êtes sur une LTS ou sur une distribution d’entreprise, vous avez jusqu’à votre prochaine montée de version majeure — c’est-à-dire précisément le moment où vous avez le moins envie de découvrir des scripts shell sans propriétaire.[ubuntu2604][ubuntu2610][rhel10]
Recenser vos scripts avant que la mise à niveau ne le fasse
Faites l’inventaire en premier, et faites-le pendant que le générateur tourne encore, parce que c’est le générateur lui-même qui vous donnera la réponse. Depuis la version 255, chaque fois que systemd encapsule un script, il émet un avertissement structuré porteur d’un identifiant de message stable, plus deux champs de journal personnalisés : SYSVSCRIPT= avec le chemin, et UNIT= avec le nom qu’il a inventé. Cela transforme un audit de parc entier en une requête exacte, au lieu d’une supposition bricolée à coups de ls et d’optimisme.[messages][v255][journalctl]
#!/usr/bin/env bash
# Inventory every SysV script this host still depends on. Run it BEFORE the
# upgrade, on every machine, and keep the output.
echo '== 1. scripts systemd is currently wrapping =========================='
# systemd 255 and later log a structured warning for every script they wrap.
# The message ID is stable, so this is an exact list rather than a guess.
journalctl -b -o json --output-fields=SYSVSCRIPT,UNIT \
MESSAGE_ID=a8fa8dacdb1d443e9503b8be367a6adb 2>/dev/null \
| python3 -c 'import sys,json
for l in sys.stdin:
d = json.loads(l)
print("%-28s %s" % (d.get("UNIT","?"), d.get("SYSVSCRIPT","?")))'
echo '== 2. fallback for systemd 254 and older ============================='
# Older systemd wraps silently. Ask the manager which units came from a
# generator instead: generated units live under /run/systemd/generator.late
# and carry a SourcePath= pointing back at the script. This also catches
# scripts that were wrapped before the current boot's journal starts.
systemctl list-units --type=service --all --no-legend --plain \
| awk '{print $1}' \
| while read -r u; do
frag=$(systemctl show -p FragmentPath --value "$u" 2>/dev/null)
case "$frag" in
/run/systemd/generator*)
src=$(systemctl show -p SourcePath --value "$u" 2>/dev/null)
[ -n "$src" ] && printf '%-28s %s\n' "$u" "$src" ;;
esac
done
echo '== 3. scripts on disk with no NATIVE unit behind them ================'
# Note the test: `systemctl cat` succeeds for a generated unit too, so asking
# whether the unit exists tells you nothing while the generator is running.
# Ask where the definition lives instead.
for f in /etc/init.d/*; do
[ -f "$f" ] && [ -x "$f" ] || continue
n=$(basename "$f"); n=${n%.sh}
frag=$(systemctl show -p FragmentPath --value "$n.service" 2>/dev/null)
case "$frag" in
/etc/systemd/system/*|/usr/lib/systemd/system/*|/lib/systemd/system/*) ;;
*) echo "no native unit: $f" ;;
esac
done
echo '== 4. runlevel wiring (this is what set the boot order) ============='
ls -l /etc/rc[1-5].d/S* 2>/dev/null | awk '{print $9, $10, $11}'
echo '== 5. rc.local ======================================================'
ls -l /etc/rc.local 2>/dev/null && \
{ [ -x /etc/rc.local ] && echo 'executable: it runs at boot today'; }Lancez l’inventaire sur chaque hôte, pas sur un hôte représentatif. D’expérience, les scripts qui survivent jusqu’en 2026 ne sont jamais ceux que connaît un dépôt de gestion de configuration — ce sont celui qu’un prestataire a posé une fois, l’agent d’un boîtier constructeur, le wrapper de sauvegarde antérieur à l’équipe actuelle. Ce sont exactement les hôtes auxquels personne ne pense, et exactement les services dont on remarque l’absence une semaine plus tard.
Trois détails de cette requête méritent d’être intégrés. L’étape 1 ne voit que le journal du démarrage courant : un hôte dont l’avertissement a été chassé par la rotation ne remontera rien et devra quand même passer par l’étape 2. Systemd n’a jamais encapsulé que des fichiers réguliers exécutables du répertoire init : un script laissé en mode 644 était déjà mort et il ne faut pas le ressusciter. Et une unit native l’emportait toujours sur un script du même nom : le générateur ignorait explicitement tout script pour lequel une vraie unit existait déjà. Cette dernière règle est ce qui rend toute la migration sûre à mener de façon incrémentale — vous pouvez installer la nouvelle unit pendant que le script est encore présent et encore activé, et le script cesse simplement d’être consulté.[gensrc]
Ce que le générateur écrivait pour vous, en silence
Avant d’écrire la moindre unit, prenez celle que le générateur produit et lisez-la. Ce n’est pas une approximation grossière, c’est une traduction déterministe, et c’est ce qui se rapproche le plus d’une spécification du comportement réel de votre script au démarrage. Capturez-la avec systemctl cat tant que vous le pouvez encore — après la mise à niveau cette information a disparu, et c’est en la reconstruisant depuis l’en-tête LSB qu’on introduit des bugs d’ordonnancement.[gensrc][genman]
# Capture the generated unit BEFORE you upgrade. It is the specification for
# the unit you are about to write, produced by the only thing that ever read
# your init script correctly.
systemctl cat acme-collector.service > ~/acme-collector.generated.service
# What comes out looks like this - and every line of it is decided by the
# generator source, not by convention:
# /run/systemd/generator.late/acme-collector.service
# Automatically generated by systemd-sysv-generator
[Unit]
Documentation=man:systemd-sysv-generator(8)
SourcePath=/etc/init.d/acme-collector
Description=LSB: ACME metrics collector
Before=multi-user.target
Before=multi-user.target
Before=multi-user.target
Before=graphical.target
After=remote-fs.target
After=network-online.target
After=time-sync.target
After=postgresql.service
Wants=network-online.target
[Service]
Type=forking
Restart=no
TimeoutSec=5min
IgnoreSIGPIPE=no
KillMode=process
GuessMainPID=no
RemainAfterExit=no
PIDFile=/var/run/acme-collector.pid
SuccessExitStatus=5 6
ExecStart=/etc/init.d/acme-collector start
ExecStop=/etc/init.d/acme-collector stop
ExecReload=/etc/init.d/acme-collector reload
# Yes, Before=multi-user.target really is repeated. The rc2.d, rc3.d and rc4.d
# symlinks each append it and nothing deduplicates the list. Harmless, but it
# tells you the file was machine-written and how.
#
# Note what is NOT there: no [Install] section. The generator wired the unit in
# by dropping symlinks next to it - into multi-user.target.wants for rc2-rc4,
# and into graphical.target.wants for rc5 - which is why `systemctl is-enabled`
# on one of these was never a straight answer.Voici maintenant le point qui surprend la plupart des gens, et que vous pouvez vérifier dans les sources mêmes du générateur. Default-Start: et Default-Stop: dans l’en-tête LSB n’ont jamais été lus. Le générateur n’analyse que Provides:, Required-Start:, Should-Start:, X-Start-Before:, X-Start-After:, les deux champs de description, ainsi que les commentaires de style Red Hat # pidfile: et # description:. Le câblage des runlevels venait entièrement des liens symboliques S??nom dans /etc/rc[1-5].d. Si vous maintenez Default-Start depuis dix ans, vous maintenez un commentaire.[lsb][exitstatus]
| Dans le script init | Ce que le générateur en faisait | Écrivez ceci dans votre unit |
|---|---|---|
Provides: nom | S’il nomme un service, un lien symbolique d’alias. S’il nomme une $facility, Before= plus Wants= sur cette target — à sens unique. S’il répète le nom du fichier, ignoré. | Alias= dans [Install], ou rien |
Required-Start: $network | After=network-online.target et Wants=network-online.target, parce que cette target est inerte si personne ne la tire | Wants= + After=network-online.target |
Required-Start: $remote_fs | After=remote-fs.target | After=remote-fs.target |
Required-Start: $named | After=nss-lookup.target | After=nss-lookup.target |
Required-Start: $portmap | After=rpcbind.target | After=rpcbind.target |
Required-Start: $time | After=time-sync.target | After=time-sync.target |
Required-Start: $local_fs ou $syslog | Rien. Les deux étaient ignorés : sous systemd ils sont déjà satisfaits avant qu’un service ordinaire ne démarre. | Rien |
Should-Start: foo | After=foo.service — ordonnancement seulement, jamais une exigence | After=foo.service, sans Requires= |
X-Start-Before: foo | Before=foo.service | Before=foo.service |
Default-Start: / Default-Stop: | Rien du tout. Jamais analysé. Le câblage des runlevels venait des liens symboliques S?? dans /etc/rc[1-5].d. | WantedBy=multi-user.target dans [Install] |
Lien symbolique dans /etc/rc2.d, rc3.d, rc4.d | Before=multi-user.target et un lien symbolique wants dedans | WantedBy=multi-user.target |
Lien symbolique dans /etc/rc5.d | Before=graphical.target et un lien symbolique wants dedans | WantedBy=graphical.target |
Lien symbolique dans /etc/rc1.d | Before=rescue.target et un lien symbolique wants vers celle-ci — le générateur traitait rc1.d exactement comme rc2 à rc5 | Presque jamais ce que vous voulez ; laissez tomber |
# pidfile: /chemin (style Red Hat) | PIDFile=/chemin, et RemainAfterExit=no. Sans aucune ligne pidfile, RemainAfterExit=yes | Supprimez les deux. Utilisez Type=exec à la place |
### BEGIN INIT INFO présent | SuccessExitStatus=5 6 — les codes LSB pour « non installé » et « non configuré » | Seulement si vous sortez encore avec ces codes |
Une ligne d’usage contenant |reload} ou similaire | ExecReload=/etc/init.d/x reload. Pas de ligne d’usage, pas de verbe reload. | ExecReload=/bin/kill -HUP $MAINPID |
Deux correspondances de facilités dans ce tableau méritent un second regard, car elles sont la source habituelle du « ça marchait sous SysV et maintenant c’est une condition de concurrence ». $network ne devenait pas network.target ; il devenait network-online.target, et le générateur ajoutait à la fois After= et Wants=, parce que cette target ne fait rien si personne ne la tire activement. De leur côté, $local_fs et $syslog ne correspondaient à rien du tout — ils étaient purement ignorés, au motif raisonnable que sous systemd les deux sont déjà garantis au moment où un service ordinaire démarre. Recopiez ces décisions dans votre unit ; n’en improvisez pas de nouvelles.[special]
Convertir un script, champ par champ
Voici un script réaliste — pas un exemple jouet. Il a un en-tête LSB, un commentaire de fichier PID à la mode Red Hat, un appel à start-stop-daemon qui passe le processus en arrière-plan et écrit le fichier PID à la place du démon, un verbe reload, et la ligne d’usage qui est la seule raison pour laquelle le générateur produisait un ExecReload=. La plupart des scripts réels que l’on rencontre sont pires que celui-ci.
#!/bin/sh
### BEGIN INIT INFO
# Provides: acme-collector
# Required-Start: $remote_fs $network $time
# Required-Stop: $remote_fs $network
# Should-Start: postgresql
# Default-Start: 2 3 4 5
# Default-Stop: 0 1 6
# Short-Description: ACME metrics collector
### END INIT INFO
# pidfile: /var/run/acme-collector.pid
DAEMON=/opt/acme/bin/collector
PIDFILE=/var/run/acme-collector.pid
RUNAS=acme
OPTS="--config /etc/acme/collector.conf"
case "$1" in
start)
start-stop-daemon --start --quiet --background --make-pidfile \
--pidfile "$PIDFILE" --chuid "$RUNAS" --exec "$DAEMON" -- $OPTS
;;
stop)
start-stop-daemon --stop --quiet --pidfile "$PIDFILE" --retry 30
rm -f "$PIDFILE"
;;
reload)
kill -HUP "$(cat "$PIDFILE")"
;;
restart)
"$0" stop; sleep 1; "$0" start
;;
*)
echo "Usage: $0 {start|stop|restart|reload}" >&2
exit 2
;;
esac
exit 0Et voici ce qui le remplace. Lisez les commentaires plutôt que les directives : les décisions intéressantes sont celles qui portent sur ce qu’il ne faut pas reporter. Le forking disparaît. Le fichier PID géré à la main disparaît. Le wrapper start-stop-daemon disparaît, parce que tout ce qu’il faisait — abaisser les privilèges, passer en arrière-plan, suivre le processus — est devenu une directive.[service][exec]
# /etc/systemd/system/acme-collector.service
#
# Everything above the blank line inside [Service] is the translation of the
# init script. Everything below it is what the wrapper could never give you.
[Unit]
Description=ACME metrics collector
Documentation=https://example.internal/acme/collector
# $network in the old header became network-online.target - and that target
# only does something if a unit actively pulls it in, hence the Wants=.
Wants=network-online.target
After=network-online.target
# "Should-Start: postgresql" was ordering only, never a requirement. Keep it
# that way: After= without Requires= means a database outage does not cascade.
After=postgresql.service
[Service]
Type=exec
User=acme
Group=acme
# The single most important line of the migration: stop the daemon forking.
# Almost every daemon has a flag for this - --foreground, -f, -D, --no-detach,
# --nodaemon. Find it and the PID file problem disappears with it.
ExecStart=/opt/acme/bin/collector --config /etc/acme/collector.conf --foreground
ExecReload=/bin/kill -HUP $MAINPID
Restart=on-failure
RestartSec=5s
TimeoutStopSec=30s
# /var/run/acme-collector.pid was in /run all along; let systemd own it.
RuntimeDirectory=acme-collector
StateDirectory=acme-collector
ConfigurationDirectory=acme
NoNewPrivileges=yes
PrivateTmp=yes
PrivateDevices=yes
ProtectSystem=strict
ProtectHome=yes
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectControlGroups=yes
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
RestrictNamespaces=yes
RestrictSUIDSGID=yes
LockPersonality=yes
MemoryDenyWriteExecute=yes
SystemCallArchitectures=native
SystemCallFilter=@system-service
# Empty means "no capabilities at all". If the daemon binds a port below 1024,
# use CapabilityBoundingSet=CAP_NET_BIND_SERVICE plus the matching
# AmbientCapabilities= instead of dropping this line.
CapabilityBoundingSet=
[Install]
WantedBy=multi-user.target| Dans le script | Dans l’unit | Remarque |
|---|---|---|
start-stop-daemon --background ou & | Type=exec et l’option premier plan du démon | Tout l’intérêt est là. Ne mettez rien en arrière-plan. |
--make-pidfile, echo $! > … | supprimez-le | systemd connaît le PID puisque c’est lui qui a forké le processus. |
--chuid, su - user -c, runuser | User= et Group= | S’applique à tous les descendants, pas seulement au premier. |
mkdir -p /run/x; chown … | RuntimeDirectory=x | Créé avant le démarrage, supprimé après l’arrêt, avec le bon propriétaire. |
mkdir -p /var/lib/x | StateDirectory=x | Survit aux redémarrages ; utilisez CacheDirectory=/LogsDirectory= pour les autres. |
ulimit -n 65535 | LimitNOFILE=65535 | Un ulimit shell dans le script s’appliquait au shell, parfois pas au démon. |
export FOO=bar | Environment= ou EnvironmentFile= | EnvironmentFile=-/etc/default/x conserve vos fichiers de configuration existants. |
nice -n 10, ionice | Nice=10, IOSchedulingClass= | Ou allez plus loin avec CPUWeight= et IOWeight=. |
cd /opt/x | WorkingDirectory=/opt/x | |
>> /var/log/x.log 2>&1 | supprimez-le | La sortie part vers le journal, étiquetée avec l’unit. Aucune rotation à écrire. |
sleep 5; check_if_up | Type=notify dans le démon, ou un contrôle de santé dans une unit séparée | Un sleep dans un verbe start est une condition de concurrence à mèche plus longue. |
restart) stop; sleep 1; start | supprimez-le | systemctl restart existe et attend correctement. |
Bloc status) | supprimez-le | systemctl status et is-active rapportent l’état réel. |
trap / nettoyage à l’arrêt | ExecStop=, ou mieux, gérez SIGTERM dans le démon | TimeoutStopSec= borne l’attente avant le SIGKILL. |
La correspondance en termes généraux, pour les idiomes que vous rencontrerez vraiment. Quand une ligne dit « supprimez-le », ce n’est pas une simplification : le comportement est fourni par le gestionnaire, et le réimplémenter en shell est la meilleure façon de se retrouver avec deux entités qui se disputent le même processus.[unit]
# Cut over on a system that still has the generator, so you can roll back by
# doing nothing. Order matters here.
# 1. Check the file before systemd ever loads it.
sudo systemd-analyze verify /etc/systemd/system/acme-collector.service
# 2. Install it and reload. The native unit now WINS over the script: the
# generator skips any script that already has a unit of the same name.
sudo systemctl daemon-reload
# 3. Prove which one is live before you touch the running process.
systemctl show -p FragmentPath -p SourcePath --value acme-collector.service
# /etc/systemd/system/acme-collector.service
# <- empty SourcePath = no longer generated. Good.
# 4. Restart through systemd, not through the script.
sudo systemctl restart acme-collector.service
systemctl status acme-collector.service --no-pager
# 5. Enable it explicitly. The generator's implicit wiring is not inherited.
sudo systemctl enable acme-collector.service
systemctl is-enabled acme-collector.service # -> enabled
# 6. Retire the old wiring. Do NOT delete the script yet - move it aside, so
# a rollback is one mv away for the length of the change window.
sudo rm -f /etc/rc[0-6].d/[SK]??acme-collector
sudo mv /etc/init.d/acme-collector /root/retired-init.d-acme-collector
# 7. The only real test: reboot, then read the boot rather than the status.
sudo systemctl reboot
journalctl -b -u acme-collector.service
systemd-analyze blame | head -20Faites la bascule dans cet ordre et un retour arrière coûte un mv. L’étape de loin la plus importante est la troisième : systemctl show -p FragmentPath -p SourcePath vous dit sans ambiguïté laquelle des deux définitions est active. Un SourcePath non vide signifie que vous regardez toujours l’unit générée, et que votre nouveau fichier a une collision de nom, une erreur de syntaxe, ou se trouve dans le mauvais répertoire.[systemctl]
Remplacer /etc/rc.local sans en garder le pire défaut
/etc/rc.local mérite sa propre section, parce que c’est celui que tout le monde repousse, et parce que l’unit de compatibilité qu’il perd avait une sémantique franchement étrange, qu’il vaut mieux connaître avant de la reporter telle quelle. Le vrai rc-local.service utilisait Type=forking avec GuessMainPID=no, RemainAfterExit=yes et TimeoutSec=infinity, ordonné seulement après network.target, et ne se tirait lui-même que si le fichier était exécutable. La page de manuel amont prenait soin de prévenir que cet ordonnancement ne signifie pas que le réseau est utilisable, et que l’ensemble existait pour la compatibilité avec certains systèmes System V, et non comme une conception que quiconque recommanderait.[rclocalunit][rclocalman]
# /etc/rc.local was never one thing. Read yours first and split it: a mount,
# a sysctl, a firewall rule and a background daemon are four different units,
# and only the last one belongs in a service.
# --- 1. the direct replacement, when it really is one script ---------------
# /etc/systemd/system/local-startup.service
[Unit]
Description=Local startup commands (former /etc/rc.local)
ConditionFileIsExecutable=/usr/local/sbin/local-startup
# The old rc-local.service used After=network.target, which upstream's own
# manual page warns does not mean the network is usable. If your script talks
# to the network, use network-online.target and pull it in.
Wants=network-online.target
After=network-online.target
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/usr/local/sbin/local-startup
# The original had TimeoutSec=infinity. Keeping that means a hung script hangs
# the boot forever with no message. Pick a number you would accept waiting.
TimeoutStartSec=90s
[Install]
WantedBy=multi-user.target
# --- 2. the pieces that should not be a service at all ---------------------
# sysctl: /etc/sysctl.d/90-local.conf
# modules: /etc/modules-load.d/local.conf
# files/dirs: /etc/tmpfiles.d/local.conf
# mounts: /etc/fstab or a .mount unit
# periodic: a .timer, not a sleep loop
# --- 3. install it --------------------------------------------------------
sudo install -m 0755 /etc/rc.local /usr/local/sbin/local-startup
sudo systemctl daemon-reload
sudo systemctl enable --now local-startup.service
systemctl status local-startup.service --no-pager
# On SELinux systems relabel the script, exactly as the old generator's manual
# page told you to do for /etc/rc.local:
command -v restorecon >/dev/null && sudo restorecon -v /usr/local/sbin/local-startupDeux habitudes valent la peine d’être abandonnées pendant que vous y êtes. Ne conservez pas TimeoutSec=infinity : cela transforme une commande bloquée en un démarrage qui ne se termine jamais, silencieusement, sans aucune unit en échec à montrer du doigt. Et ne gardez pas tout dans un seul script. Un rc.local, c’est en général quatre choses sans rapport regroupées parce qu’il n’y avait qu’un seul point d’accroche disponible — un sysctl, un montage, une règle de pare-feu et quelque chose qui aurait dû être un démon. En units séparées, elles peuvent être ordonnées, réessayées et désactivées indépendamment, et quand l’une casse, vous savez laquelle.[special]
Type=forking, fichiers PID et démons récalcitrants
Voici l’erreur qui coûte une nuit, et elle ne s’annonce pas. Si le démon fait un fork et que l’unit déclare Type=simple, systemd suit le parent, le voit sortir en quelques millisecondes, puis soit tue les enfants survivants comme des processus orphelins, soit déclare le démarrage réussi alors que rien ne tourne. Si le démon fait un fork et que l’unit déclare Type=forking mais que le fichier PID arrive trop tard, systemd perd le processus principal et rapporte MainPID=0 : l’unit paraît active, Restart= ne se déclenche jamais, et l’extinction laisse des processus derrière elle.[daemon][service]
# The failure mode nobody warns you about: the unit reports "active" and the
# daemon is dead, or reports "failed" while the daemon is happily running.
# Both come from getting Type= wrong.
# --- diagnosis -------------------------------------------------------------
systemctl show -p Type -p MainPID -p PIDFile -p ControlGroup --value myd.service
# forking
# 0 <- systemd has no main process: it lost the daemon
# /var/run/myd.pid
# /system.slice/myd.service
# If MainPID is 0 while the process exists, systemd is not supervising it.
# Restart=, watchdogs, and clean shutdown are all silently not working.
systemd-cgls -u myd.service # who is actually inside the unit's cgroup
# --- fix 1 (preferred): stop forking --------------------------------------
# Type=exec: systemd considers the unit started once the binary has been
# executed. No PID file, no race, no double-fork, no GuessMainPID guesswork.
# ExecStart=/usr/sbin/myd --foreground
# Type=exec
# --- fix 2 (best, if the daemon supports it): tell systemd when you are up --
# Type=notify with sd_notify(READY=1) from the daemon, or Type=notify-reload
# if it can also signal that a reload finished. These - together with Type=dbus
# for D-Bus services - are the only readiness protocols precise enough that
# After= on a dependent unit means "the port is actually accepting".
# Type=notify
# NotifyAccess=main
# --- fix 3 (last resort): keep forking, but do it correctly ---------------
# PIDFile= must be an absolute path under /run, and the daemon must write it
# BEFORE the parent exits. Anything else is a race you will lose under load.
# Type=forking
# PIDFile=/run/myd/myd.pid
# RuntimeDirectory=myd
# --- do not do this -------------------------------------------------------
# Type=simple with a daemon that forks. systemd will track the parent, see it
# exit immediately, and either kill the children or declare success while the
# service never started. This is the single most common broken conversion.Le correctif durable n’est pas un fichier PID plus astucieux, c’est d’arrêter de faire un fork. Presque tous les démons écrits depuis vingt ans ont une option pour rester au premier plan, et la documentation amont recommande de l’utiliser pour les démons de conception moderne. Type=exec élimine alors toute cette classe de problèmes, et Type=notify va plus loin : c’est lui — avec notify-reload, ou Type=dbus pour les services D-Bus — qui constitue le seul moyen fiable de faire qu’un After= sur une unit dépendante signifie « la socket accepte réellement des connexions » et non « le binaire a été exécuté ». Tout le reste n’est qu’un ordonnancement plein d’espoir.[incompat]
init 3, telinit et runlevel ont disparu aussi
C’est la partie du changement qui dépasse les serveurs pour atteindre la mémoire musculaire et les procédures d’exploitation. La version 258 a supprimé le nœud de périphérique /dev/initctl et les commandes initctl, runlevel et telinit, supprimé la prise en charge des changements d’état via init 3, supprimé les units runlevel[0-6].target, et cessé d’enregistrer les transitions de runlevel dans utmp et wtmp — parce que la notion même de runlevel n’existe plus. La version 259 a restauré les targets derrière l’option de compilation -Dcompat-sysv-interfaces=yes ; elle n’a pas restauré les commandes.[v258][v259]
| SysV | systemd | Remarque |
|---|---|---|
init 3, telinit 3 | systemctl isolate multi-user.target | Seul telinit a été supprimé en 258. init existe toujours — c’est PID 1 ; ce qui a disparu, c’est le pilotage de l’état du système par son intermédiaire. |
init 5 | systemctl isolate graphical.target | |
init 1, telinit S | systemctl isolate rescue.target | emergency.target descend encore plus bas. |
init 0 / init 6 | systemctl poweroff / systemctl reboot | |
runlevel | systemctl get-default, systemctl list-units --type=target | La commande a été supprimée ; une target n’est pas un runlevel. |
Runlevel par défaut dans /etc/inittab | systemctl set-default multi-user.target | /etc/inittab n’est plus lu depuis des années. |
chkconfig x on, update-rc.d x defaults | systemctl enable x.service | Nécessite une section [Install] dans l’unit. |
chkconfig x off, update-rc.d -f x remove | systemctl disable x.service | |
| (aucun équivalent) | systemctl mask x.service | Rend l’unit impossible à démarrer, même comme dépendance. |
chkconfig --list, service --status-all | systemctl list-unit-files --type=service | |
service x start | systemctl start x.service | service existe souvent encore comme adaptateur ; ne comptez pas dessus. |
# v258 removed /dev/initctl and the initctl, runlevel and telinit commands,
# and with them `init 3`. v259 brought the runlevel[0-6].target ALIASES back,
# but only if the distribution builds with -Dcompat-sysv-interfaces=yes, and
# the commands did not come back at all. Do not build habits on them.
# What runlevel am I in? -> what is the system aiming at?
systemctl get-default # the boot target
systemctl list-units --type=target --state=active
# init 3 / telinit 3 ->
sudo systemctl isolate multi-user.target
# init 5 ->
sudo systemctl isolate graphical.target
# init 1 / telinit 1 / S ->
sudo systemctl isolate rescue.target
# init 0 ->
sudo systemctl poweroff
# init 6 ->
sudo systemctl reboot
# Change the default "runlevel" permanently (this replaces /etc/inittab):
sudo systemctl set-default multi-user.target
# chkconfig foo on / update-rc.d foo defaults ->
sudo systemctl enable foo.service
# chkconfig foo off / update-rc.d -f foo remove ->
sudo systemctl disable foo.service
# ...and the one with no SysV equivalent, for a unit that must never start:
sudo systemctl mask foo.service
# chkconfig --list / service --status-all ->
systemctl list-unit-files --type=service
systemctl list-units --type=service --allL’un de ces remplaçants n’a aucun équivalent SysV et mérite d’être appris pour lui-même. systemctl mask rend une unit impossible à démarrer, par quoi que ce soit, y compris en tant que dépendance d’autre chose — ce dont SysV se rapprochait le plus, c’était supprimer le script en espérant que le gestionnaire de paquets ne le remette pas. C’est l’outil correct pour mettre au rebut un service que vous n’êtes pas prêt à désinstaller, et pour garantir qu’un script décommissionné ne sera pas ressuscité par un collègue bien intentionné.[systemctl]
Ce que vous y gagnez et que l’encapsulation n’a jamais donné
Autant être honnête : cette migration est un travail imposé sans nouvelle fonctionnalité au bout. Alors prenez la compensation, parce qu’elle est réelle et gratuite : un script init encapsulé ne pouvait rien en utiliser. L’encapsulation lançait votre script shell, et votre script shell lançait le démon ; systemd n’avait aucune idée de ce qu’était le processus, ne pouvait pas le redémarrer de façon fiable, et ne pouvait pas le contraindre du tout. Une unit native obtient tout ce qui suit en ajoutant des lignes :[exec][resctl]
- Une vraie boucle de supervision.
Restart=on-failureavecRestartSec=, adossé àStartLimitIntervalSec=pour qu’une boucle de plantage s’arrête au lieu de saturer un cœur. L’unit générée codait en durRestart=nopour chaque script qu’elle ait jamais produit. - Une isolation du système de fichiers.
ProtectSystem=strictpasse tout le système de fichiers en lecture seule sauf ce que vous nommez,PrivateTmp=yesdonne au service son propre/tmp, etStateDirectory=,RuntimeDirectory=etConfigurationDirectory=créent, possèdent et nettoient les répertoires que votre script créait à la main avecmkdir -p. - Une réduction de privilèges qui tient réellement.
User=,NoNewPrivileges=yeset unCapabilityBoundingSet=explicite remplacentsuetstart-stop-daemon --chuid, et contrairement à eux ils s’appliquent à tous les processus engendrés par le service, indéfiniment, y compris ceux qu’un démon compromis tenterait de lancer. - Une restriction des appels système et des espaces de noms.
SystemCallFilter=@system-service,RestrictAddressFamilies=,RestrictNamespaces=etMemoryDenyWriteExecute=yesconstituent une politique seccomp que vous écrivez en quatre lignes. Il n’existait aucun équivalent en shell, à aucun prix. - Une comptabilité des ressources par service.
MemoryMax=,CPUQuota=,TasksMax=etIOWeight=s’appliquent au cgroup de l’unit, c’est-à-dire au démon et à tout ce qu’il engendre — exactement ce qu’un script fondé sur un fichier PID n’a jamais su contenir. - Des journaux qui arrivent quelque part. La sortie standard et la sortie d’erreur partent vers le journal avec le nom de l’unit attaché, si bien que
journalctl -ufonctionne sans que le démon sache ce qu’est syslog, et sans qu’un fichier de log remplisse silencieusement une partition faute de règle de rotation.
N’ajoutez pas tout cela à l’aveugle pour autant. Lancez systemd-analyze security sur l’unit et traitez le score comme une liste de tâches plutôt que comme une note : il nomme chaque directive que vous n’avez pas définie et ce qu’elle apporterait. Resserrez par petites étapes, redémarrez, et lisez le journal — le mode de défaillance d’un durcissement excessif, c’est un service qui démarre parfaitement puis ne peut plus ouvrir un fichier trois heures plus tard, ce qui vous gâche la journée bien plus sûrement qu’un service qui refuse de démarrer.[analyze]
Vérifier la migration, pas l’espérer
Ne vérifiez pas en regardant systemctl status et en constatant du vert. Un script encapsulé affichait du vert lui aussi. Vérifiez les quatre choses qui étaient réellement différentes : que la définition est un fichier que vous maîtrisez et non un fichier généré, qu’aucun SourcePath ne renvoie vers un script init, que l’unit est activée explicitement et non par un câblage résiduel, et que systemd connaît l’identifiant du processus principal.[analyze]
#!/usr/bin/env bash
# Run after the cut-over and again after the first reboot. Non-zero exit means
# something in the migration is not finished.
set -u
rc=0
units=("$@") # e.g. ./verify.sh acme-collector.service local-startup.service
for u in "${units[@]}"; do
echo "--- $u"
# 1. Is it a real file, not a generated one?
frag=$(systemctl show -p FragmentPath --value "$u")
case "$frag" in
/etc/systemd/system/*|/usr/lib/systemd/system/*|/lib/systemd/system/*) ;;
*) echo " FAIL still generated or missing: '$frag'"; rc=1 ;;
esac
# 2. No SourcePath = no init script behind it.
src=$(systemctl show -p SourcePath --value "$u")
[ -z "$src" ] || { echo " FAIL SourcePath=$src"; rc=1; }
# 3. Syntactically valid, with no warnings.
systemd-analyze verify "$frag" || { echo " FAIL verify"; rc=1; }
# 4. Enabled explicitly, not by leftover generator wiring.
[ "$(systemctl is-enabled "$u")" = enabled ] \
|| { echo " FAIL not enabled"; rc=1; }
# 5. Actually supervised: a Type= that forks and loses its child shows
# MainPID=0 while looking perfectly healthy.
mp=$(systemctl show -p MainPID --value "$u")
ty=$(systemctl show -p Type --value "$u")
[ "$ty" = oneshot ] || [ "$mp" != 0 ] \
|| { echo " FAIL Type=$ty but MainPID=0"; rc=1; }
# 6. Report the sandbox score. Not pass/fail - a number to improve.
systemd-analyze security "$u" | tail -1
done
# 7. Nothing anywhere is still being generated from an init script.
if ls /run/systemd/generator.late/*.service >/dev/null 2>&1; then
for g in /run/systemd/generator.late/*.service; do
grep -q '^SourcePath=/etc/init.d/' "$g" && { echo "STILL WRAPPED: $g"; rc=1; }
done
fi
# 8. Nothing failed at boot for an ordering reason you introduced.
systemctl --failed --no-legend --no-pager
exit $rcRedémarrez ensuite, parce que toute la classe de bugs qu’introduit cette migration est une classe de bugs d’ordonnancement, et que les bugs d’ordonnancement sont invisibles sur un système en marche où toutes les dépendances sont déjà levées. Lisez journalctl -b -u pour chaque unit convertie et systemd-analyze blame pour le démarrage dans son ensemble. Un service qui démarrait sous un lien symbolique numéroté en position 20 et qui démarre désormais trop tôt n’échouera pas — il réessaiera, ou il démarrera avec une configuration vide, ou il fera un bind avant qu’une interface n’ait une adresse.
Si vous livrez du logiciel sur les serveurs des autres
Si vous distribuez du logiciel que d’autres installent, cela a cessé d’être optionnel il y a un moment déjà. Le générateur journalisait un avertissement pour chaque script encapsulé depuis la 255, adressé directement à vous : please update package to include a native systemd unit file. Sur systemd 260 cet avertissement n’est plus là, parce qu’il n’y a plus rien à signaler — le script s’installe, et puis rien ne l’exécute.[gensrc]
Ce qu’un paquet doit désormais livrer, c’est un fichier unit dans /usr/lib/systemd/system/ avec une section [Install] en bonne et due forme, parce que le troisième composant supprimé, systemd-sysv-install, était le point d’accroche qui permettait à systemctl enable de se rabattre sur chkconfig ou update-rc.d pour les services fondés sur des scripts. Sans lui, activer un service relève uniquement des liens symboliques que décrit [Install]. Gardez le script init dans le paquet si vous prenez encore en charge d’anciennes distributions — une unit native et un script peuvent coexister, et l’unit l’emporte partout où systemd est assez récent pour compter.[unit]
L’ordre dans lequel s’y prendre
Il n’existe aucune version de cette histoire où s’y prendre plus tard coûte moins cher. Le travail est proportionnel au nombre de scripts, qui ne diminue pas, et la fenêtre est définie par le calendrier de publication de quelqu’un d’autre. Ce qui varie, c’est le préavis dont vous disposez ; la première décision consiste donc à déterminer dans laquelle de ces situations vous vous trouvez :[ubuntu2604]
| Situation | Le temps dont vous disposez | Faites ceci |
|---|---|---|
| Distribution en rolling release, ou systemd 260 déjà installé | Aucun — c’est déjà parti | Faites l’inventaire depuis les sauvegardes ou la gestion de configuration, puis convertissez. Les scripts sur le disque sont inertes mais disent encore ce qui existait. |
| Série intermédiaire Ubuntu (26.04 → 26.10) | Des semaines | Inventoriez et capturez les units générées maintenant, pendant que la 259 tourne encore. Convertissez avant la version d’octobre. |
| Ubuntu 26.04 LTS, en restant sur LTS | Jusqu’à la prochaine LTS | Pas d’urgence, mais le journal avertit déjà. Convertissez au fil de l’eau, en commençant par tout ce qui est sans propriétaire. |
| Debian stable, RHEL 9 ou 10 | Jusqu’à la prochaine version majeure | Le générateur est présent ; sur Debian 13 et RHEL 10 il avertit déjà pour chaque script. Faites l’inventaire — cela coûte une commande et se conserve bien. |
| Vous empaquetez du logiciel pour d’autres | Aucun | Livrez dès maintenant un fichier unit avec une section [Install]. Les mises à niveau de vos utilisateurs ne dépendent pas de vous. |
| Un boîtier ou un agent constructeur que vous ne pouvez pas modifier | Le même que l’hôte | Écrivez l’unit vous-même et masquez le script du constructeur, ou remontez le sujet avant que sa prochaine version ne vous laisse en plan. |
- Recensez tout le parc cette semaine, avec la requête de journal ci-dessus, et conservez la sortie. Deux colonnes — nom d’hôte et chemin du script — suffisent à dimensionner le travail et à attraper les machines dont personne ne se souvient.
- Capturez l’unit générée pour chaque script avec
systemctl cat, dans un répertoire que vous conservez. C’est une opération en lecture seule, cela prend quelques minutes, et après la mise à niveau l’information est irrécupérable. - Triez en trois piles : supprimer, remplacer, réécrire. Une part surprenante de ce que vous trouverez concerne un service décommissionné il y a des années. Le supprimer est une migration complète et ne prend aucun temps.
- Convertissez les faciles d’abord — tout ce qui est déjà empaqueté en amont possède presque à coup sûr une unit officielle, et installer le paquet courant est plus rapide et plus correct que d’en écrire une soi-même.
- Faites la bascule pendant que le générateur existe encore, un service à la fois, sur un système où le script est toujours présent. Revenir en arrière consiste alors à supprimer un fichier et à recharger, pas à restaurer une sauvegarde sous pression.
- Redémarrez et lisez le démarrage, hôte par hôte, avant de considérer quoi que ce soit comme terminé. Puis masquez les units retirées pour que rien ne les ramène.
La même fenêtre de maintenance contient généralement le reste : la montée de version serveur d’Ubuntu 24.04 vers 26.04 traite la montée de version qui va vous retirer le générateur sous les pieds, les timers systemd face à cron est l’autre moitié du même nettoyage, puisqu’une machine avec des scripts SysV a presque toujours des crontabs qui devraient être des units elles aussi, et ce qui casse en passant à Docker Engine 29 couvre le moteur de conteneurs, qui a changé ses propres valeurs par défaut à la même période et pour des raisons voisines.
Questions fréquentes
Puis-je simplement réinstaller le sysv-generator sur systemd 260 ?
Non, et les contournements qui ont l’air de pouvoir marcher sont pires que de faire la migration. Le générateur a été supprimé de l’arbre des sources, pas désactivé derrière une option : il n’y a donc aucun paquet à installer et aucune option à positionner — les trois options meson dépréciées qui subsistent (-Drc-local=, -Dsysvinit-path=, -Dsysvrcnd-path=) ne touchent que des chemins, pas la présence du code. Vous pourriez en principe copier le binaire du générateur d’une compilation 259 dans /usr/lib/systemd/system-generators/, mais vous feriez tourner un générateur non maintenu contre un gestionnaire avec lequel il n’a jamais été testé, et il serait écrasé silencieusement à la prochaine mise à jour de systemd. Écrire le fichier unit prend moins de temps que déboguer cela une seule fois.
Mon script init fonctionne-t-il encore si je le lance à la main ?
Oui. Rien n’a changé dans le script ; c’est un script shell ordinaire et /etc/init.d/x start fera exactement ce qu’il a toujours fait. Ce qui a disparu, c’est la traduction automatique en service, ce qui signifie : il ne démarre pas au boot, systemctl ne le connaît pas, rien ne supervise ni ne redémarre le processus, et arrêter la machine ne l’arrêtera pas proprement. Cette combinaison est plus grave qu’elle n’en a l’air, parce qu’un script que l’on peut encore lancer à la main ne donne pas l’impression d’être cassé — alors on le contourne avec une entrée @reboot dans une crontab et le problème se tait pendant un an.
Comment savoir quels serveurs sont concernés sans me connecter à tous ?
Interrogez le journal sur l’avertissement structuré. Depuis systemd 255, chaque script encapsulé produit une entrée de journal avec MESSAGE_ID=a8fa8dacdb1d443e9503b8be367a6adb et deux champs personnalisés, SYSVSCRIPT= et UNIT=. Si vous centralisez vos journaux, c’est une seule requête sur tout le parc. Sinon, exécutez journalctl -b MESSAGE_ID=a8fa8dacdb1d443e9503b8be367a6adb via ce qui vous sert à lancer des commandes sur de nombreux hôtes. Sur systemd 254 et antérieurs l’encapsulation est silencieuse : repliez-vous alors sur l’interrogation de chaque unit pour son FragmentPath et son SourcePath, comme le fait le script d’inventaire ci-dessus.
Quelle différence entre Type=simple, Type=exec, Type=forking et Type=notify ?
Type=simple considère le service démarré dès que systemd a forké, avant même que le binaire ait forcément été exécuté avec succès. Type=exec attend que le binaire ait réellement été exécuté, ce qui transforme un fichier manquant ou un User= erroné en échec de démarrage au lieu d’une sortie immédiate mystérieuse — c’est la meilleure valeur par défaut pour un démon au premier plan. Type=forking vise les démons qui se mettent eux-mêmes en arrière-plan et attend un PIDFile= ; c’est ce qu’utilisait le sysv-generator, faute de choix. Type=notify attend que le démon appelle sd_notify(READY=1), et c’est le seul des quatre où une dépendance After= signifie que le service est réellement prêt. Convertissez vers Type=exec ; utilisez Type=notify si le démon le prend en charge.
/etc/rc.local a-t-il vraiment disparu, ou est-il seulement déprécié ?
Disparu, sur systemd 260 et ultérieurs. systemd-rc-local-generator et l’unit rc-local.service qu’il tirait ont tous deux été supprimés. Le fichier restera dans /etc, toujours exécutable, et plus rien ne l’appellera jamais. Notez que le chemin était un réglage de compilation et variait selon les distributions — certaines utilisaient /etc/rc.d/rc.local — donc vérifiez avec quoi votre générateur était réellement configuré avant de supposer que vous les avez tous trouvés.
Mon unit indique active (running) mais le processus n’existe pas. Qu’ai-je raté ?
Très probablement un Type=forking avec un fichier PID que systemd n’a pas pu lire au bon moment, ou un Type=simple sur un démon qui fait un fork. Lancez systemctl show -p Type -p MainPID -p PIDFile --value votreunite.service : si MainPID vaut 0 alors que le démon tourne, systemd l’a perdu de vue et aucune supervision ne s’applique. Le correctif consiste à empêcher le démon de forker — trouvez son option premier plan et utilisez Type=exec. Si vous ne le pouvez vraiment pas, assurez-vous que PIDFile= est un chemin absolu sous /run et que le démon l’écrit avant que le processus parent ne sorte.
Faut-il conserver le script init après avoir écrit l’unit ?
Gardez-le pendant la fenêtre de changement, puis supprimez-le. Tant que les deux existent, l’unit native l’emporte — le générateur ignorait explicitement tout script disposant déjà d’une unit du même nom — donc avoir les deux est sans risque et rend le retour arrière trivial. Mais laissez-le indéfiniment et vous avez deux définitions du même service, dont l’une est un piège pour celui qui déboguera cela à trois heures du matin. Déplacez-le dans un répertoire d’archives une fois que l’hôte a redémarré proprement, et supprimez les liens symboliques /etc/rc*.d en même temps.
Dois-je m’en préoccuper sur RHEL ou Debian stable ?
Pas dans l’urgence, mais faites quand même l’inventaire. RHEL 10 et Debian 13 embarquent tous deux systemd 257 : le générateur y est encore présent, et tous deux affichent déjà l’avertissement de dépréciation par script apparu en 255 — les notes de version de Red Hat indiquent par ailleurs que la prise en charge des scripts de service System V est dépréciée et sera supprimée. Le changement arrivera avec la prochaine version majeure de chacun. La raison d’agir maintenant, c’est que l’étape d’inventaire tient en une commande en lecture seule, qu’elle est bien plus facile pendant que le générateur tourne et vous dit ce qu’il encapsule, et que l’alternative consiste à découvrir un script sans propriétaire pendant une montée de version majeure, c’est-à-dire au pire moment possible pour rétro-concevoir le shell d’un collègue.
Par quoi systemd-sysv-install est-il remplacé ?
Par rien, parce qu’il ne lui reste rien à faire. C’était le point d’accroche qui permettait à systemctl enable, disable et is-enabled de se rabattre sur l’outil propre à la distribution — chkconfig sur les systèmes Red Hat, update-rc.d sur les Debian — lorsque le service nommé était un script et non une unit. La prise en charge des scripts ayant été supprimée, activer un service relève entièrement des liens symboliques décrits par la section [Install] de l’unit. Si votre paquet reposait sur ce repli, il lui faut désormais un vrai fichier unit avec une vraie section [Install].
Puis-je convertir un script automatiquement plutôt qu’à la main ?
En partie, et le meilleur convertisseur est celui qui est sur le point de disparaître : systemctl cat votreservice.service sur un système qui possède encore le générateur vous donne une traduction mécaniquement correcte des dépendances, c’est-à-dire la partie qu’il est facile de rater. Ce qu’aucun outil ne fera pour vous, c’est la partie qui compte — décider que le démon doit cesser de forker, que le fichier PID doit disparaître, que start-stop-daemon --chuid devient User=, et quelles directives de bac à sable sont sûres pour cette charge de travail précise. Traitez l’unit générée comme la spécification et écrivez la vraie à partir d’elle.
La couche de contrôle des ressources a bougé dans les mêmes versions : systemd 258 a purement supprimé cgroup v1, si bien que chaque hôte démarre désormais sur la hiérarchie unifiée, que quelqu’un l’ait demandé ou non. la migration de cgroup v1 vers cgroup v2 détaille l’audit, la conversion fichier par fichier et les deux traductions qui changent le comportement et pas seulement le nom.
Sources
Chaque numéro de version, chaque directive et chaque valeur par défaut de cet article ont été lus dans les sources ci-dessous plutôt que repris d’autres publications. Là où le code a depuis été supprimé, le lien pointe vers le dernier tag où il existait encore, pour que vous puissiez vérifier vous-même.
- systemd — NEWS: the upstream changelog, and the only authoritative statement of what was removed in which release. Everything dated in this article was checked against it
- systemd v260 release notes — "Support for System V service scripts has been removed", with the three components that went with it: systemd-sysv-generator, systemd-rc-local-generator plus rc-local.service, and systemd-sysv-install
- systemd v259 release notes — the release that restored runlevel[0-6].target behind the new -Dcompat-sysv-interfaces=yes build option, and the version shipped by Ubuntu 26.04 LTS
- systemd v258 release notes — the removal of /dev/initctl and of the initctl, runlevel and telinit commands, the removal of the runlevel[0-6].target units, and the removal of cgroup v1
- systemd v255 release notes — where support for System V service scripts was first declared deprecated, and the release whose sysv-generator already logs the structured per-script warning this article uses for the inventory
- systemd-sysv-generator source at v259 — the last tag where the code still exists. It is the definitive answer to what the generator read, what it ignored, and exactly which directives it wrote
- systemd-sysv-generator(8) — the manual page, including the statement that the wrapper units are always ordered after basic.target and that compatibility was never 100%
- systemd-rc-local-generator(8) — the rc.local compatibility generator, its warning that rc-local.service is ordered after network.target and that this does not mean the network works, and the SELinux note about restorecon
- rc-local.service at v259 — the actual unit that ran /etc/rc.local, so you can see what semantics you are replacing rather than guessing them
- systemd exit-status.h — the LSB start-verb exit codes, and the origin of the SuccessExitStatus=5 6 line the generator emitted for scripts with an LSB header
- systemd sd-messages.h — the catalogue of structured log message IDs, including SD_MESSAGE_SYSV_GENERATOR_DEPRECATED, which is what makes the journal-based inventory in this article possible
- systemd — Incompatibilities: the upstream list of the places where SysV behaviour and systemd behaviour genuinely differ, worth reading before you assume a script will behave the same way
- Linux Standard Base — Init Script Actions: the specification that defines the LSB header fields and the exit codes an init script is supposed to return
- systemd.service(5) — Type=, Restart=, PIDFile=, ExecReload=, RemainAfterExit= and the rest of the service options used in the unit files below
- systemd.unit(5) — Wants=, After=, Before=, the Condition family and the [Install] section that replaces update-rc.d and chkconfig
- systemd.exec(5) — User=, RuntimeDirectory=, StateDirectory= and the whole sandboxing vocabulary, none of which a wrapped init script could ever use
- systemd.special(7) — what multi-user.target, graphical.target, network.target and network-online.target actually mean, and why After=network.target does not mean the network is up
- systemd.generator(7) — how generators work and where their output lands, which is why the units you are about to lose live under /run/systemd/generator.late
- systemctl(1) — enable, mask, cat, show, list-unit-files and isolate: the commands that replace chkconfig, update-rc.d, service and telinit
- systemd-analyze(1) — verify, security and blame: the three subcommands that turn this migration from a guess into something you can check
- journalctl(1) — matching on MESSAGE_ID= and on arbitrary structured fields, which is how the inventory command in this article finds every wrapped script
- daemon(7) — the difference between a SysV-style forking daemon and a new-style daemon, and upstream's own recommendation to stop forking
- systemd.resource-control(5) — the per-unit cgroup accounting and limits that come for free once a service is a real unit
- Ubuntu 26.04 LTS release notes — the section stating that 26.04 LTS is the last release with System V compatibility in systemd, that the change takes effect in 26.10, and that the release ships systemd 259
- Ubuntu 26.10 release schedule — the release date that turns "eventually" into a deadline for anyone tracking the interim series
- Red Hat Enterprise Linux 10 release notes — the systemd rebase to 257, the statement that System V service script support is deprecated and will be removed, and the move of default configuration files under /usr/lib/systemd
- Debian trixie systemd package — the version currently in Debian stable, for readers deciding how much runway they actually have
Cet article vous a été utile ?