Administration Linux

Processus Linux et systemd : ps, signaux, unités, journalctl et timers

Intermédiaire11 min de lectureMis à jour

Cette page a été traduite de l'anglais avec l'aide de l'IA, puis vérifiée par rapport à l'original. Lire la version anglaise

Un système Linux en marche est un arbre de processus supervisé par systemd. Ce guide commence par les processus — états, ps et top, signaux, jobs en arrière-plan et priorité d'ordonnancement — puis passe à systemd : ce qu'est une unité, comment écrire et durcir un service, comment les dépendances et les targets ordonnent le démarrage, et quand un timer est une meilleure réponse qu'une entrée cron.

Ce que vous allez apprendre

  • Interpréter les colonnes d'état, de mémoire et de CPU dans la sortie de ps et de top
  • Envoyer le bon signal pour arrêter, recharger ou inspecter un processus, et savoir ce qui ne peut pas être intercepté
  • Piloter les jobs au premier plan et en arrière-plan, et détacher un travail de longue durée du terminal
  • Écrire, activer et durcir une unité de service systemd, et lire ses logs avec journalctl
  • Exprimer l'ordre de démarrage avec les dépendances et les targets, et planifier des tâches avec les timers systemd

Tout ce qui tourne sur une machine Linux est un processus, et chaque processus, à l'exception du premier, a été créé par un autre. Ce simple fait explique beaucoup de choses : pourquoi une variable du shell ne survit pas à un script, pourquoi fermer un terminal tue ce qu'il a lancé, et pourquoi la gestion des services est en réalité une affaire de filiation et de durée de vie plutôt que de lancement de programmes.

Pendant ses premières décennies, Linux gérait ce cycle de vie avec des scripts shell dans /etc/init.d. Toutes les distributions actuelles démarrent désormais avec systemd, qui supervise directement les services, suit leurs processus dans des control groups, collecte leur sortie dans un journal structuré et lance les choses en parallèle selon les dépendances déclarées. Ce guide présente d'abord les notions de processus dont vous avez besoin dans le shell, puis la couche systemd qui s'appuie dessus.

La vie d'un processus

Un processus naît lorsqu'un processus existant appelle fork(), qui produit une copie quasi identique ; la copie appelle en général execve() pour remplacer son image mémoire par un autre programme. L'enfant hérite de l'environnement du parent, de ses descripteurs de fichiers ouverts, de son répertoire de travail et de son umask — c'est précisément cet héritage qui rend possibles les redirections du shell, comme l'explique le guide sur les bases du shell.

Quand un processus se termine, il devient un zombie jusqu'à ce que son parent appelle wait() pour récupérer son code de sortie. Si le parent meurt en premier, l'orphelin est rattaché au PID 1, qui se charge de le récupérer. Sur un hôte systemd, le PID 1 est systemd lui-même ; dans un conteneur, c'est le point d'entrée de votre image, quel qu'il soit. Voilà pourquoi les conteneurs qui exécutent un script shell en PID 1 accumulent les zombies et ignorent les signaux, à moins que le script ne soit écrit pour assumer ce rôle.

La colonne d'état affichée par les outils indique ce que le noyau attend :

ÉtatSignification
Ren cours d'exécution ou exécutable — sur un CPU ou dans la file d'exécution
Ssommeil interruptible — en attente d'une entrée, d'un timer, d'un socket
Dsommeil non interruptible — généralement bloqué sur une E/S disque ou NFS ; impossible à tuer
Tarrêté, typiquement par Ctrl-Z ou SIGSTOP
Zzombie — terminé, code de sortie pas encore récupéré

Une accumulation de processus en état D est la signature classique d'un problème de stockage, pas d'un problème de CPU. Cette distinction compte quand vous lisez la charge moyenne (load average), et elle est approfondie dans logs et dépannage.

Voir ce qui tourne

ps affiche un instantané. Deux invocations couvrent l'essentiel des besoins : ps aux, dans le style BSD, pour une vue d'ensemble large, et ps -ef pour les mêmes informations dans le style UNIX. La vue en arbre (forest) est celle qu'il vaut la peine de retenir, car la question porte le plus souvent sur la filiation.

$ ps -eo pid,ppid,user,stat,pcpu,pmem,rss,etime,cmd --sort=-pcpu | head -5
  PID  PPID USER     STAT %CPU %MEM   RSS     ELAPSED CMD
 1842     1 postgres Ss    7.4  9.1 748120  3-04:11:52 /usr/lib/postgresql/16/bin/postgres
 2310  1842 postgres Ss    2.1  1.4 118004  3-04:11:50 postgres: checkpointer
 9921     1 www-data S     1.8  0.6  52208    02:14:07 php-fpm: pool www

$ ps -ef --forest | grep -A3 sshd

RSS est la taille de l'ensemble résident (resident set size), c'est-à-dire la mémoire physique réellement mappée ; VSZ (taille virtuelle) compte l'espace d'adressage, et elle est couramment énorme et dénuée de sens pour une JVM ou une base de données. Comparez les processus sur la RSS, pas sur la VSZ.

Pour une vue en direct, top se rafraîchit toutes les quelques secondes : appuyez sur 1 pour détailler l'utilisation CPU par cœur, M pour trier par mémoire, P par CPU, k pour envoyer un signal à un processus et f pour choisir les colonnes. htop, s'il est installé, ajoute une arborescence défilable, la prise en charge de la souris et l'envoi de signaux à un processus depuis un menu. pidstat 2, du paquet sysstat, échantillonne le CPU, la mémoire et les E/S de chaque processus à intervalle fixe, ce qui est bien plus parlant qu'un instantané isolé quand vous traquez un pic intermittent.

Pour trouver un processus par son nom, c'est pgrep, et pgrep -a affiche la ligne de commande complète :

$ pgrep -a -u www-data php-fpm
9921 php-fpm: pool www
9922 php-fpm: pool www

Les signaux

Un signal est une petite notification asynchrone délivrée à un processus. La plupart peuvent être interceptés par un gestionnaire, ignorés, ou laissés à leur action par défaut.

SignalNuméroAction par défautUsage typique
SIGHUP1terminaisonraccrochage du terminal ; par convention, « recharger la configuration » pour les démons
SIGINT2terminaisonCtrl-C
SIGQUIT3terminaison + core dumpCtrl-\ ; une JVM affiche plutôt un thread dump
SIGKILL9terminaisonimpossible à intercepter, dernier recours
SIGTERM15terminaisonle signal poli par défaut de kill et de systemctl stop
SIGSTOP / SIGCONT19 / 18arrêt / reprisepause et reprise impossibles à intercepter
SIGUSR1 / SIGUSR210 / 12terminaisondéfinis par l'application, souvent pour la rotation des logs ou l'activation du debug
kill 9921                 # SIGTERM by default
kill -HUP $(pgrep -o nginx)
pkill -u bob -TERM        # every process belonging to bob
kill -QUIT <jvm-pid>      # thread dump into the JVM's stdout
kill -0 9921 && echo "still alive"   # signal 0 tests existence and permission only

Dégainer kill -9 en premier est la mauvaise habitude la plus répandue dans ce domaine. Cela prive le processus de toute chance de vider un tampon d'écriture, de valider une transaction ou de nettoyer un fichier de verrou, et laisse derrière lui des sockets et des fichiers PID obsolètes. Envoyez SIGTERM, laissez-lui quelques secondes, puis passez à l'étape supérieure. Si un processus ignore SIGTERM de manière répétée, c'est un bug à signaler, pas une raison de continuer à utiliser -9.

SIGKILL ne peut rien non plus pour un processus bloqué en état D : il est coincé dans le noyau, et le signal ne sera délivré qu'à son retour en espace utilisateur. Réparez plutôt le stockage ou le montage réseau qu'il attend.

Le contrôle des jobs dans le shell

Les shells interactifs regroupent les processus d'une même ligne de commande en un job, pour que vous puissiez les suspendre et les reprendre.

$ ./import-data.sh
^Z
[1]+  Stopped                 ./import-data.sh
$ bg                    # resume it in the background
$ jobs -l
[1]+ 21507 Running       ./import-data.sh &
$ fg %1                 # bring it back to the foreground

& lance directement une commande en arrière-plan ; wait bloque jusqu'à la fin des jobs en arrière-plan. Rien de tout cela ne survit à la session : quand le terminal se ferme, SIGHUP est envoyé au job. nohup cmd & et disown %1 contournent tous deux le problème, mais sur un système moderne les vraies réponses sont un multiplexeur de terminal (tmux, screen) pour le travail interactif, et une unité transitoire pour tout ce qui tourne sans surveillance :

systemd-run --unit=import-once --collect /usr/local/bin/import-data.sh
journalctl -u import-once -f

La priorité d'ordonnancement

L'ordonnanceur du noyau pondère les processus selon leur valeur nice, de -20 (la plus favorable) à 19 (la moins favorable). Seul root peut abaisser une valeur nice.

nice -n 10 tar -czf /backup/site.tar.gz /var/www   # start a batch job politely
renice -n 5 -p 21507                               # adjust a running one
ionice -c 3 -p 21507                               # idle I/O class: only when nothing else wants the disk

Les valeurs nice sont une indication, pas un quota. Quand il vous faut un vrai plafond, utilisez les limites de control groups que systemd expose sur n'importe quelle unité — CPUQuota=, MemoryMax=, IOWeight= — car elles sont imposées par le noyau au lieu d'être négociées par l'ordonnanceur.

systemd : les unités et le fichier d'unité

systemd gère des unités, chacune décrite par un fichier au format INI. Le suffixe détermine le type : .service pour un démon ou une tâche ponctuelle, .socket pour l'activation par socket, .timer pour les tâches planifiées, .mount et .automount pour les systèmes de fichiers, .target pour un point de regroupement, .path pour les déclencheurs sur le système de fichiers, .slice pour le partitionnement des ressources.

Les fichiers d'unité proviennent de trois endroits, par ordre de priorité croissante : /usr/lib/systemd/system/ (installés par les paquets, à ne jamais modifier), /etc/systemd/system/ (les vôtres), et les répertoires drop-in comme /etc/systemd/system/nginx.service.d/override.conf, qui surchargent quelques paramètres d'une unité fournie par un paquet. systemctl edit nginx crée le drop-in pour vous ; systemctl cat nginx affiche la configuration effective, fusionnée.

Voici une unité complète pour une application Java, du type de celle que vous déploieriez avec le packaging décrit dans packaging et déploiement :

# /etc/systemd/system/orders-api.service
[Unit]
Description=Orders API
Documentation=https://example.com/runbooks/orders-api
After=network-online.target postgresql.service
Wants=network-online.target

[Service]
Type=exec
User=orders
Group=orders
WorkingDirectory=/opt/orders-api
EnvironmentFile=/etc/orders-api/env
ExecStart=/usr/lib/jvm/java-21-openjdk-amd64/bin/java -XX:MaxRAMPercentage=70 -jar app.jar
ExecReload=/bin/kill -HUP $MAINPID
Restart=on-failure
RestartSec=5s
TimeoutStopSec=30s

# Hardening: each line removes something the service does not need
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/var/lib/orders-api
MemoryMax=2G

[Install]
WantedBy=multi-user.target

Trois champs méritent un commentaire. Type= indique à systemd à quel moment le service est considéré comme démarré : exec dès que le binaire est exécuté, simple dès qu'il est forké, notify quand le programme signale qu'il est prêt via sd_notify, forking pour les démons à l'ancienne qui passent eux-mêmes en arrière-plan, oneshot pour une tâche qui s'exécute puis se termine. Une erreur ici explique pourquoi une unité dépendante démarre parfois avant que le service n'écoute réellement ; notify est l'option exacte quand le programme la prend en charge.

Restart=on-failure redémarre après un code de sortie non nul ou un signal, mais pas après un arrêt propre. Associez-le à StartLimitIntervalSec= et StartLimitBurst= dans [Unit] pour qu'une boucle de plantages finisse par abandonner au lieu de marteler la machine.

WantedBy=multi-user.target est ce sur quoi agit systemctl enable : l'activation crée un lien symbolique dans multi-user.target.wants/. C'est pour cela qu'activer et démarrer sont deux opérations distinctes — l'une concerne le prochain démarrage, l'autre l'instant présent.

Piloter systemd

sudo systemctl daemon-reload            # after adding or editing any unit file
sudo systemctl enable --now orders-api  # start it and start it at boot
systemctl status orders-api             # state, PID, cgroup, last log lines
systemctl is-active orders-api          # scriptable: exit code plus one word
sudo systemctl restart orders-api
sudo systemctl reload orders-api        # runs ExecReload, no downtime if supported
systemctl list-units --type=service --state=running
systemctl list-unit-files --state=enabled
systemctl show orders-api -p MemoryMax -p Restart

Deux commandes se révèlent précieuses quand le démarrage est lent ou en échec : systemd-analyze blame classe les unités par temps de démarrage, et systemd-analyze critical-chain montre le chemin de dépendances qui a réellement déterminé la durée du boot. systemctl list-dependencies orders-api affiche l'arbre dans l'autre sens.

systemctl mask va plus loin que disable : il fait pointer l'unité vers /dev/null, si bien que rien, pas même la dépendance d'une autre unité, ne peut la démarrer. Pensez à faire unmask avant de vous demander pourquoi le service d'un paquet refuse de se lancer.

Lire les logs d'une unité

systemd capture la sortie standard et l'erreur standard de chaque unité dans le journal, en les étiquetant avec le nom de l'unité, le PID, l'identifiant de boot et la priorité. journalctl permet de l'interroger.

journalctl -u orders-api -f                # follow, like tail -f
journalctl -u orders-api --since "1 hour ago" -p err
journalctl -b -1 -u orders-api             # previous boot
journalctl -u orders-api -o json-pretty | head -40
journalctl --disk-usage

La rétention du journal et sa persistance après un redémarrage se configurent dans /etc/systemd/journald.conf ; la question du stockage et la relation avec /var/log sont traitées dans le guide de dépannage.

Targets, dépendances et ordre de démarrage

Une target est une unité qui en regroupe d'autres et remplace les anciens runlevels. multi-user.target correspond à un système entièrement démarré sans session graphique ; graphical.target l'inclut et y ajoute un gestionnaire d'affichage ; rescue.target est le mode mono-utilisateur. systemctl get-default et set-default déterminent celle qui est utilisée au démarrage.

Dépendances et ordre sont volontairement deux notions distinctes :

  • Requires= et Wants= entraînent le démarrage d'une autre unité — avec Requires, votre unité échoue si l'autre échoue ; avec Wants, non.
  • After= et Before= se contentent d'ordonner des unités déjà en cours de démarrage ; elles n'entraînent rien.

Une unité avec After=postgresql.service mais sans Wants= démarrera après la base de données si la base démarre de toute façon, et immédiatement sinon. Presque tous les signalements du type « mon service démarre trop tôt » viennent d'un Wants= manquant, ou de l'utilisation de network.target (interfaces configurées) là où network-online.target (une adresse réellement utilisable) était visé.

Les timers plutôt que cron

Une unité timer active une autre unité selon un calendrier. Il faut deux fichiers : la tâche et le déclencheur.

# /etc/systemd/system/db-backup.service
[Unit]
Description=Nightly database dump

[Service]
Type=oneshot
User=postgres
ExecStart=/usr/local/bin/db-backup.sh
# /etc/systemd/system/db-backup.timer
[Unit]
Description=Run the nightly database dump

[Timer]
OnCalendar=*-*-* 02:30:00
RandomizedDelaySec=15m
Persistent=true

[Install]
WantedBy=timers.target
sudo systemctl enable --now db-backup.timer
systemctl list-timers --all
systemd-analyze calendar "Mon..Fri 09:00"    # verify an expression before trusting it
sudo systemctl start db-backup.service       # run it once, now, to test

Persistent=true exécute une tâche manquée dès que la machine redémarre — la propriété qui manque à cron, et la raison pour laquelle des sauvegardes sautent silencieusement un jour sur les portables et les instances spot. RandomizedDelaySec évite qu'une centaine de machines ne frappent le même endpoint à 02:30:00 pile. OnUnitActiveSec= exprime « toutes les 15 minutes depuis la dernière exécution » pour les tâches dont la durée varie. Et comme la tâche est une unité ordinaire, sa sortie arrive dans le journal au lieu d'être envoyée par mail dans une boîte que personne ne lit.

Cron existe toujours et convient pour une poignée de tâches personnelles ; crontab -e modifie votre propre table et /etc/cron.d/ contient les entrées système, avec une colonne utilisateur. Gardez simplement à l'esprit que cron s'exécute avec un environnement minimal et sans fichiers de connexion : les scripts ont besoin de chemins absolus et d'un PATH explicite.

Erreurs courantes

  • Modifier une unité fournie par un paquet dans /usr/lib/systemd/system. La prochaine mise à jour l'écrase. Utilisez systemctl edit ou un fichier dans /etc/systemd/system.
  • Oublier daemon-reload. systemd met les fichiers d'unité en cache ; une modification reste invisible tant que vous n'avez pas rechargé, et status ne vous dira pas pourquoi.
  • Confondre enable et start. L'activation ne concerne que le démarrage. Utilisez enable --now quand vous voulez les deux.
  • Utiliser Type=simple pour un programme qui forke. systemd suit le mauvais PID, MAINPID est faux, et le rechargement comme l'arrêt se comportent mal.
  • After=network.target pour tout ce qui écoute sur une adresse publique. Utilisez network-online.target avec le Wants= correspondant.
  • Tuer avec -9 par réflexe. Vous perdez le chemin d'arrêt propre que le service implémente déjà.
  • Compter sur nice pour protéger une machine chargée. Définissez plutôt CPUQuota= et MemoryMax= sur l'unité.

Points clés à retenir

  • Les processus forment un arbre : les enfants héritent de l'environnement et des descripteurs, les orphelins sont adoptés par le PID 1, et un zombie est un code de sortie non récupéré, pas un programme en cours d'exécution.
  • ps -eo avec des colonnes explicites, top/htop et pgrep répondent à la plupart des questions du type « qu'est-ce qui tourne, et pourquoi » ; comparez la RSS, pas la VSZ.
  • Envoyez SIGTERM et attendez avant SIGKILL ; SIGHUP recharge la configuration par convention, et rien n'atteint un processus bloqué dans une E/S non interruptible.
  • Un fichier d'unité systemd déclare comment lancer un programme, quand il est considéré comme démarré, comment le redémarrer et à quoi il a le droit de toucher ; les drop-ins surchargent sans risque les unités fournies par les paquets.
  • Wants=/Requires= entraînent le démarrage d'unités, After=/Before= se contentent de les ordonner, et network-online.target est ce dont les services réseau ont réellement besoin.
  • Le journal contient la sortie de chaque unité ; journalctl -u … -p err --since est le chemin le plus rapide vers ce qui a mal tourné.
  • Les timers offrent aux tâches planifiées les mêmes unités, la même journalisation, les mêmes dépendances et la même sémantique de rattrapage que tout ce que systemd exécute.

Sources et lectures complémentaires

Questions fréquentes

Pourquoi mon service s'arrête-t-il quand je ferme la session SSH ?
Un programme lancé depuis un shell de connexion appartient à cette session. Quand le terminal disparaît, le noyau envoie SIGHUP au groupe au premier plan, et la plupart des programmes se terminent. Préfixer la commande par nohup ou la passer en arrière-plan avec une esperluette ne fait que masquer le problème. Tout ce qui doit survivre a sa place dans une unité systemd, ou au minimum dans systemd-run --user --scope, qui le sort du cgroup de la session.
Quelle est la différence concrète entre SIGTERM et SIGKILL ?
SIGTERM est une demande polie : le processus peut l'intercepter, vider ses tampons, fermer ses sockets, supprimer son fichier PID et se terminer proprement. SIGKILL ne peut être ni intercepté, ni bloqué, ni ignoré — le noyau détruit immédiatement le processus : les transactions en cours sont perdues et les fichiers temporaires restent en place. Envoyez toujours SIGTERM d'abord, attendez, et réservez SIGKILL à un processus qui ne répond vraiment plus.
Pourquoi un processus apparaît-il comme defunct ou zombie, et comment s'en débarrasser ?
Un zombie est un processus terminé dont le code de sortie n'a pas encore été récupéré par son parent via wait(). Il n'occupe ni mémoire ni CPU, seulement une entrée dans la table des processus. Impossible de le tuer : il est déjà mort. Corrigez le parent — redémarrez-le ou envoyez-lui SIGCHLD — et les entrées disparaissent. Si le parent se termine, init adopte les enfants et les récupère.
Faut-il utiliser un timer systemd ou une tâche cron ?
Les timers s'intègrent au reste de systemd : la tâche s'exécute comme une unité, sa sortie part dans le journal, les dépendances et les limites de ressources s'appliquent, Persistent=true rattrape les exécutions manquées après un arrêt, et RandomizedDelaySec étale la charge sur un parc de machines. Cron demande moins de fichiers et convient pour un script périodique simple sur une machine gérée à la main. Sur un serveur que vous administrez déjà avec des unités, préférez les timers par cohérence.