Signaux Linux avancés : génération, attente, temps réel, sigqueue, sigwaitinfo et signalfd

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

Moderator: Rick

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

Signaux Linux avancés : génération, attente, temps réel, sigqueue, sigwaitinfo et signalfd

Post by Hydraxx »

Signaux Linux avancés : génération, attente, temps réel, sigqueue, sigwaitinfo et signalfd

Objectif du cours : comprendre les parties avancées des signaux Linux, au-delà des bases comme kill(), signal(), sigaction(), sigset_t et sigprocmask().

1. Rappel rapide : un signal, c’est quoi ?

Un signal est une notification envoyée à un processus ou à un thread.

Il peut servir à :
  • interrompre un programme ;
  • demander un arrêt propre ;
  • notifier qu’un enfant a terminé ;
  • prévenir qu’un timer a expiré ;
  • signaler une faute CPU ou mémoire ;
  • réveiller un processus bloqué ;
  • transmettre une petite information avec les signaux temps réel.
Exemples classiques :

Code: Select all

SIGINT    // Ctrl+C
SIGTERM   // demande d'arrêt propre
SIGKILL   // arrêt forcé, impossible à intercepter
SIGSTOP   // pause forcée, impossible à intercepter
SIGCONT   // reprise après stop
SIGSEGV   // segmentation fault
SIGFPE    // erreur arithmétique
SIGILL    // instruction illégale
SIGBUS    // erreur mémoire / bus
SIGCHLD   // enfant terminé ou changé d'état
SIGUSR1   // signal utilisateur 1
SIGUSR2   // signal utilisateur 2
À retenir :

Code: Select all

kill(pid, SIGTERM);
ne veut pas forcément dire “tuer brutalement”. Cela signifie simplement : envoyer le signal SIGTERM au processus.

2. Les 3 dispositions possibles d’un signal

Quand un signal arrive, Linux peut appliquer une disposition.

1. Action par défaut

Chaque signal possède une action par défaut.

Exemples :
  • SIGTERM termine le processus ;
  • SIGSEGV termine le processus et peut produire un core dump ;
  • SIGCHLD est souvent ignoré par défaut ;
  • SIGSTOP stoppe le processus ;
  • SIGCONT reprend le processus.
2. Ignorer le signal

Exemple :

Code: Select all

signal(SIGINT, SIG_IGN);
Cela veut dire que Ctrl+C sera ignoré.

Mais certains signaux ne peuvent pas être ignorés :

Code: Select all

SIGKILL
SIGSTOP
3. Installer un handler

Un handler est une fonction appelée quand le signal arrive.

Exemple simple :

Code: Select all

#include <signal.h>
#include <unistd.h>

void handler(int sig)
{
    write(1, "signal recu\n", 12);
}

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

    sa.sa_handler = handler;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = 0;

    sigaction(SIGINT, &sa, NULL);

    while (1)
        pause();

    return 0;
}
3. Pourquoi sigaction() est préférable à signal()

L’ancienne fonction signal() existe encore, mais elle a eu des comportements historiques différents selon les systèmes UNIX.

Sur d’anciens systèmes, après l’appel du handler, le comportement pouvait revenir à SIG_DFL. Il fallait donc parfois réinstaller le handler dans le handler lui-même.

Aujourd’hui, sous Linux/glibc, signal() est plus stable, mais pour du code système propre, on préfère sigaction().

À retenir :

Code: Select all

signal()    = ancien, simple, moins précis
sigaction() = moderne, robuste, contrôlable
sigaction() permet de contrôler :
  • la fonction handler ;
  • les signaux bloqués pendant le handler ;
  • les options comme SA_RESTART ou SA_SIGINFO ;
  • l’ancien comportement du signal.
Structure importante :

Code: Select all

struct sigaction {
    void     (*sa_handler)(int);
    void     (*sa_sigaction)(int, siginfo_t *, void *);
    sigset_t sa_mask;
    int      sa_flags;
};
4. Core dump files

Un core dump est un fichier contenant l’état mémoire d’un programme au moment du crash.

Cela permet de debugger après coup avec gdb.

Signaux pouvant produire un core dump :

Code: Select all

SIGSEGV
SIGABRT
SIGFPE
SIGILL
SIGBUS
Exemple :

Code: Select all

int *p = NULL;
*p = 123; // SIGSEGV
Pour autoriser les core dumps :

Code: Select all

ulimit -c unlimited
Puis analyse :

Code: Select all

gdb ./programme core
Dans gdb :

Code: Select all

bt
info registers
frame 0
list
Piège : un core dump n’est pas toujours généré. Il dépend des limites système, de la configuration du kernel, de systemd-coredump, des permissions et du répertoire courant.

5. Signaux de stop et de reprise

Certains signaux servent à suspendre ou reprendre un processus.

Code: Select all

SIGSTOP   // stop forcé, impossible à intercepter
SIGTSTP   // Ctrl+Z, peut être intercepté
SIGCONT   // reprise du processus
Exemple shell :

Code: Select all

kill -STOP 1234
kill -CONT 1234
Différence :

Code: Select all

SIGSTOP = pause forcée, non interceptable
SIGTSTP = pause demandée par le terminal, interceptable
SIGCONT = reprise
Si un processus est stoppé, il n’exécute plus de code utilisateur. Les signaux peuvent devenir pending, mais ils ne seront pas forcément traités avant reprise.

Exceptions importantes :
  • SIGKILL peut tuer un processus stoppé ;
  • SIGCONT peut reprendre un processus stoppé.
6. Signaux synchrones et asynchrones

Signal synchrone

Un signal synchrone est causé directement par l’instruction exécutée.

Exemples :

Code: Select all

SIGSEGV   // accès mémoire invalide
SIGFPE    // erreur arithmétique
SIGILL    // instruction invalide
SIGBUS    // erreur mémoire ou mapping incorrect
Exemple :

Code: Select all

int *p = NULL;
*p = 10; // SIGSEGV causé par cette instruction
Ici, le signal n’arrive pas “au hasard”. Il est lié à l’instruction fautive.

Signal asynchrone

Un signal asynchrone arrive depuis l’extérieur ou depuis un événement indépendant.

Exemples :

Code: Select all

SIGINT    // Ctrl+C
SIGTERM   // kill
SIGALRM   // timer
SIGCHLD   // enfant terminé
SIGUSR1   // signal utilisateur
SIGUSR2
À retenir :

Code: Select all

synchrone  = causé par l’instruction actuelle
asynchrone = arrive depuis l’extérieur
7. Signaux générés par le matériel

Certains signaux sont générés par une exception CPU ou une erreur mémoire.

Exemples :
  • SIGSEGV : accès à une adresse mémoire interdite ;
  • SIGBUS : erreur d’accès mémoire plus bas niveau, mapping invalide, alignement sur certaines architectures ;
  • SIGILL : instruction CPU invalide ;
  • SIGFPE : erreur arithmétique, division entière par zéro, exception flottante selon configuration.
Exemple SIGFPE :

Code: Select all

int a = 1;
int b = 0;
int c = a / b; // SIGFPE possible
Point important :

Un handler de SIGSEGV ne “répare” pas magiquement le programme.

Si le handler retourne normalement, le programme peut reprendre sur la même instruction fautive, donc recrasher immédiatement.

Mauvais exemple :

Code: Select all

void handler(int sig)
{
    // ne corrige rien
}
Si le programme reprend à l’instruction fautive, il peut entrer dans une boucle infinie de SIGSEGV.

À retenir :

Code: Select all

SIGSEGV/SIGBUS/SIGILL/SIGFPE = souvent état grave du programme
8. Timing et ordre de livraison des signaux

Quand plusieurs signaux sont pending, l’ordre de livraison peut être subtil.

Pour les signaux standards, il ne faut pas construire un protocole qui dépend fortement de l’ordre exact.

Point très important :

Les signaux standards ne sont pas une vraie file d’attente.

Si le même signal standard est envoyé plusieurs fois pendant qu’il est bloqué, le noyau peut retenir seulement :

Code: Select all

SIGUSR1 est pending
et pas :

Code: Select all

SIGUSR1 est arrivé 10 fois
Exemple :

Code: Select all

kill -USR1 pid
kill -USR1 pid
kill -USR1 pid
Si SIGUSR1 est bloqué, le processus peut ne recevoir qu’une seule livraison de SIGUSR1.

À retenir :

Code: Select all

signal standard bloqué 10 fois envoyé = souvent 1 seule livraison
9. Appels système interrompus : EINTR et SA_RESTART

Un signal peut interrompre un appel système bloquant.

Exemples d’appels pouvant être concernés :

Code: Select all

read()
write()
accept()
recv()
send()
wait()
sleep()
nanosleep()
poll()
select()
epoll_wait()
Exemple :

Code: Select all

ssize_t n = read(fd, buffer, sizeof(buffer));

if (n == -1 && errno == EINTR) {
    // l'appel a été interrompu par un signal
}
Dans ce cas, il faut souvent recommencer l’appel.

Exemple robuste :

Code: Select all

ssize_t safe_read(int fd, void *buf, size_t size)
{
    ssize_t ret;

    do {
        ret = read(fd, buf, size);
    } while (ret == -1 && errno == EINTR);

    return ret;
}
Avec sigaction(), on peut utiliser SA_RESTART :

Code: Select all

sa.sa_flags = SA_RESTART;
Cela demande au kernel/libc de relancer automatiquement certains appels système interrompus par un signal.

Mais attention :
  • tous les appels ne sont pas forcément redémarrés ;
  • certaines fonctions retournent quand même EINTR ;
  • il faut encore savoir gérer EINTR dans du code sérieux.
À retenir :

Code: Select all

SA_RESTART aide, mais ne remplace pas une vraie gestion de EINTR
10. Handlers : règle d’or

Un handler doit rester minuscule.

Dans un handler, il ne faut pas appeler n’importe quelle fonction.

À éviter :

Code: Select all

printf()
malloc()
free()
pthread_mutex_lock()
std::cout
fopen()
fprintf()
exit()
Pourquoi ?

Parce que beaucoup de fonctions ne sont pas async-signal-safe.

Elles peuvent utiliser des locks internes, de la mémoire dynamique ou un état global déjà interrompu par le signal.

Pattern propre :

Code: Select all

#include <signal.h>
#include <unistd.h>

volatile sig_atomic_t running = 1;

void handler(int sig)
{
    running = 0;
}

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

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

    sigaction(SIGINT, &sa, NULL);

    while (running) {
        // travail
    }

    return 0;
}
À retenir :

Code: Select all

dans un handler :
- modifier un sig_atomic_t
- écrire avec write()
- faire le minimum
11. sa_mask : bloquer automatiquement pendant le handler

Dans struct sigaction, le champ sa_mask permet de dire quels signaux doivent être bloqués pendant l’exécution du handler.

Exemple :

Code: Select all

struct sigaction sa = {0};

sa.sa_handler = handler;
sigemptyset(&sa.sa_mask);
sigaddset(&sa.sa_mask, SIGTERM);

sigaction(SIGINT, &sa, NULL);
Cela signifie :

Code: Select all

quand le handler de SIGINT tourne,
SIGTERM est temporairement bloqué
Utilité :
  • éviter qu’un handler soit interrompu par certains signaux ;
  • éviter des problèmes de réentrance ;
  • protéger une zone courte du handler.
12. Signaux temps réel

Les signaux temps réel ont été ajoutés à POSIX pour corriger certaines limites des signaux standards.

Ils vont de :

Code: Select all

SIGRTMIN
à :

Code: Select all

SIGRTMAX
Exemples :

Code: Select all

SIGRTMIN
SIGRTMIN + 1
SIGRTMIN + 2
Avantages :
  • ils peuvent être mis en file ;
  • plusieurs occurrences peuvent être conservées ;
  • ils peuvent transporter une donnée ;
  • leur ordre est mieux défini.
Comparaison :

Code: Select all

signal standard envoyé 10 fois pendant blocage
=> peut être livré 1 fois

signal temps réel envoyé 10 fois pendant blocage
=> peut être livré 10 fois
Attention :

Il existe une limite au nombre de signaux temps réel pouvant être en file. Si la limite est atteinte, sigqueue() peut échouer.

13. sigqueue() : envoyer signal + donnée

kill() envoie seulement un signal :

Code: Select all

kill(pid, SIGUSR1);
sigqueue() envoie un signal avec une petite donnée.

Prototype :

Code: Select all

#include <signal.h>

int sigqueue(pid_t pid, int sig, const union sigval value);
union sigval :

Code: Select all

union sigval {
    int   sival_int;
    void *sival_ptr;
};
Exemple côté émetteur :

Code: Select all

#include <signal.h>
#include <unistd.h>

int main(void)
{
    pid_t pid = 1234;

    union sigval val;
    val.sival_int = 42;

    sigqueue(pid, SIGRTMIN, val);

    return 0;
}
À retenir :

Code: Select all

kill()     = signal simple
sigqueue() = signal + petite donnée
14. Handler avancé avec SA_SIGINFO et siginfo_t

Pour recevoir les informations envoyées avec sigqueue(), il faut utiliser un handler avancé.

Handler simple :

Code: Select all

void handler(int sig)
{
}
Handler avancé :

Code: Select all

void handler(int sig, siginfo_t *info, void *context)
{
}
Installation :

Code: Select all

struct sigaction sa = {0};

sa.sa_sigaction = handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_SIGINFO;

sigaction(SIGRTMIN, &sa, NULL);
Champs utiles de siginfo_t :

Code: Select all

info->si_pid           // PID de l'émetteur
info->si_uid           // UID de l'émetteur
info->si_code          // origine du signal
info->si_value         // donnée envoyée par sigqueue()
info->si_value.sival_int
info->si_value.sival_ptr
Attention :

Même avec SA_SIGINFO, un handler reste un handler. Il faut éviter printf(), malloc(), etc. Pour traiter proprement l’information, on peut utiliser sigwaitinfo() ou signalfd().

15. sigsuspend() : attendre un signal sans race condition

Problème classique :

Code: Select all

sigprocmask(SIG_UNBLOCK, &set, NULL);
pause();
Le signal peut arriver entre le déblocage et pause(). Résultat : le programme peut manquer le signal et dormir indéfiniment.

sigsuspend() corrige ce problème.

Prototype :

Code: Select all

int sigsuspend(const sigset_t *mask);
sigsuspend() fait atomiquement :
  • remplacer temporairement le masque de signaux ;
  • dormir jusqu’à recevoir un signal ;
  • laisser le handler s’exécuter ;
  • restaurer l’ancien masque.
Pattern propre :

Code: Select all

#define _POSIX_C_SOURCE 200809L
#include <signal.h>
#include <unistd.h>

volatile sig_atomic_t got_sigint = 0;

void handler(int sig)
{
    got_sigint = 1;
}

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

    sa.sa_handler = handler;
    sigemptyset(&sa.sa_mask);
    sigaction(SIGINT, &sa, NULL);

    sigemptyset(&block_mask);
    sigaddset(&block_mask, SIGINT);

    sigprocmask(SIG_BLOCK, &block_mask, &old_mask);

    while (!got_sigint) {
        sigsuspend(&old_mask);
    }

    sigprocmask(SIG_SETMASK, &old_mask, NULL);

    return 0;
}
À retenir :

Code: Select all

sigsuspend() = attendre un signal proprement avec un handler
16. sigwaitinfo() : attendre un signal sans handler

sigwaitinfo() permet d’attendre un signal de manière synchrone.

Cela veut dire :

Code: Select all

au lieu que Linux appelle un handler,
ton code attend et récupère le signal comme un retour de fonction
Prototype :

Code: Select all

#include <signal.h>

int sigwaitinfo(const sigset_t *set, siginfo_t *info);
Pattern important :
  • préparer un sigset_t ;
  • bloquer les signaux qu’on veut attendre ;
  • appeler sigwaitinfo().
Exemple :

Code: Select all

#define _POSIX_C_SOURCE 200809L
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>

int main(void)
{
    sigset_t set;
    siginfo_t info;

    printf("PID: %d\n", getpid());

    sigemptyset(&set);
    sigaddset(&set, SIGUSR1);
    sigaddset(&set, SIGTERM);

    if (sigprocmask(SIG_BLOCK, &set, NULL) == -1) {
        perror("sigprocmask");
        return 1;
    }

    for (;;) {
        int sig = sigwaitinfo(&set, &info);

        if (sig == -1) {
            if (errno == EINTR)
                continue;

            perror("sigwaitinfo");
            return 1;
        }

        printf("signal recu: %d\n", sig);
        printf("pid emetteur: %ld\n", (long)info.si_pid);
        printf("uid emetteur: %ld\n", (long)info.si_uid);

        if (sig == SIGTERM)
            break;
    }

    return 0;
}
Test :

Code: Select all

gcc waitinfo.c -o waitinfo
./waitinfo
kill -USR1 <pid>
kill -TERM <pid>
Point clé :

Les signaux attendus par sigwaitinfo() doivent être bloqués avant.

Sinon, le signal peut être traité par son comportement normal ou par un handler avant que sigwaitinfo() ne le récupère.

À retenir :

Code: Select all

sigwaitinfo() = attendre et consommer un signal bloqué
17. sigwaitinfo() avec sigqueue()

sigwaitinfo() peut récupérer la donnée envoyée par sigqueue().

Récepteur :

Code: Select all

#define _POSIX_C_SOURCE 200809L
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>

int main(void)
{
    sigset_t set;
    siginfo_t info;

    printf("PID recepteur: %d\n", getpid());

    sigemptyset(&set);
    sigaddset(&set, SIGRTMIN);

    sigprocmask(SIG_BLOCK, &set, NULL);

    for (;;) {
        int sig = sigwaitinfo(&set, &info);

        if (sig == -1) {
            if (errno == EINTR)
                continue;
            perror("sigwaitinfo");
            return 1;
        }

        printf("signal: %d\n", sig);
        printf("envoye par PID: %ld\n", (long)info.si_pid);
        printf("valeur: %d\n", info.si_value.sival_int);
    }

    return 0;
}
Émetteur :

Code: Select all

#define _POSIX_C_SOURCE 200809L
#include <signal.h>
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>

int main(int argc, char **argv)
{
    if (argc != 3) {
        fprintf(stderr, "usage: %s <pid> <valeur>\n", argv[0]);
        return 1;
    }

    pid_t pid = (pid_t)strtol(argv[1], NULL, 10);

    union sigval val;
    val.sival_int = atoi(argv[2]);

    if (sigqueue(pid, SIGRTMIN, val) == -1) {
        perror("sigqueue");
        return 1;
    }

    return 0;
}
Compilation :

Code: Select all

gcc receiver.c -o receiver
gcc sender.c -o sender
Test :

Code: Select all

./receiver
./sender <pid> 100
./sender <pid> 200
./sender <pid> 300
Avec un signal temps réel, plusieurs envois peuvent être conservés dans l’ordre.

18. sigtimedwait() : attendre avec timeout

sigtimedwait() est comme sigwaitinfo(), mais avec un temps maximum d’attente.

Prototype :

Code: Select all

int sigtimedwait(const sigset_t *set,
                 siginfo_t *info,
                 const struct timespec *timeout);
Exemple :

Code: Select all

#define _POSIX_C_SOURCE 200809L
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <time.h>

int main(void)
{
    sigset_t set;
    siginfo_t info;
    struct timespec timeout;

    sigemptyset(&set);
    sigaddset(&set, SIGUSR1);

    sigprocmask(SIG_BLOCK, &set, NULL);

    timeout.tv_sec = 5;
    timeout.tv_nsec = 0;

    printf("attente de SIGUSR1 pendant 5 secondes...\n");

    int sig = sigtimedwait(&set, &info, &timeout);

    if (sig == -1) {
        if (errno == EAGAIN) {
            printf("timeout: aucun signal recu\n");
            return 0;
        }

        perror("sigtimedwait");
        return 1;
    }

    printf("signal recu: %d\n", sig);
    return 0;
}
À retenir :

Code: Select all

sigwaitinfo()  = attente infinie
sigtimedwait() = attente avec timeout
19. Différence entre handler, sigsuspend(), sigwaitinfo()

sigaction() + handler

Code: Select all

le signal arrive
le code courant est interrompu
Linux appelle le handler
le code reprend après
Avantage : réaction immédiate.

Inconvénient : handler dangereux si on fait trop de choses dedans.

sigsuspend()

Code: Select all

le programme dort
un signal arrive
le handler s'exécute
sigsuspend() retourne
Avantage : évite la race condition entre déblocage et pause.

Inconvénient : nécessite encore un handler.

sigwaitinfo()

Code: Select all

le signal est bloqué
le programme attend avec sigwaitinfo()
sigwaitinfo() retourne le signal et ses infos
pas besoin de handler
Avantage : traitement dans le flux normal du programme, plus facile à raisonner.

Résumé :

Code: Select all

sigaction()   = réaction asynchrone
sigsuspend()  = dormir jusqu'à un signal + handler
sigwaitinfo() = attendre/consommer un signal sans handler
20. signalfd() : recevoir les signaux comme des fichiers

signalfd() est une API Linux non standard POSIX.

Elle transforme des signaux en événements lisibles sur un descripteur de fichier.

Prototype :

Code: Select all

#include <sys/signalfd.h>

int signalfd(int fd, const sigset_t *mask, int flags);
Principe :

Code: Select all

signal -> signalfd -> read()
Au lieu d’avoir un handler, tu fais :

Code: Select all

read(sfd, &fdsi, sizeof(fdsi));
Champs utiles de struct signalfd_siginfo :

Code: Select all

ssi_signo  // numéro du signal
ssi_pid    // PID de l'émetteur
ssi_uid    // UID de l'émetteur
ssi_int    // valeur entière envoyée par sigqueue()
ssi_ptr    // pointeur envoyé par sigqueue()
21. Exemple signalfd()

Code: Select all

#define _GNU_SOURCE
#include <sys/signalfd.h>
#include <signal.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    sigset_t mask;
    int sfd;
    struct signalfd_siginfo fdsi;

    printf("PID: %d\n", getpid());

    sigemptyset(&mask);
    sigaddset(&mask, SIGINT);
    sigaddset(&mask, SIGTERM);
    sigaddset(&mask, SIGUSR1);

    if (sigprocmask(SIG_BLOCK, &mask, NULL) == -1) {
        perror("sigprocmask");
        return 1;
    }

    sfd = signalfd(-1, &mask, 0);
    if (sfd == -1) {
        perror("signalfd");
        return 1;
    }

    for (;;) {
        ssize_t s = read(sfd, &fdsi, sizeof(fdsi));
        if (s != sizeof(fdsi)) {
            perror("read");
            return 1;
        }

        printf("signal recu via fd: %u\n", fdsi.ssi_signo);
        printf("pid emetteur: %u\n", fdsi.ssi_pid);
        printf("uid emetteur: %u\n", fdsi.ssi_uid);

        if (fdsi.ssi_signo == SIGTERM)
            break;

        if (fdsi.ssi_signo == SIGINT)
            break;
    }

    close(sfd);
    return 0;
}
Test :

Code: Select all

gcc signalfd_demo.c -o signalfd_demo
./signalfd_demo
kill -USR1 <pid>
kill -TERM <pid>
À retenir :

Code: Select all

signalfd() = signaux lisibles avec read()
22. signalfd() avec epoll

signalfd() devient très intéressant dans un serveur.

Sans signalfd :

Code: Select all

sockets gérées dans epoll
signaux gérés par handlers
Avec signalfd :

Code: Select all

sockets + signaux dans la même boucle epoll
Schéma :

Code: Select all

epoll_wait()
    -> socket serveur lisible
    -> socket client lisible
    -> signalfd lisible
Exemple minimal :

Code: Select all

#define _GNU_SOURCE
#include <sys/signalfd.h>
#include <sys/epoll.h>
#include <signal.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    sigset_t mask;
    int sfd;
    int epfd;
    struct epoll_event ev;
    struct epoll_event events[8];

    sigemptyset(&mask);
    sigaddset(&mask, SIGINT);
    sigaddset(&mask, SIGTERM);

    if (sigprocmask(SIG_BLOCK, &mask, NULL) == -1) {
        perror("sigprocmask");
        return 1;
    }

    sfd = signalfd(-1, &mask, SFD_NONBLOCK | SFD_CLOEXEC);
    if (sfd == -1) {
        perror("signalfd");
        return 1;
    }

    epfd = epoll_create1(EPOLL_CLOEXEC);
    if (epfd == -1) {
        perror("epoll_create1");
        return 1;
    }

    ev.events = EPOLLIN;
    ev.data.fd = sfd;

    if (epoll_ctl(epfd, EPOLL_CTL_ADD, sfd, &ev) == -1) {
        perror("epoll_ctl");
        return 1;
    }

    printf("PID: %d\n", getpid());
    printf("Ctrl+C ou kill -TERM pour quitter proprement\n");

    for (;;) {
        int n = epoll_wait(epfd, events, 8, -1);

        if (n == -1) {
            perror("epoll_wait");
            return 1;
        }

        for (int i = 0; i < n; i++) {
            if (events[i].data.fd == sfd) {
                struct signalfd_siginfo fdsi;

                ssize_t r = read(sfd, &fdsi, sizeof(fdsi));
                if (r != sizeof(fdsi))
                    continue;

                printf("signal recu: %u\n", fdsi.ssi_signo);

                if (fdsi.ssi_signo == SIGINT || fdsi.ssi_signo == SIGTERM) {
                    printf("arret propre\n");
                    close(sfd);
                    close(epfd);
                    return 0;
                }
            }
        }
    }
}
À retenir :

Code: Select all

signalfd + epoll = très propre pour serveur Linux
23. Signaux comme IPC

Les signaux peuvent être vus comme une forme d’IPC, mais très limitée.

IPC signifie inter-process communication.

Les signaux permettent à un processus de notifier un autre processus.

Exemples :

Code: Select all

kill(pid, SIGTERM);      // demande d'arrêt
kill(pid, SIGHUP);       // recharge config possible
sigqueue(pid, SIGRTMIN, val); // notification + petite valeur
Mais les signaux ont de grosses limites.
  • Les signaux standards ne sont pas vraiment queueés.
  • Les handlers sont asynchrones.
  • On ne peut transporter qu’une petite donnée avec sigqueue().
  • Ce n’est pas adapté aux gros messages.
  • Ce n’est pas adapté aux protocoles complexes.
  • L’ordre peut devenir subtil.
À retenir :

Code: Select all

signal = notification
pipe/socket/shared memory = vraie communication
Bons usages :
  • arrêt propre ;
  • rechargement de configuration ;
  • réveil d’un processus ;
  • notification simple ;
  • timer ;
  • gestion d’enfant avec SIGCHLD.
Mauvais usages :
  • envoyer des structures complètes ;
  • faire un protocole réseau ;
  • envoyer beaucoup d’événements ;
  • remplacer une socket ;
  • transférer un fichier ;
  • faire une messagerie.
24. Anciennes API System V et BSD

Le chapitre mentionne aussi d’anciennes APIs.

System V :

Code: Select all

sigset()
sighold()
sigrelse()
sigignore()
sigpause()
BSD :

Code: Select all

sigvec()
sigblock()
sigsetmask()
siggetmask()
Aujourd’hui, il faut surtout savoir les reconnaître dans du vieux code.

Équivalents modernes :

Code: Select all

signal(), sigset()      -> sigaction()
sighold(), sigblock()   -> sigprocmask(SIG_BLOCK)
sigrelse()              -> sigprocmask(SIG_UNBLOCK)
sigpause()              -> sigsuspend()
sigvec()                -> sigaction()
À retenir :

Code: Select all

ancien code UNIX = possible de voir ces APIs
code moderne Linux/POSIX = sigaction(), sigprocmask(), sigsuspend()
25. Tableau mental des API importantes

Code: Select all

sigaction()
    installer une réaction à un signal

sigemptyset()
    vider un ensemble de signaux

sigaddset()
    ajouter un signal dans un ensemble

sigdelset()
    retirer un signal d’un ensemble

sigismember()
    tester si un signal est dans un ensemble

sigprocmask()
    bloquer / débloquer / remplacer le masque de signaux

sigpending()
    voir les signaux en attente

sigsuspend()
    attendre un signal avec changement atomique du masque

kill()
    envoyer un signal simple

raise()
    envoyer un signal à soi-même

sigqueue()
    envoyer un signal avec une petite donnée

sigwaitinfo()
    attendre un signal bloqué et obtenir siginfo_t

sigtimedwait()
    comme sigwaitinfo(), mais avec timeout

signalfd()
    recevoir les signaux via un file descriptor

epoll()
    surveiller signalfd avec d’autres fd
26. Comparaison rapide

Code: Select all

kill()
    envoie un signal simple

sigqueue()
    envoie un signal + int/pointeur

sigaction()
    installe un handler

sigsuspend()
    dort jusqu'à un signal, puis handler

sigwaitinfo()
    attend et consomme le signal sans handler

sigtimedwait()
    pareil avec timeout

signalfd()
    transforme les signaux en événements read()
27. Patterns à connaître

Pattern 1 : arrêt propre avec sig_atomic_t

Code: Select all

volatile sig_atomic_t running = 1;

void handler(int sig)
{
    running = 0;
}

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

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

    sigaction(SIGINT, &sa, NULL);
    sigaction(SIGTERM, &sa, NULL);

    while (running) {
        // boucle principale
    }

    return 0;
}
Pattern 2 : attendre proprement avec sigsuspend()

Code: Select all

sigprocmask(SIG_BLOCK, &block_set, &old_set);

while (!flag) {
    sigsuspend(&old_set);
}

sigprocmask(SIG_SETMASK, &old_set, NULL);
Pattern 3 : attendre sans handler avec sigwaitinfo()

Code: Select all

sigemptyset(&set);
sigaddset(&set, SIGUSR1);
sigprocmask(SIG_BLOCK, &set, NULL);

sigwaitinfo(&set, &info);
Pattern 4 : signaux dans epoll avec signalfd()

Code: Select all

sigprocmask(SIG_BLOCK, &mask, NULL);
sfd = signalfd(-1, &mask, SFD_NONBLOCK);
epoll_ctl(epfd, EPOLL_CTL_ADD, sfd, &ev);
28. Pièges classiques

Piège 1 : croire qu’un signal bloqué est supprimé

Faux.

Code: Select all

bloqué = pending
ignoré = jeté
Piège 2 : croire que les signaux standards comptent les occurrences

Faux.

Code: Select all

10 SIGUSR1 standards pendant blocage
=> pas forcément 10 handlers
Piège 3 : faire printf() dans un handler

Mauvais en production.

Utiliser plutôt :

Code: Select all

write()
volatile sig_atomic_t
Piège 4 : oublier EINTR

Un signal peut interrompre un syscall.

Code: Select all

if (ret == -1 && errno == EINTR)
    recommencer ou gérer proprement
Piège 5 : utiliser sigwaitinfo() sans bloquer le signal

Mauvais pattern.

Il faut :

Code: Select all

sigprocmask(SIG_BLOCK, &set, NULL);
sigwaitinfo(&set, &info);
Piège 6 : utiliser les signaux pour une vraie messagerie

Mauvais choix.

Utiliser :

Code: Select all

pipe
socket Unix
socket TCP
message queue
shared memory
eventfd
29. Mini-projet conseillé

Pour bien comprendre, fais ce mini-projet :

Programme 1 : receiver
  • affiche son PID ;
  • bloque SIGRTMIN ;
  • attend avec sigwaitinfo() ;
  • affiche PID émetteur + valeur reçue ;
  • quitte si valeur == 0.
Programme 2 : sender
  • prend PID + valeur en argument ;
  • envoie SIGRTMIN avec sigqueue().
Ensuite, version 2 :
  • remplace sigwaitinfo() par signalfd() ;
  • mets signalfd dans epoll ;
  • ajoute stdin dans epoll ;
  • si l’utilisateur tape quit, arrêt propre.
Ce projet te donne une vraie compréhension système des signaux modernes Linux.

30. Résumé final

À retenir absolument :

Code: Select all

1. sigaction() remplace signal() dans du code propre.

2. Les handlers doivent rester minuscules.

3. Les signaux synchrones viennent du code fautif :
   SIGSEGV, SIGBUS, SIGILL, SIGFPE.

4. Les signaux asynchrones viennent de l’extérieur :
   SIGINT, SIGTERM, SIGALRM, SIGCHLD.

5. Les signaux standards ne sont pas une vraie file d’attente.

6. Les signaux temps réel sont queueés et peuvent transporter une petite donnée.

7. sigqueue() = signal + donnée.

8. SA_SIGINFO + siginfo_t = infos détaillées sur le signal.

9. sigsuspend() évite la race condition entre déblocage et pause.

10. sigwaitinfo() permet d’attendre un signal sans handler.

11. sigtimedwait() ajoute un timeout.

12. signalfd() transforme les signaux en fd lisible avec read().

13. signalfd + epoll est excellent pour les serveurs Linux.

14. Les signaux sont utiles pour notifier, pas pour transporter une grosse logique.
Phrase finale à retenir :

Code: Select all

Les signaux Linux sont faits pour interrompre, notifier et synchroniser légèrement.
Pour communiquer vraiment entre processus, il faut plutôt utiliser pipe, socket, eventfd, shared memory ou message queue.
31. Fiche ultra-courte de révision

Code: Select all

sigaction       -> installer handler
sa_mask         -> signaux bloqués pendant handler
SA_RESTART      -> relancer certains syscalls interrompus
SA_SIGINFO      -> handler avancé avec siginfo_t
siginfo_t       -> infos signal : PID, UID, valeur, origine
sigqueue        -> envoyer signal + valeur
SIGRTMIN/MAX    -> signaux temps réel
sigwaitinfo     -> attendre signal bloqué sans handler
sigtimedwait    -> attendre avec timeout
sigsuspend      -> attendre avec handler sans race
signalfd        -> recevoir signaux via read()
EINTR           -> syscall interrompu par signal
core dump       -> image mémoire du crash

Who is online

Users browsing this forum: No registered users and 1 guest