Les démons sous Unix/Linux

Ce forum est dédié à apprendre le développement de programmes user mode sur Linux

Moderator: Rick

Post Reply
Hydraxx
Site Admin
Posts: 114
Joined: Mon Jan 12, 2026 4:04 pm
Location: France
Contact:

Les démons sous Unix/Linux

Post by Hydraxx »

Les démons sous Unix/Linux

Introduction

Un démon est un processus qui fonctionne en arrière-plan, généralement pendant une longue durée, sans interaction directe avec l’utilisateur et sans terminal de contrôle.

Les démons assurent de nombreux services du système :
  • gestion des connexions réseau ;
  • exécution de tâches planifiées ;
  • journalisation des événements ;
  • hébergement de services web ;
  • surveillance de ressources ou de fichiers ;
  • exécution de traitements périodiques.
Le nom de nombreux démons se termine par la lettre d :

Code: Select all

sshd
cron
httpd
syslogd
Un simple programme lancé avec & n’est pas forcément un véritable démon. Il peut encore appartenir à la session du terminal et conserver certains descripteurs ou signaux liés à celui-ci.


1. Caractéristiques d’un démon

Un démon possède généralement les caractéristiques suivantes :
  • il fonctionne en arrière-plan ;
  • il ne possède pas de terminal de contrôle ;
  • il reste actif pendant une longue durée ;
  • il attend des événements ou exécute des tâches périodiques ;
  • il journalise ses messages dans le système plutôt que dans un terminal ;
  • il peut démarrer automatiquement avec le système.
Un démon peut par exemple attendre :
  • une connexion réseau ;
  • une modification de fichier ;
  • un signal ;
  • l’expiration d’un délai ;
  • une requête provenant d’un autre processus.

2. Création traditionnelle d’un démon

La transformation d’un processus normal en démon suit plusieurs étapes.

2.1 Premier appel à fork()

Le processus effectue un premier appel à fork() :

Code: Select all

pid_t pid = fork();

if (pid == -1) {
    /* Erreur */
}

if (pid > 0) {
    _exit(EXIT_SUCCESS);
}
Le processus parent se termine tandis que le processus enfant continue.

Cette étape permet notamment :
  • au shell de considérer que la commande initiale est terminée ;
  • au processus enfant de ne plus être leader de groupe ;
  • de permettre l’appel à setsid().
Il est préférable d’utiliser _exit() dans le processus parent créé par fork(), car cette fonction termine immédiatement le processus sans retraiter les tampons de la bibliothèque standard.


2.2 Création d’une nouvelle session avec setsid()

Le processus enfant appelle ensuite :

Code: Select all

if (setsid() == -1) {
    /* Erreur */
}
L’appel à setsid() :
  • crée une nouvelle session ;
  • rend le processus leader de cette session ;
  • crée un nouveau groupe de processus ;
  • détache le processus de son terminal de contrôle.
Après cet appel, le processus ne dépend plus du terminal depuis lequel il a été lancé.


2.3 Second appel à fork()

Le programme effectue généralement un deuxième fork() :

Code: Select all

pid = fork();

if (pid == -1) {
    /* Erreur */
}

if (pid > 0) {
    _exit(EXIT_SUCCESS);
}
Le parent se termine et le nouvel enfant continue.

Le processus final n’est plus leader de session. Il ne peut donc normalement plus acquérir accidentellement un nouveau terminal de contrôle.


3. Réinitialisation du masque de création

Le démon peut remettre son masque de création à zéro :

Code: Select all

umask(0);
Le masque umask hérité du processus parent peut retirer certaines permissions lors de la création de fichiers.

En le réinitialisant, le démon contrôle explicitement les permissions transmises à :

Code: Select all

open()
mkdir()
creat()
La permission finale d’un fichier dépend toujours :
  • du mode demandé par le programme ;
  • du masque umask actif.

4. Changement du répertoire courant

Un démon change souvent son répertoire courant vers la racine :

Code: Select all

if (chdir("/") == -1) {
    /* Erreur */
}
Un processus conserve une référence vers son répertoire courant.

S’il reste dans un répertoire appartenant à un système de fichiers monté, ce système de fichiers peut devenir impossible à démonter tant que le processus fonctionne.

Le répertoire / est généralement utilisé car il reste disponible pendant toute la durée de fonctionnement du système.

Certains démons utilisent toutefois un autre répertoire de travail adapté à leur service.


5. Fermeture des descripteurs hérités

Après un fork(), le processus enfant hérite des descripteurs ouverts par le parent.

Un démon doit fermer ceux dont il n’a pas besoin afin d’éviter :
  • de conserver des fichiers ouverts inutilement ;
  • de maintenir des pipes ou sockets ouverts ;
  • de bloquer un périphérique ;
  • d’empêcher le démontage d’un système de fichiers ;
  • de gaspiller des ressources.
La limite des descripteurs peut être obtenue avec getrlimit() :

Code: Select all

struct rlimit rl;

if (getrlimit(RLIMIT_NOFILE, &rl) == -1) {
    /* Erreur */
}
Puis les descripteurs peuvent être fermés :

Code: Select all

for (int fd = 0; fd < maxfd; fd++) {
    close(fd);
}
Sur les systèmes modernes, il peut exister des méthodes plus efficaces, mais la boucle reste une méthode classique et portable.


6. Redirection de stdin, stdout et stderr

Les descripteurs standards sont :

Code: Select all

0 : stdin
1 : stdout
2 : stderr
Un démon n’ayant plus de terminal, ces descripteurs sont généralement redirigés vers :

Code: Select all

/dev/null
Exemple :

Code: Select all

int fd = open("/dev/null", O_RDWR);

if (fd == -1) {
    /* Erreur */
}

if (dup2(fd, STDIN_FILENO) == -1) {
    /* Erreur */
}

if (dup2(fd, STDOUT_FILENO) == -1) {
    /* Erreur */
}

if (dup2(fd, STDERR_FILENO) == -1) {
    /* Erreur */
}

if (fd > STDERR_FILENO) {
    close(fd);
}
Cette redirection permet :
  • d’éviter une lecture accidentelle depuis un terminal ;
  • d’ignorer les sorties standard inutiles ;
  • de garantir que les descripteurs 0, 1 et 2 restent valides ;
  • d’éviter qu’un futur open() ne récupère accidentellement ces numéros.

7. Fonction de démonisation

La logique de démonisation peut être regroupée dans une fonction :

Code: Select all

int becomeDaemon(int flags);
Les drapeaux permettent éventuellement de désactiver certaines opérations :

Code: Select all

BD_NO_CHDIR
BD_NO_CLOSE_FILES
BD_NO_REOPEN_STD_FDS
BD_NO_UMASK0
BD_MAX_CLOSE
Exemple simplifié :

Code: Select all

#include <sys/types.h>
#include <sys/stat.h>
#include <sys/resource.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>

int becomeDaemon(void)
{
    pid_t pid;
    struct rlimit rl;
    int fd;
    long maxfd;

    pid = fork();

    if (pid == -1)
        return -1;

    if (pid > 0)
        _exit(EXIT_SUCCESS);

    if (setsid() == -1)
        return -1;

    pid = fork();

    if (pid == -1)
        return -1;

    if (pid > 0)
        _exit(EXIT_SUCCESS);

    umask(0);

    if (chdir("/") == -1)
        return -1;

    if (getrlimit(RLIMIT_NOFILE, &rl) == -1)
        return -1;

    if (rl.rlim_max == RLIM_INFINITY)
        maxfd = 8192;
    else
        maxfd = (long)rl.rlim_max;

    for (fd = 0; fd < maxfd; fd++)
        close(fd);

    fd = open("/dev/null", O_RDWR);

    if (fd == -1)
        return -1;

    if (dup2(fd, STDIN_FILENO) == -1)
        return -1;

    if (dup2(fd, STDOUT_FILENO) == -1)
        return -1;

    if (dup2(fd, STDERR_FILENO) == -1)
        return -1;

    if (fd > STDERR_FILENO)
        close(fd);

    return 0;
}

8. Règles de programmation d’un démon

8.1 Gestion des signaux

Les signaux permettent notamment :

Code: Select all

SIGTERM : arrêt propre
SIGHUP  : rechargement de la configuration
SIGCHLD : changement d’état d’un processus enfant
Un gestionnaire de signal doit rester très simple.

Il peut modifier une variable globale :

Code: Select all

#include <signal.h>

static volatile sig_atomic_t stop_requested = 0;

static void handle_sigterm(int sig)
{
    stop_requested = 1;
}
La boucle principale traite ensuite la demande :

Code: Select all

while (!stop_requested) {
    /* Travail du démon */
}
Il faut éviter d’appeler des fonctions non sûres depuis un gestionnaire de signal.


8.2 Gestion des processus enfants

Lorsqu’un démon crée des processus enfants, il doit récupérer leur état avec :

Code: Select all

wait()
waitpid()
Sinon, les enfants terminés peuvent rester sous forme de processus zombies.

Exemple :

Code: Select all

while (waitpid(-1, NULL, WNOHANG) > 0) {
}

8.3 Instance unique

Certains démons doivent garantir qu’une seule instance fonctionne.

Une solution courante consiste à créer un fichier PID :

Code: Select all

/run/nom_daemon.pid
Ce fichier peut contenir l’identifiant du processus :

Code: Select all

getpid()
Le fichier doit être verrouillé afin d’empêcher une seconde instance de démarrer.


8.4 Rechargement de la configuration

La configuration est généralement stockée sous :

Code: Select all

/etc
Le démon peut relire son fichier de configuration lorsqu’il reçoit SIGHUP.


8.5 Réduction des privilèges

Un démon peut avoir besoin de privilèges élevés au démarrage afin de :
  • ouvrir un port privilégié ;
  • accéder à un périphérique ;
  • créer un fichier protégé ;
  • modifier une ressource système.
Après ces opérations, il est préférable de réduire ses privilèges et de fonctionner sous un utilisateur spécialisé.

Cela limite les conséquences d’une vulnérabilité dans le démon.


9. Boucle principale d’un démon

Un démon exécute souvent une boucle principale :

Code: Select all

while (!stop_requested) {
    /* Attendre un événement */
    /* Traiter la requête */
    /* Journaliser le résultat */
}
Pour une tâche périodique, il peut utiliser :

Code: Select all

sleep()
nanosleep()
timer_create()
timerfd_create()
Exemple simple :

Code: Select all

while (!stop_requested) {
    effectuer_traitement();
    sleep(10);
}
Une attente bloquante sur un événement est généralement préférable à une boucle qui consomme continuellement le processeur.


10. Journalisation avec syslog

Un démon ne peut pas compter sur :

Code: Select all

printf()
fprintf(stderr, ...)
car il ne possède généralement plus de terminal.

L’interface syslog permet d’envoyer les messages vers un système centralisé de journalisation.

Le chemin logique est le suivant :

Code: Select all

Application
    |
  syslog()
    |
service de journalisation
    |
fichier, console, terminal ou serveur distant
Selon le système, les messages peuvent être traités par :

Code: Select all

syslogd
rsyslogd
systemd-journald

11. API syslog

Les fonctions principales sont :

Code: Select all

openlog()
syslog()
closelog()
setlogmask()

11.1 openlog()

Prototype :

Code: Select all

#include <syslog.h>

void openlog(const char *ident, int option, int facility);
Exemple :

Code: Select all

openlog("mondemon", LOG_PID | LOG_CONS, LOG_DAEMON);
Paramètres :
  • ident : nom ajouté au début des messages ;
  • option : options de journalisation ;
  • facility : catégorie du programme.

12. Options de openlog()

LOG_PID

Ajoute le PID du processus dans chaque message.

LOG_CONS

Écrit le message sur la console système si l’envoi vers le service de journalisation échoue.

LOG_NDELAY

Ouvre immédiatement la connexion avec le système de journalisation.

LOG_ODELAY

Retarde l’ouverture de la connexion jusqu’au premier appel à syslog().

LOG_NOWAIT

Évite d’attendre un éventuel processus enfant créé par l’implémentation.

LOG_PERROR

Écrit également le message sur stderr.

Cette option est principalement utile pendant le développement ou avant la démonisation complète.


13. Facilities syslog

La facility indique l’origine générale du message.

Code: Select all

LOG_AUTH       : authentification
LOG_AUTHPRIV   : authentification sensible
LOG_CRON       : tâches planifiées
LOG_DAEMON     : démons système
LOG_FTP        : serveur FTP
LOG_KERN       : noyau
LOG_LPR        : impression
LOG_MAIL       : courrier électronique
LOG_NEWS       : service de nouvelles
LOG_SYSLOG     : service de journalisation
LOG_USER       : applications utilisateur
LOG_UUCP       : système UUCP
LOG_LOCAL0     : usage local personnalisé
LOG_LOCAL1     : usage local personnalisé
LOG_LOCAL2     : usage local personnalisé
LOG_LOCAL3     : usage local personnalisé
LOG_LOCAL4     : usage local personnalisé
LOG_LOCAL5     : usage local personnalisé
LOG_LOCAL6     : usage local personnalisé
LOG_LOCAL7     : usage local personnalisé
Pour un démon classique, LOG_DAEMON est généralement adapté.


14. Écriture d’un message avec syslog()

Prototype :

Code: Select all

void syslog(int priority, const char *format, ...);
Exemple :

Code: Select all

syslog(LOG_INFO, "Le démon a démarré");
Avec des arguments :

Code: Select all

syslog(LOG_INFO, "Client connecté : pid=%ld", (long)getpid());
Il est possible de combiner une facility et un niveau :

Code: Select all

syslog(LOG_DAEMON | LOG_ERR,
       "Échec de l’ouverture du fichier");
Lorsque la facility a déjà été définie dans openlog(), le niveau seul suffit :

Code: Select all

syslog(LOG_ERR, "Erreur d’ouverture");

15. Niveaux de gravité

Les niveaux sont classés du plus grave au moins grave :

Code: Select all

LOG_EMERG   : système inutilisable
LOG_ALERT   : action immédiate nécessaire
LOG_CRIT    : condition critique
LOG_ERR     : erreur
LOG_WARNING : avertissement
LOG_NOTICE  : événement normal mais important
LOG_INFO    : information
LOG_DEBUG   : débogage
Exemples :

Code: Select all

syslog(LOG_ERR,
       "Impossible d’ouvrir la configuration");

syslog(LOG_WARNING,
       "Connexion client interrompue");

syslog(LOG_INFO,
       "Service démarré");

syslog(LOG_DEBUG,
       "Valeur du compteur : %d", compteur);

16. Utilisation de %m

Dans la chaîne de format de syslog(), %m est remplacé par le message correspondant à la valeur actuelle de errno.

Exemple :

Code: Select all

int fd = open("/etc/mondemon.conf", O_RDONLY);

if (fd == -1) {
    syslog(LOG_ERR,
           "Impossible d’ouvrir la configuration : %m");
}
Cela peut produire :

Code: Select all

Impossible d’ouvrir la configuration : No such file or directory
Il ne faut jamais utiliser directement une chaîne contrôlée par l’utilisateur comme chaîne de format.

Mauvais :

Code: Select all

syslog(LOG_ERR, user_string);
Correct :

Code: Select all

syslog(LOG_ERR, "%s", user_string);
Les caractères % présents dans une chaîne utilisateur pourraient sinon être interprétés comme des spécificateurs de format.


17. Fermeture avec closelog()

Prototype :

Code: Select all

void closelog(void);
Exemple :

Code: Select all

closelog();
Cette fonction ferme la connexion avec le système de journalisation.

Elle peut être appelée lors de l’arrêt propre du démon.


18. Filtrage avec setlogmask()

Prototype :

Code: Select all

int setlogmask(int mask_priority);
Cette fonction permet de choisir les niveaux réellement transmis.

Pour accepter tous les messages jusqu’à LOG_INFO :

Code: Select all

setlogmask(LOG_UPTO(LOG_INFO));
Cela inclut :

Code: Select all

LOG_EMERG
LOG_ALERT
LOG_CRIT
LOG_ERR
LOG_WARNING
LOG_NOTICE
LOG_INFO
Mais exclut :

Code: Select all

LOG_DEBUG
Pour sélectionner plusieurs niveaux précis :

Code: Select all

setlogmask(
    LOG_MASK(LOG_ERR) |
    LOG_MASK(LOG_WARNING) |
    LOG_MASK(LOG_INFO)
);
La fonction retourne l’ancien masque.


19. Configuration du système de journalisation

Historiquement, la configuration de syslog se trouve dans :

Code: Select all

/etc/syslog.conf
Les systèmes modernes peuvent utiliser :

Code: Select all

/etc/rsyslog.conf
/etc/rsyslog.d/
systemd-journald
Une règle syslog possède généralement la forme :

Code: Select all

facility.level    action
Exemples :

Code: Select all

*.err             /dev/tty10
auth.notice       root
authpriv.*        /var/log/secure
La partie de gauche sélectionne les messages.

La partie de droite indique leur destination :
  • fichier ;
  • console ;
  • terminal ;
  • utilisateur connecté ;
  • pipe nommé ;
  • serveur distant.

20. Exemple complet de démon avec syslog

Code: Select all

#define _DEFAULT_SOURCE

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <syslog.h>
#include <fcntl.h>

static volatile sig_atomic_t stop_requested = 0;

static void handle_signal(int sig)
{
    if (sig == SIGTERM || sig == SIGINT)
        stop_requested = 1;
}

static int daemonize(void)
{
    pid_t pid;
    int fd;

    pid = fork();

    if (pid == -1)
        return -1;

    if (pid > 0)
        _exit(EXIT_SUCCESS);

    if (setsid() == -1)
        return -1;

    pid = fork();

    if (pid == -1)
        return -1;

    if (pid > 0)
        _exit(EXIT_SUCCESS);

    umask(0);

    if (chdir("/") == -1)
        return -1;

    for (fd = 0; fd < 1024; fd++)
        close(fd);

    fd = open("/dev/null", O_RDWR);

    if (fd == -1)
        return -1;

    if (dup2(fd, STDIN_FILENO) == -1)
        return -1;

    if (dup2(fd, STDOUT_FILENO) == -1)
        return -1;

    if (dup2(fd, STDERR_FILENO) == -1)
        return -1;

    if (fd > STDERR_FILENO)
        close(fd);

    return 0;
}

int main(void)
{
    struct sigaction sa = {0};

    sa.sa_handler = handle_signal;
    sigemptyset(&sa.sa_mask);

    if (sigaction(SIGTERM, &sa, NULL) == -1)
        return EXIT_FAILURE;

    if (sigaction(SIGINT, &sa, NULL) == -1)
        return EXIT_FAILURE;

    if (daemonize() == -1)
        return EXIT_FAILURE;

    openlog("mondemon",
            LOG_PID | LOG_CONS,
            LOG_DAEMON);

    setlogmask(LOG_UPTO(LOG_INFO));

    syslog(LOG_INFO, "Démon démarré");

    while (!stop_requested) {
        syslog(LOG_INFO, "Le démon fonctionne");
        sleep(10);
    }

    syslog(LOG_INFO, "Arrêt du démon");

    closelog();

    return EXIT_SUCCESS;
}

21. Démons traditionnels et systemd

La démonisation classique repose sur :

Code: Select all

fork()
setsid()
fork()
Cependant, sur un système utilisant systemd, il est souvent préférable que le service reste au premier plan.

Le gestionnaire de services se charge alors :
  • du démarrage ;
  • de l’arrêt ;
  • du redémarrage ;
  • de la supervision ;
  • de la journalisation ;
  • des dépendances ;
  • de l’identité utilisateur ;
  • des restrictions de sécurité.
Dans ce cas, le programme ne doit pas forcément effectuer un double fork().

Il peut rester au premier plan et écrire ses messages sur stdout, stderr ou syslog, tandis que systemd assure sa supervision.


22. APIs essentielles

Création et détachement du démon

Code: Select all

fork()
setsid()
_exit()
Environnement du processus

Code: Select all

umask()
chdir()
getrlimit()
Descripteurs

Code: Select all

close()
open()
dup2()
Signaux et enfants

Code: Select all

sigaction()
wait()
waitpid()
Journalisation

Code: Select all

openlog()
syslog()
closelog()
setlogmask()

23. Schéma récapitulatif

Code: Select all

Processus normal
      |
    fork()
      |
Fin du parent
      |
    setsid()
      |
Second fork()
      |
Fin du second parent
      |
   umask(0)
      |
   chdir("/")
      |
Fermeture des descripteurs
      |
Redirection de 0, 1 et 2
vers /dev/null
      |
Initialisation des signaux
      |
    openlog()
      |
Boucle principale du démon
      |
    syslog()
      |
   closelog()
      |
Arrêt propre

Conclusion

Un démon Unix/Linux est un processus de longue durée, détaché du terminal et destiné à fournir un service en arrière-plan.

La démonisation traditionnelle repose principalement sur :

Code: Select all

fork()
setsid()
fork()
umask()
chdir()
close()
dup2()
La journalisation repose principalement sur :

Code: Select all

openlog()
syslog()
closelog()
setlogmask()
Un démon correctement conçu doit aussi :
  • gérer proprement les signaux ;
  • récupérer ses processus enfants ;
  • éviter plusieurs instances non désirées ;
  • recharger sa configuration si nécessaire ;
  • réduire ses privilèges ;
  • journaliser ses erreurs ;
  • libérer ses ressources lors de l’arrêt.

Who is online

Users browsing this forum: No registered users and 1 guest