systemd Type= : simple, exec, forking, notify ou oneshot ?
Votre unité reste active alors que le daemon est arrêté ? Choisissez le bon Type= systemd et identifiez le vrai PID principal au lieu d’un wrapper.
Pourquoi systemd indique-t-il qu’une unité est active alors que le processus est arrêté
Une unité de service systemd reste active tant que le processus que systemd considère comme processus principal est actif. La valeur Type= dans la section [Service] détermine ce processus. Si vous choisissez la mauvaise valeur, systemd surveille finalement un wrapper shell ou un processus parent de courte durée, tandis que le daemon qui vous intéresse s’arrête à l’intérieur de la même unité. L’unité indique correctement l’état du processus qu’on lui a demandé de surveiller.
Modifier la stratégie de redémarrage ne résoudra pas le problème. Restart= intervient lorsque le processus principal s’arrête. Ainsi, Restart=always ne se déclenche jamais tant que le PID principal (identifiant de processus) appartient à un processus qui fonctionne encore. Corrigez d’abord Type=. Le comportement de systemd après l’arrêt réel du processus principal relève d’une décision distincte, présentée dans le guide sur Restart= et RestartSec=.
Que décide réellement Type=
Chaque valeur de Type= répond à deux questions à la fois. Quand systemd peut-il considérer cette unité comme démarrée ? Quel est le processus principal ?
La première réponse contrôle l’ordre de démarrage. Une unité qui référence la vôtre dans After= attend que systemd considère la vôtre comme démarrée. Un Type= qui signale « démarré » trop tôt permet aux unités dépendantes de s’exécuter avant que votre service puisse leur répondre.
La seconde réponse contrôle la supervision. systemd place chaque processus lancé par une unité dans un cgroup (groupe de contrôle), une fonctionnalité du noyau qui regroupe les processus afin de pouvoir les limiter et les arrêter ensemble. Le cgroup permet à systemctl stop d’effectuer le nettoyage : KillMode= vaut par défaut control-group, donc l’arrêt d’une unité envoie un signal à chaque processus qu’elle contient. Le PID principal est plus précis. Il s’agit du seul processus dont la fin met fin à l’unité et dont le code de sortie devient le résultat de l’unité. La confusion commence lorsqu’on interprète le cgroup comme s’il s’agissait du PID principal.
Type=simple signale le démarrage avant l’exécution du binaire
Type=simple est la valeur par défaut lorsque ExecStart= est défini et qu’aucun de Type= et BusName= n’est présent. systemd crée le processus, considère immédiatement l’unité comme démarrée et utilise ce processus comme PID principal. Les unités suivantes démarrent aussitôt, avant même l’exécution du binaire du service.
Ce dernier point explique une surprise fréquente. Une faute de frappe dans le chemin ExecStart= produit tout de même une tâche de démarrage réussie. L’échec survient un instant plus tard, lorsque l’exécution échoue. systemd enregistre ce cas avec le code de sortie 203, que son propre tableau nomme EXEC et définit comme un échec d’exécution du binaire du service. Ainsi, le retour de systemctl start sans erreur ne prouve pas que votre binaire existe.
Utilisez simple pour un programme qui reste au premier plan et ne se met jamais lui-même en arrière-plan. Cela concerne la plupart des daemons modernes et presque tous les programmes que vous écrivez vous-même.
Type=exec attend le démarrage effectif du programme
Type=exec correspond à simple avec une étape supplémentaire. systemd considère l’unité comme démarrée uniquement après la réussite du fork et de l’exécution du binaire. Un binaire absent ou un User= qui ne peut pas être résolu échoue alors le job de démarrage lui-même, au lieu de signaler une réussite puis d’échouer silencieusement un instant plus tard.
Type=exec a été ajouté dans systemd 240. Toutes les distributions serveur actuelles le prennent donc en charge. Ubuntu 24.04 fournit systemd 255 et Debian 13 fournit systemd 257, en août 2026. Vérifiez votre version avec systemctl --version.
Le coût est une étape de synchronisation supplémentaire au démarrage. Le gain est un code de sortie fiable renvoyé par systemctl start. Pour un programme au premier plan, préférez exec à simple.
Type=forking et perte du PID principal
Type=forking indique à systemd que le processus défini dans ExecStart= va créer un processus enfant, puis se terminer volontairement. systemd attend la fin de ce premier processus, puis considère l’unité comme démarrée. Le processus enfant restant est le daemon.
La difficulté concerne l’identification. Le processus lancé par systemd a disparu. systemd doit donc déterminer quel processus survivant est le processus principal. Définissez PIDFile= sur le fichier écrit par le daemon, généralement un chemin sous /run. systemd y lit le PID. systemd vérifie également que le PID indiqué dans ce fichier correspond à un processus qui appartient déjà à ce service. Un fichier obsolète désignant un processus sans rapport est ainsi rejeté au lieu d’être utilisé.
Sans PIDFile=, GuessMainPID= s’applique et sa valeur par défaut est yes. Cette déduction est fiable uniquement lorsque le service se stabilise avec un seul processus. Le manuel indique clairement la limite : si le daemon comporte plusieurs processus, la déduction peut être incorrecte et la détection des échecs cesse de fonctionner. Une unité peut également se retrouver avec un PID principal égal à 0. Dans ce cas, systemd n’a aucun processus à superviser.
La plupart des daemons qui créent un processus enfant disposent également d’une option pour rester au premier plan. Utilisez cette option avec Type=exec et supprimez la ligne PIDFile=. Moins d’éléments à gérer signifie moins de risques de perdre le PID.
Type=oneshot pour les tâches qui se terminent
Type=oneshot attend que le processus s’exécute puis se termine. systemd considère l’unité comme démarrée uniquement après la fin du processus. oneshot convient donc aux tâches dont une autre unité doit attendre la fin. Il s’agit également de la valeur par défaut implicite lorsqu’une unité ne définit ni Type= ni ExecStart=.
Deux comportements sont propres à oneshot. C’est le seul type qui accepte plusieurs lignes ExecStart=. Elles sont exécutées dans l’ordre. Le délai d’expiration du démarrage est également désactivé par défaut. Ainsi, un oneshot qui se bloque attend indéfiniment, sauf si vous définissez vous-même TimeoutStartSec=.
Après la fin du processus, l’unité repasse à l’état inactif. RemainAfterExit=yes la maintient à l’état active, sans aucun processus en cours. Il s’agit de la version délibérée du symptôme présenté au début de cette page. Ce comportement est correct lorsque le rôle de l’unité consiste à laisser un état configuré plutôt qu’à maintenir un processus en cours : charger un jeu de règles de pare-feu ou démarrer une stack de conteneurs. C’est le principe utilisé par une stack Docker Compose qui redémarre après un reboot : l’unité exécute la commande compose, se termine, puis reste active parce que les conteneurs qu’elle a démarrés continuent de fonctionner. Une unité oneshot est également ce qu’un calendrier déclenche. C’est l’autre aspect de l’exécution d’une tâche avec un timer systemd plutôt qu’avec cron.
Type=notify permet au service d’indiquer qu’il est prêt
Type=notify confie cette décision au service. systemd maintient la tâche de démarrage ouverte jusqu’à ce que le processus envoie READY=1 sur un socket Unix dont le chemin lui est transmis dans la variable d’environnement NOTIFY_SOCKET. L’interface C est sd_notify(3), et de nombreux serveurs la prennent déjà en charge.
C’est la réponse exacte à la question « est-il démarré ? ». simple et exec indiquent que le service est démarré avant qu’il ait lu sa configuration ou ouvert son socket d’écoute. Une unité dépendante peut donc démarrer trop tôt et échouer lors de sa première connexion. notify indique que le service est démarré au moment où il se déclare lui-même prêt.
systemd accepte ce message uniquement du processus principal. C’est ce que signifie NotifyAccess=main, et Type=notify l’implique. Si le message provient d’un processus enfant ou d’un helper, définissez NotifyAccess=all. Un script shell peut appeler systemd-notify --ready, mais cela exécute un processus court distinct. Il faut donc utiliser NotifyAccess=all, et systemd peut ne pas réussir à attribuer un message dont l’émetteur a déjà quitté. Un service qui parle lui-même le protocole est plus fiable.
Deux paramètres associés sont utiles à connaître. Type=notify-reload, disponible depuis systemd 253, étend la même négociation aux rechargements. Ainsi, systemctl reload se termine lorsque le service indique que le rechargement est terminé, et non lorsque le signal a été envoyé. WatchdogSec= demande à un service qui envoie des notifications d’émettre un message keep-alive à intervalles réguliers. systemd considère qu’une échéance manquée constitue un échec.
Type=dbus et Type=idle
Type=dbus attend que le service prenne un nom sur D-Bus, le bus de messages que les services système et les services de bureau utilisent pour communiquer entre eux. Cette valeur nécessite BusName= et devient la valeur par défaut dès que BusName= est défini. Utilisez-la uniquement pour un service qui enregistre réellement un nom sur le bus.
Type=idle se comporte comme simple, mais retarde l’exécution du programme jusqu’à ce que les tâches en attente aient été distribuées, avec une limite de cinq secondes. Cette valeur empêche que la sortie de la console au démarrage se mélange aux messages d’état. Elle ne sert pas à définir l’ordre de démarrage et ne doit pas être utilisée pour un service classique.
Pourquoi un script wrapper laisse systemd suivre le mauvais PID
Voici la structure qui produit le symptôme initial.
[Service]
Type=simple
ExecStart=/opt/app/run.sh#!/bin/bash
export APP_ENV=production
/opt/app/bin/server --config /etc/app.yaml &
/opt/app/bin/exporter --port 9101systemd enregistre le shell comme PID principal. Le shell reste actif pendant que exporter s’exécute au premier plan. Si server se termine, le shell ne le détecte pas. Le PID principal reste donc actif, l’unité est toujours active et Restart= n’a rien sur quoi agir. Les deux processus restent en permanence dans le cgroup de l’unité. systemctl stop effectue donc toujours correctement le nettoyage. C’est la supervision qui est défaillante, pas le nettoyage.
Le correctif dépend du nombre réel de processus persistants de l’unité.
S’il n’y en a qu’un, remplacez le shell par ce processus.
#!/bin/bash
export APP_ENV=production
exec /opt/app/bin/server --config /etc/app.yamlexec remplace le shell par le programme indiqué et conserve le même PID. Le PID enregistré par systemd appartient donc maintenant au daemon. Mieux encore, supprimez le wrapper. Environment= et EnvironmentFile= transmettent les variables, et ExecStartPre= exécute l’étape de préparation. systemd peut ainsi lancer directement le daemon et connaître son PID par construction.
S’il y en a deux, aucun PID unique ne représente l’unité. Séparez-les en deux unités et ordonnez-les avec After= et Wants=. Une unité par processus est la configuration que systemd supervise le mieux. C’est aussi la seule méthode permettant à chaque processus d’avoir son propre comportement de redémarrage.
Ce que modifie ExitType=cgroup
ExitType= a été ajouté dans systemd 250. La valeur par défaut est main : l’unité est considérée comme arrêtée lorsque le processus principal se termine. Avec ExitType=cgroup, l’unité est considérée comme active tant qu’un processus de son cgroup est encore en vie.
[Service]
Type=simple
ExitType=cgroup
ExecStart=/opt/app/launcherCela résout un problème précis. Un launcher qui démarre le véritable traitement puis se termine ferait, avec ExitType=main, considérer l’unité comme arrêtée par systemd, qui tuerait alors les processus restants. Avec ExitType=cgroup, l’unité suit plutôt l’ensemble du groupe.
Il faut bien comprendre ce que cette option ne résout pas. ExitType=cgroup maintient une unité active tant qu’au moins un processus est encore en vie. Une unité qui contient deux daemons reste donc active après l’arrêt de l’un d’eux. Cette option corrige le cas du launcher. Elle ne transforme pas une unité en supervisor de plusieurs processus indépendants. ExitType= ne peut pas non plus être combiné avec Type=oneshot.
Le cgroup sert également à comptabiliser les ressources. Les limites telles que MemoryMax= et CPUQuota= s’appliquent donc à chaque processus lancé par l’unité, indépendamment de ce que Type= indique concernant le PID principal. Cet aspect est expliqué dans limiter la mémoire et le CPU d’un service avec systemd.
Comment trouver le processus que systemd surveille réellement
Effectuez ces vérifications dans l’ordre sur l’unité que vous déboguez. Lisez d’abord ce que systemd a chargé, puis ce qu’il suit, et comparez ensuite ces informations avec la table des processus.
systemctl cat app.service
systemctl show -p Type,ExitType,GuessMainPID,PIDFile,MainPID,RemainAfterExit app.servicesystemctl cat affiche le fichier d’unité ainsi que tous les drop-ins qui s’y appliquent. Vous consultez ainsi ce que systemd a chargé, et non le fichier dont vous vous souvenez. systemctl show affiche les valeurs effectives, y compris les valeurs par défaut que vous n’avez jamais notées. Notez la valeur de MainPID avant de poursuivre.
systemd-cgls --unit=app.service
ps -o pid,ppid,stat,etime,args -p "$(systemctl show -p MainPID --value app.service)"systemd-cgls liste tous les processus du cgroup de l’unité. La ligne ps décrit l’unique processus que systemd supervise. Lisez ces deux informations ensemble. Un MainPID égal à 0 signifie que systemd n’a aucun processus à surveiller. Un MainPID qui correspond à un shell alors que le cgroup contient également votre daemon indique le cas du wrapper décrit plus haut. Un cgroup qui contient plus de processus que prévu indique la présence d’un launcher ou d’un daemon qui effectue un fork.
systemctl status app.service
journalctl -u app.service -bsystemctl status affiche ensemble la ligne d’état et l’arborescence du cgroup. Cette commande répond donc souvent aux deux questions à la fois. journalctl -u limité au boot courant avec -b affiche les événements de démarrage et d’arrêt que systemd a enregistrés pour l’unité, avec les codes de sortie qu’il a vus. Si le daemon écrit dans son propre fichier de log au lieu du journal, lisez également ce fichier, car systemd ne peut enregistrer que ce qui lui parvient.
Lorsque vous modifiez Type=, rechargez la configuration et redémarrez l’unité.
systemd-analyze verify /etc/systemd/system/app.service
sudo systemctl daemon-reload
sudo systemctl restart app.servicesystemd-analyze verify analyse le fichier et signale les paramètres qu’il ne peut pas accepter. daemon-reload demande à systemd de relire les fichiers d’unité depuis le disque. Une modification de Type= ne s’applique pas à une unité déjà en cours d’exécution. Le redémarrage est donc obligatoire.
Testez ensuite la modification. Relevez le PID du processus qui vous intéresse réellement dans systemd-cgls, puis arrêtez-le. Exécutez immédiatement systemctl is-active app.service. Si Type= est correct, l’unité quitte l’état active. Si elle reste active, systemd surveille toujours autre chose.
Quel Type= systemd devez-vous utiliser
- Un programme qui reste au premier plan :
Type=exec. - Un programme qui prend en charge la notification de disponibilité :
Type=notify, etnotify-reloads’il confirme également les rechargements. - Un daemon qui exige de passer en arrière-plan :
Type=forkingavecPIDFile=, ou son option de premier plan avecType=exec. - Un script qui effectue une tâche puis se termine :
Type=oneshot, ainsi queRemainAfterExit=yeslorsque l’objectif est de laisser un état persistant. - Un lanceur qui se termine alors que ses processus enfants continuent de fonctionner :
Type=simpleavecExitType=cgroup.
Si vous ne savez pas quel type utiliser pour un daemon tiers, consultez d’abord son unit file fourni avec le paquet. L’exécution de systemctl cat sur une unité fournie par la distribution affiche le Type= choisi en amont. Ce choix a été testé par davantage de personnes que le vôtre.
FAQ
Pourquoi mon unité systemd reste-t-elle active alors que le processus est terminé ?
Parce que le processus que systemd considère comme principal est toujours actif. systemd surveille un seul PID par service, choisi selon Type=, et non chaque processus du cgroup de l’unité. Un script wrapper lancé avec Type=simple est la cause la plus courante : le shell est le PID principal. L’unité reste donc active lorsqu’un daemon lancé en arrière-plan par le shell se termine. Exécutez systemctl show -p MainPID app.service, puis affichez le cgroup de l’unité avec systemd-cgls --unit=app.service et comparez les deux résultats.
Quelle est la différence entre Type=simple et Type=exec ?
Type=simple considère que l’unité a démarré dès que systemd a créé le processus, avant l’exécution du binaire. Ainsi, un chemin incorrect dans ExecStart= produit quand même un job de démarrage réussi, suivi d’un échec. Type=exec attend que l’exécution ait réussi. L’échec est donc signalé directement par le job de démarrage. Les deux types considèrent le même processus comme le PID principal. Type=exec nécessite systemd 240 ou une version ultérieure.
Ai-je encore besoin de PIDFile= avec Type=forking ?
Oui, lorsque le daemon en écrit un. Sans ce paramètre, systemd utilise GuessMainPID=, qui repose sur une estimation et n’est fiable que pour un service qui se stabilise avec un seul processus. Si cette estimation est incorrecte ou impossible, la détection des échecs et le redémarrage automatique cessent de fonctionner pour cette unité. Configurez PIDFile= avec le chemin exact que le daemon utilise, généralement sous /run.
Quand dois-je utiliser RemainAfterExit=yes ?
Lorsque le rôle de l’unité consiste à modifier l’état du système plutôt qu’à maintenir un processus en fonctionnement. Une unité Type=oneshot qui charge les règles du firewall ou démarre une stack de conteneurs se termine dès que son travail est terminé. Sans RemainAfterExit=yes, l’unité devient inactive. systemctl stop n’a alors rien à arrêter et ne peut pas exécuter de nettoyage avec ExecStop=. Avec ce paramètre, l’unité reste active sans aucun processus. C’est le comportement attendu dans ce cas.
La modification de Type= nécessite-t-elle un daemon-reload ?
Oui, ainsi qu’un redémarrage de l’unité. systemctl daemon-reload demande à systemd de relire les fichiers d’unité présents sur le disque, mais une instance en cours d’exécution conserve le Type= avec lequel elle a démarré. Exécutez sudo systemctl daemon-reload, puis sudo systemctl restart app.service avant d’effectuer vos tests. Sinon, vous observez encore l’ancien comportement de supervision.