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.
Code: Select all
sshd
cron
httpd
syslogd
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.
- 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);
}
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().
2.2 Création d’une nouvelle session avec setsid()
Le processus enfant appelle ensuite :
Code: Select all
if (setsid() == -1) {
/* Erreur */
}
- 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.
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 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);
En le réinitialisant, le démon contrôle explicitement les permissions transmises à :
Code: Select all
open()
mkdir()
creat()
- 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 */
}
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.
Code: Select all
struct rlimit rl;
if (getrlimit(RLIMIT_NOFILE, &rl) == -1) {
/* Erreur */
}
Code: Select all
for (int fd = 0; fd < maxfd; fd++) {
close(fd);
}
6. Redirection de stdin, stdout et stderr
Les descripteurs standards sont :
Code: Select all
0 : stdin
1 : stdout
2 : stderr
Code: Select all
/dev/null
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);
}
- 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);
Code: Select all
BD_NO_CHDIR
BD_NO_CLOSE_FILES
BD_NO_REOPEN_STD_FDS
BD_NO_UMASK0
BD_MAX_CLOSE
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
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;
}
Code: Select all
while (!stop_requested) {
/* Travail du démon */
}
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()
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
Code: Select all
getpid()
8.4 Rechargement de la configuration
La configuration est généralement stockée sous :
Code: Select all
/etc
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.
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 */
}
Code: Select all
sleep()
nanosleep()
timer_create()
timerfd_create()
Code: Select all
while (!stop_requested) {
effectuer_traitement();
sleep(10);
}
10. Journalisation avec syslog
Un démon ne peut pas compter sur :
Code: Select all
printf()
fprintf(stderr, ...)
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
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);
Code: Select all
openlog("mondemon", LOG_PID | LOG_CONS, LOG_DAEMON);
- 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é
14. Écriture d’un message avec syslog()
Prototype :
Code: Select all
void syslog(int priority, const char *format, ...);
Code: Select all
syslog(LOG_INFO, "Le démon a démarré");
Code: Select all
syslog(LOG_INFO, "Client connecté : pid=%ld", (long)getpid());
Code: Select all
syslog(LOG_DAEMON | LOG_ERR,
"Échec de l’ouverture du fichier");
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
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");
}
Code: Select all
Impossible d’ouvrir la configuration : No such file or directory
Mauvais :
Code: Select all
syslog(LOG_ERR, user_string);
Code: Select all
syslog(LOG_ERR, "%s", user_string);
17. Fermeture avec closelog()
Prototype :
Code: Select all
void closelog(void);
Code: Select all
closelog();
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);
Pour accepter tous les messages jusqu’à LOG_INFO :
Code: Select all
setlogmask(LOG_UPTO(LOG_INFO));
Code: Select all
LOG_EMERG
LOG_ALERT
LOG_CRIT
LOG_ERR
LOG_WARNING
LOG_NOTICE
LOG_INFO
Code: Select all
LOG_DEBUG
Code: Select all
setlogmask(
LOG_MASK(LOG_ERR) |
LOG_MASK(LOG_WARNING) |
LOG_MASK(LOG_INFO)
);
19. Configuration du système de journalisation
Historiquement, la configuration de syslog se trouve dans :
Code: Select all
/etc/syslog.conf
Code: Select all
/etc/rsyslog.conf
/etc/rsyslog.d/
systemd-journald
Code: Select all
facility.level action
Code: Select all
*.err /dev/tty10
auth.notice root
authpriv.* /var/log/secure
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()
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é.
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()
Code: Select all
umask()
chdir()
getrlimit()
Code: Select all
close()
open()
dup2()
Code: Select all
sigaction()
wait()
waitpid()
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()
Code: Select all
openlog()
syslog()
closelog()
setlogmask()
- 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.
