SSD Nodes Learn Hosting plans →
Guides Matt ConnorPar Matt Connor · Mis à jour le 2026-08-22

systemd Type= simple, forking ou notify : lequel choisir ?

Votre unité reste active alors que le daemon est arrêté ? Comparez simple, exec, forking, oneshot et notify pour identifier le vrai PID principal.

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 de Type= dans la section [Service] détermine ce processus. Si vous choisissez la mauvaise valeur, systemd surveille 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 policy de redémarrage ne résoudra pas le problème. Restart= intervient lorsque le processus principal s’arrête. Restart=always ne se déclenche donc jamais tant que le PID principal (process identifier) correspond à un processus encore actif. Corrigez d’abord Type=. Le comportement de systemd après l’arrêt réel du processus principal relève d’une autre décision, expliquée dans ce guide sur Restart= et RestartSec=.

Que décide réellement Type= ?

Chaque valeur de Type= répond simultanément à deux questions : quand systemd peut-il considérer cette unité comme démarrée, et quel processus 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 trop tôt qu’il est « démarré » permet aux unités dépendantes de s’exécuter avant que votre service puisse répondre.

La seconde réponse contrôle la supervision. systemd place chaque processus créé 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= prend par défaut la valeur control-group, si bien que 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 sortie met fin à l’unité et dont le code de sortie devient le résultat de l’unité. La confusion commence lorsque l’on considère le cgroup comme s’il s’agissait du PID principal.

Type=simple signale un démarrage avant l’exécution du binaire

Type=simple est la valeur par défaut lorsque ExecStart= est défini et que Type= et BusName= sont absents. systemd crée le processus, considère immédiatement l’unité comme démarrée et traite ce processus comme le 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. Dans sa propre table, il le nomme EXEC et le 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 place 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 que le programme démarre réellement

Type=exec est simple avec une étape supplémentaire. systemd considère l’unité comme démarrée uniquement lorsque le fork et l’exécution du binaire ont réussi. Un binaire absent ou un User= qui ne peut pas être résolu échoue alors au démarrage lui-même, au lieu de signaler une réussite puis d’échouer silencieusement quelques instants plus tard.

Type=exec est arrivé dans systemd 240. Toutes les distributions serveur actuelles le proposent donc. Ubuntu 24.04 fournit systemd 255 et Debian 13 fournit systemd 257, en août 2026. Vérifiez la vôtre avec systemctl --version.

Le coût est une étape de synchronisation supplémentaire au démarrage. Le gain est un code de retour fiable de 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 de ExecStart= va créer un processus fils, puis se terminer volontairement. systemd attend la fin de ce premier processus, puis considère l’unité comme démarrée. Le processus fils restant est le daemon. Cette pratique date de l’ère SysV : une fois le script init terminé, plus rien ne supervisait le daemon, et le fichier PID était le seul moyen de savoir quel processus était en cours d’exécution. Cette limitation est au cœur de la raison pour laquelle systemd a remplacé les scripts init.

La difficulté concerne l’identité du processus. Le processus lancé par systemd a disparu. systemd doit donc déterminer quel processus restant est le processus principal. Définissez PIDFile= sur le fichier écrit par le daemon, généralement un chemin sous /run, afin que systemd y lise le PID. systemd vérifie également que le PID indiqué dans ce fichier correspond à un processus appartenant déjà à ce service. Un fichier obsolète qui désigne un processus sans rapport est donc rejeté, et non utilisé sans vérification.

Sans PIDFile=, GuessMainPID= s’applique et sa valeur par défaut est yes. Cette déduction n’est fiable que lorsque le service se stabilise avec un seul processus. La documentation indique clairement la limite : si le daemon comprend plusieurs processus, la déduction peut être incorrecte et la détection des défaillances 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 fils 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 suppose 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 lorsqu’une autre unité doit attendre sa fin. C’est également 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=. Ces lignes 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 inactive. RemainAfterExit=yes la maintient à l’état active, sans aucun processus en cours. C’est la version volontaire du symptôme décrit en haut de cette page. Ce comportement est correct lorsque le rôle de l’unité consiste à laisser un état en place 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 modèle utilisé par une stack Docker Compose qui revient après un redémarrage : 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 que déclenche une planification. 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, ce que signifie NotifyAccess=main, et ce que suppose Type=notify. 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 cette commande s’exécute dans un processus court distinct. Il doit donc utiliser NotifyAccess=all, et systemd peut ne pas être en mesure d’attribuer un message dont l’émetteur s’est déjà terminé. Un service qui implémente 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 le même handshake aux reloads. Ainsi, systemctl reload se termine lorsque le service indique que le reload est terminé, et non lorsque le signal a été envoyé. WatchdogSec= demande à un service utilisant les notifications d’envoyer un message keep-alive à intervalles réguliers. systemd considère qu’une échéance manquée est 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. Il nécessite BusName= et devient la valeur par défaut dès que BusName= est défini. Utilisez-le 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 option évite que la sortie de la console au démarrage se mélange aux messages d’état. Elle ne définit pas l’ordre de démarrage et n’a pas sa place dans un service standard.

Pourquoi un script wrapper laisse systemd suivre le mauvais PID

Voici la structure à l’origine du 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 9101

systemd enregistre le shell comme PID principal. Le shell reste actif tant que exporter s’exécute au premier plan. Si server s’arrête, 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 dans le cgroup de l’unité pendant toute cette période, et systemctl stop effectue donc toujours correctement le nettoyage. C’est la supervision qui ne fonctionne plus, pas le nettoyage.

La correction 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.yaml

exec remplace le shell par le programme indiqué et conserve le même PID. Le PID enregistré par systemd appartient donc désormais au daemon. Mieux encore, supprimez le wrapper. Environment= et EnvironmentFile= transmettent les variables, tandis que ExecStartPre= exécute l’étape de préparation. systemd peut ainsi lancer directement le daemon et connaître son PID dès le départ.

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 structure que systemd supervise le mieux. C’est aussi la seule manière d’attribuer à chaque processus son propre comportement de redémarrage.

Ce que change 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/launcher

Cela résout un problème précis. Un launcher qui démarre le traitement réel puis se termine ferait, avec ExitType=main, considérer l’unité comme arrêtée par systemd, qui tuerait 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 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 superviseur de plusieurs processus indépendants. ExitType= ne peut pas non plus être combiné avec Type=oneshot.

Le cgroup reçoit également les données de comptabilisation des ressources. Les limites telles que MemoryMax= et CPUQuota= s’appliquent donc à chaque processus lancé par l’unité, quelle que soit la valeur indiquée par Type= pour le PID principal. Ce point est abordé 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 cet ordre sur l’unité que vous déboguez. Lisez d’abord ce que systemd a chargé, puis ce qu’il suit, et comparez ensuite avec la table des processus.

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

systemctl cat affiche le fichier d’unité ainsi que tous les drop-ins qui s’y appliquent. Vous lisez 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 continuer.

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 le seul processus supervisé par systemd. Lisez ces deux informations ensemble. Une valeur MainPID égale à 0 signifie que systemd n’a aucun processus à surveiller. Une valeur MainPID qui correspond à un shell alors que le cgroup contient également votre daemon indique le cas du wrapper décrit plus haut. Un cgroup contenant plus de processus que prévu indique la présence d’un lanceur ou d’un daemon qui se détache en plusieurs processus.

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

systemctl status affiche ensemble la ligne d’état et l’arborescence du cgroup. Il répond donc souvent aux deux questions en même temps. journalctl -u limité à ce boot avec -b affiche les événements de démarrage et d’arrêt que systemd a enregistrés pour l’unité, ainsi que les codes de sortie observés. Si le daemon écrit dans son propre fichier journal au lieu du journal systemd, consultez également ce fichier, car systemd ne peut enregistrer que ce qui lui est transmis.

Lorsque vous modifiez Type=, rechargez la configuration, puis redémarrez l’unité.

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

systemd-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. Récupérez dans systemd-cgls le PID du processus qui vous intéresse réellement, 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 Type= de service 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, et notify-reload s’il confirme également les rechargements.
  • Un daemon qui exige de passer en arrière-plan : Type=forking avec PIDFile=, ou son option de premier plan avec Type=exec.
  • Un script qui effectue une tâche puis se termine : Type=oneshot, ainsi que RemainAfterExit=yes lorsque le but était de laisser un état persistant.
  • Un lanceur qui se termine tandis que ses processus enfants continuent de s’exécuter : Type=simple avec ExitType=cgroup.

Si vous ne savez pas quel type utiliser pour un daemon tiers, consultez d’abord son fichier d’unité fourni par le paquet. Exécuter systemctl cat sur une unité fournie par la distribution affiche le Type= choisi en amont, un choix testé par davantage de personnes que le vôtre.

FAQ

Pourquoi mon unité systemd reste-t-elle active alors que le processus est arrêté ?

Parce que le processus que systemd considère comme principal est toujours en vie. 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, donc l’unité reste 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é est démarrée 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 utilisent le même processus comme PID principal. Type=exec nécessite systemd 240 ou une version plus récente.

Ai-je encore besoin de PIDFile= avec Type=forking ?

Oui, lorsque le daemon en écrit un. Sans ce fichier, systemd utilise GuessMainPID=, qui repose sur une déduction et n’est fiable que pour un service qui se stabilise sous la forme d’un processus unique. Si cette déduction 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 écrit par le daemon, normalement sous /run.

Quand dois-je utiliser RemainAfterExit=yes ?

Lorsque le rôle de l’unité est de modifier l’état du système plutôt que de maintenir un processus en fonctionnement. Une unité Type=oneshot qui charge les règles du pare-feu ou démarre une stack de conteneurs se termine dès que son travail est fini. Sans RemainAfterExit=yes, l’unité devient inactive. systemctl stop n’a alors plus rien à arrêter et ne peut pas exécuter de nettoyage ExecStop=. Avec cette option, l’unité reste active sans aucun processus, ce qui est le comportement attendu dans ce cas.

Le changement de Type= nécessite-t-il 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 de tester. Sinon, vous observez toujours l’ancien comportement de supervision.