Signal handlers, handlers avancés et appels système interrompus

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:

Signal handlers, handlers avancés et appels système interrompus

Post by Hydraxx »

Signal handlers, handlers avancés et appels système interrompus

Objectif du cours

Ce cours explique la partie avancée des signaux Linux : comment écrire un signal handler propre, pourquoi certaines fonctions sont dangereuses dans un handler, ce que veulent dire réentrant et async-signal-safe, comment utiliser sig_atomic_t, comment faire un handler avancé avec SA_SIGINFO et siginfo_t, comment utiliser une stack alternative avec sigaltstack(), et comment gérer les appels système interrompus avec EINTR et SA_RESTART.

L'idée principale à retenir est simple :
Un signal handler peut interrompre ton programme à n'importe quel moment. Donc le code exécuté dans un handler doit être minimal, prudent et compatible avec les règles async-signal-safe.
Dans le chapitre précédent, on voyait surtout les bases : signal, sigaction(), sigset_t, signal mask, pending signals.

Dans ce chapitre, on passe au niveau supérieur :
  • comment écrire un handler sans casser l'état interne du programme ;
  • pourquoi printf(), malloc() ou free() peuvent être dangereux dans un handler ;
  • comment communiquer proprement entre handler et programme principal ;
  • comment récupérer des informations détaillées sur un signal avec siginfo_t ;
  • comment gérer les crashs mémoire avec SIGSEGV, si_addr et parfois ucontext_t ;
  • comment éviter les problèmes avec les appels système interrompus par des signaux.
1. Rappel : c'est quoi un signal handler ?

Un signal handler est une fonction appelée automatiquement quand un signal est livré au processus ou au thread.

Exemple simple :

Code: Select all

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

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

int main(void)
{
    signal(SIGINT, handler);

    while(1){
        pause();
    }

    return 0;
}
Quand l'utilisateur appuie sur Ctrl+C, le terminal envoie SIGINT. Au lieu de tuer directement le programme, le noyau appelle la fonction handler().

Schéma mental :

Code: Select all

programme en cours d'execution
        ↓
signal recu
        ↓
interruption temporaire du flux normal
        ↓
execution du handler
        ↓
retour au code interrompu
Le piège important : le signal peut arriver pendant que ton programme est en plein milieu d'une fonction sensible.

Exemple mental :

Code: Select all

le programme est dans malloc()
        ↓
SIGINT arrive
        ↓
le handler est execute
        ↓
le handler appelle aussi malloc()
        ↓
risque de corruption interne ou deadlock
C'est pour ça que les handlers doivent être simples.

2. Les signaux ne sont pas des appels de fonction normaux

Un handler n'est pas appelé comme une fonction normale par ton code.

Une fonction normale :

Code: Select all

main() appelle fonctionA()
fonctionA() retourne vers main()
Un signal handler :

Code: Select all

le programme execute n'importe quelle instruction
        ↓
le noyau interrompt le programme
        ↓
le noyau force l'appel du handler
        ↓
le handler retourne
        ↓
le programme reprend ou continue selon le cas
Donc le handler peut tomber pendant :
  • un malloc() ;
  • un printf() ;
  • un accès à une structure globale ;
  • une mise à jour de liste chaînée ;
  • un verrou déjà pris ;
  • un appel système bloquant ;
  • un code critique de ton programme.
C'est cette nature asynchrone qui rend les signaux puissants mais dangereux.

3. Les signaux standards ne sont pas vraiment mis en file d'attente

Point très important : les signaux standards Linux/POSIX ne sont généralement pas comptés un par un.

Si un signal standard est bloqué et envoyé plusieurs fois, le noyau peut seulement retenir :
Ce signal est en attente.
Il ne retient pas forcément le nombre exact d'arrivées.

Exemple :

Code: Select all

SIGUSR1 est bloque
        ↓
SIGUSR1 est envoye 10 fois
        ↓
SIGUSR1 devient pending
        ↓
on debloque SIGUSR1
        ↓
le handler peut etre appele une seule fois
Donc il ne faut pas utiliser les signaux standards comme une file d'événements fiable.

Mauvaise idée :

Code: Select all

1 signal = 1 evenement exact
Meilleure idée :

Code: Select all

1 signal = une notification indiquant qu'au moins un evenement est arrive
Pour compter précisément des événements, il vaut mieux utiliser :
  • un pipe ;
  • un eventfd ;
  • une file de messages ;
  • une socket ;
  • un mécanisme de synchronisation adapté ;
  • éventuellement les signaux temps réel, qui sont plus adaptés à la mise en file.
4. Réentrant : définition simple

Une fonction réentrante est une fonction qui peut être interrompue puis appelée à nouveau sans casser son état interne.

Exemple mental :

Code: Select all

fonctionA() commence
        ↓
signal arrive
        ↓
handler appelle fonctionA() aussi
        ↓
les deux executions ne doivent pas se casser
Une fonction réentrante ne doit pas dépendre d'un état global fragile.

Exemple de fonction naturellement plus sûre :

Code: Select all

int add(int a, int b)
{
    return a + b;
}
Elle utilise seulement ses paramètres et ses variables locales.

Exemple de fonction potentiellement non réentrante :

Code: Select all

char *get_buffer(void)
{
    static char buffer[128];

    /* modification du buffer statique */

    return buffer;
}
Ici, la fonction utilise un buffer statique partagé. Si elle est interrompue et rappelée, le buffer peut être modifié au mauvais moment.

5. Async-signal-safe : définition simple

Une fonction async-signal-safe est une fonction que POSIX considère comme sûre à appeler depuis un signal handler.

Ce n'est pas exactement la même chose que réentrant.

Résumé :

Code: Select all

reentrant            -> peut etre rappelee sans casser son propre etat
async-signal-safe    -> garantie utilisable dans un signal handler
Une fonction peut être réentrante mais pas forcément officiellement async-signal-safe.

Dans un handler, la règle pratique est :
Appelle seulement des fonctions async-signal-safe, ou fais presque rien.
Fonctions souvent acceptées dans un handler :

Code: Select all

write()
_exit()
sigaction()
sigprocmask()
kill()
getpid()
Fonctions à éviter dans un handler :

Code: Select all

printf()
fprintf()
sprintf()
malloc()
free()
exit()
pthread_mutex_lock()
fonctions stdio en general
fonctions complexes de la libc
6. Pourquoi printf() est dangereux dans un handler

Beaucoup de débutants font ça :

Code: Select all

void handler(int sig)
{
    printf("signal recu\n");
}
Pour un petit test, ça peut sembler marcher. Mais en vrai code système, c'est dangereux.

Pourquoi ?

Parce que printf() utilise des buffers internes, de l'état global, parfois des verrous internes.

Exemple dangereux :

Code: Select all

le programme principal est deja dans printf()
        ↓
SIGINT arrive au milieu de printf()
        ↓
le handler appelle printf()
        ↓
les structures internes de stdio peuvent etre dans un etat instable
Version plus propre :

Code: Select all

#include <unistd.h>

void handler(int sig)
{
    const char msg[] = "signal recu\n";
    write(STDOUT_FILENO, msg, sizeof(msg) - 1);
}
write() est beaucoup plus adapté dans un handler.

7. Pourquoi malloc() et free() sont dangereux dans un handler

malloc() et free() manipulent l'allocateur mémoire de la libc.

Cet allocateur possède des structures internes :
  • listes de blocs libres ;
  • métadonnées de heap ;
  • verrous ;
  • zones de mémoire partagées entre appels.
Si un signal arrive pendant un malloc() ou un free(), puis que le handler appelle lui-même malloc() ou free(), tu peux obtenir :
  • corruption mémoire ;
  • deadlock ;
  • comportement indéfini ;
  • crash difficile à comprendre.
Mauvais exemple :

Code: Select all

void handler(int sig)
{
    char *p = malloc(128);

    if(p != NULL){
        free(p);
    }
}
Bon réflexe :

Code: Select all

void handler(int sig)
{
    flag = 1;
}
Puis le programme principal fait le vrai travail plus tard.

8. La bonne mentalité : le handler met un flag

Le modèle propre le plus courant :
  • le handler ne fait presque rien ;
  • il met à jour un flag global ;
  • la boucle principale voit le flag ;
  • la boucle principale fait le vrai traitement.
Exemple :

Code: Select all

#include <signal.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>

static volatile sig_atomic_t got_sigint = 0;

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

int main(void)
{
    struct sigaction sa;

    memset(&sa, 0, sizeof(sa));

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

    if(sigaction(SIGINT, &sa, NULL) == -1){
        perror("sigaction");
        exit(EXIT_FAILURE);
    }

    while(!got_sigint){
        pause();
    }

    printf("SIGINT recu, fin propre du programme\n");

    return 0;
}
Ici, le handler ne fait qu'une chose :

Code: Select all

got_sigint = 1;
C'est simple, lisible et beaucoup plus sûr.

9. sig_atomic_t : variable adaptée aux handlers

Le type sig_atomic_t est prévu pour les communications simples entre un signal handler et le programme principal.

Déclaration classique :

Code: Select all

static volatile sig_atomic_t flag = 0;
Pourquoi sig_atomic_t ?

Parce que POSIX/C garantit qu'une lecture ou écriture simple de ce type peut se faire sans être coupée au milieu par un signal.

Pourquoi volatile ?

Parce que la variable peut changer de manière imprévisible pour le compilateur, depuis un handler.

Sans volatile, le compilateur pourrait optimiser la boucle comme si la variable ne changeait jamais.

Exemple :

Code: Select all

while(flag == 0){
    /* attendre */
}
Le compilateur ne voit pas forcément que flag peut être modifiée par un signal handler. volatile l'aide à ne pas faire une optimisation dangereuse.

Important :
sig_atomic_t garantit une lecture/ecriture simple. Cela ne veut pas dire que toutes les opérations complexes sont atomiques.
Par exemple, ceci n'est pas forcément sûr comme compteur parfait :

Code: Select all

flag++;
Car ++ peut être composé de plusieurs opérations : lire, ajouter, écrire.

Pour un handler, préfère :

Code: Select all

flag = 1;
10. errno dans un signal handler

errno est une variable utilisée pour indiquer la cause d'une erreur après certains appels système ou fonctions libc.

Problème : un signal handler peut appeler une fonction qui modifie errno.

Donc le programme principal peut être perturbé.

Exemple mental :

Code: Select all

read() echoue et place errno
        ↓
avant que le programme lise errno, un signal arrive
        ↓
le handler appelle une fonction qui modifie errno
        ↓
le programme principal lit un errno different
Bonne pratique dans un handler : sauvegarder et restaurer errno.

Exemple :

Code: Select all

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

void handler(int sig)
{
    int saved_errno;
    const char msg[] = "signal recu\n";

    saved_errno = errno;

    write(STDOUT_FILENO, msg, sizeof(msg) - 1);

    errno = saved_errno;
}
Ce réflexe est important dans du code système propre.

11. Terminer un programme depuis un handler

Dans un handler, il ne faut pas utiliser n'importe quelle fonction pour quitter.

À éviter :

Code: Select all

exit(1);
Pourquoi ?

Parce que exit() fait du nettoyage libc :
  • flush des buffers stdio ;
  • appel des fonctions enregistrées avec atexit() ;
  • nettoyage de structures internes.
Tout ça peut être dangereux dans un handler.

Plus adapté :

Code: Select all

_exit(1);
_exit() termine directement le processus sans passer par le nettoyage classique de la libc.

Exemple :

Code: Select all

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

void handler(int sig)
{
    const char msg[] = "arret immediat\n";

    write(STDERR_FILENO, msg, sizeof(msg) - 1);
    _exit(1);
}

int main(void)
{
    struct sigaction sa;

    memset(&sa, 0, sizeof(sa));

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

    sigaction(SIGINT, &sa, NULL);

    while(1){
        pause();
    }

    return 0;
}
Autres possibilités vues dans le chapitre :

Code: Select all

_exit(status)
kill(getpid(), signal)
abort()
abort() termine anormalement le processus, souvent avec génération d'un core dump.

12. setjmp(), longjmp(), sigsetjmp() et siglongjmp()

Le chapitre parle aussi d'une technique plus avancée : sortir d'un signal handler avec un saut non local.

Fonctions classiques :

Code: Select all

setjmp()
longjmp()
Versions adaptées aux signaux :

Code: Select all

sigsetjmp()
siglongjmp()
L'idée :
  • le programme enregistre un point de retour avec sigsetjmp() ;
  • le handler peut revenir à ce point avec siglongjmp() ;
  • on évite de retourner normalement du handler.
Exemple simplifié :

Code: Select all

#include <signal.h>
#include <setjmp.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>

static sigjmp_buf env;

void handler(int sig)
{
    siglongjmp(env, 1);
}

int main(void)
{
    struct sigaction sa;

    memset(&sa, 0, sizeof(sa));
    sa.sa_handler = handler;
    sigemptyset(&sa.sa_mask);

    sigaction(SIGINT, &sa, NULL);

    if(sigsetjmp(env, 1) == 0){
        printf("point initial enregistre\n");
    }
    else{
        printf("retour depuis le handler avec siglongjmp\n");
    }

    while(1){
        pause();
    }

    return 0;
}
Attention : cette technique est puissante mais dangereuse.

Pourquoi ?

Parce que le signal peut interrompre ton programme pendant qu'une fonction non réentrante est en train de modifier une structure interne. Si tu fais un siglongjmp(), tu abandonnes cette fonction au milieu de son exécution.

Donc à utiliser seulement quand tu comprends très bien ce que tu fais.

13. sigaltstack() : exécuter un handler sur une stack alternative

Normalement, un signal handler utilise la stack normale du thread.

Mais si ton programme provoque un débordement de stack, la stack normale peut être inutilisable.

Exemple :

Code: Select all

fonction recursive infinie
        ↓
stack overflow
        ↓
SIGSEGV
        ↓
le handler SIGSEGV veut s'executer
        ↓
mais la stack normale est deja morte
Solution : utiliser une stack alternative avec sigaltstack().

Prototype :

Code: Select all

#include <signal.h>

int sigaltstack(const stack_t *ss, stack_t *old_ss);
Structure :

Code: Select all

typedef struct {
    void  *ss_sp;
    int    ss_flags;
    size_t ss_size;
} stack_t;
Champs importants :

Code: Select all

ss_sp      adresse de la stack alternative
ss_size    taille de la stack alternative
ss_flags   flags, souvent 0 lors de l'installation
Pour dire à un handler d'utiliser cette stack, on met le flag :

Code: Select all

SA_ONSTACK
14. Exemple avec sigaltstack() et SA_ONSTACK

Exemple pédagogique :

Code: Select all

#define _GNU_SOURCE
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>

static void handler(int sig)
{
    const char msg[] = "SIGSEGV sur stack alternative\n";

    write(STDERR_FILENO, msg, sizeof(msg) - 1);
    _exit(1);
}

int main(void)
{
    stack_t ss;
    struct sigaction sa;

    ss.ss_sp = malloc(SIGSTKSZ);
    if(ss.ss_sp == NULL){
        perror("malloc");
        return 1;
    }

    ss.ss_size = SIGSTKSZ;
    ss.ss_flags = 0;

    if(sigaltstack(&ss, NULL) == -1){
        perror("sigaltstack");
        return 1;
    }

    memset(&sa, 0, sizeof(sa));

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

    if(sigaction(SIGSEGV, &sa, NULL) == -1){
        perror("sigaction");
        return 1;
    }

    raise(SIGSEGV);

    return 0;
}
Ce programme installe une stack alternative, puis demande à SIGSEGV de l'utiliser.

Utilité réelle :
  • crash handler ;
  • runtime de langage ;
  • debugger ;
  • système de diagnostic ;
  • gestion plus propre d'un stack overflow.
15. SA_SIGINFO : handler avancé avec informations détaillées

Par défaut, un handler reçoit seulement le numéro du signal :

Code: Select all

void handler(int sig)
{
}
Avec SA_SIGINFO, le handler reçoit trois arguments :

Code: Select all

void handler(int sig, siginfo_t *info, void *ucontext)
{
}
Pour l'installer, il faut utiliser sa_sigaction au lieu de sa_handler.

Exemple :

Code: Select all

struct sigaction sa;

memset(&sa, 0, sizeof(sa));

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

sigaction(SIGSEGV, &sa, NULL);
Attention :

Code: Select all

sa.sa_handler     -> handler classique avec 1 argument
sa.sa_sigaction   -> handler avance avec 3 arguments
16. siginfo_t : les champs importants

siginfo_t contient des informations sur le signal reçu.

Champs très utiles :

Code: Select all

si_signo     numero du signal
si_code      cause precise du signal
si_pid       PID de l'emetteur, selon le cas
si_uid       UID de l'emetteur, selon le cas
si_addr      adresse fautive pour SIGSEGV/SIGBUS/SIGILL/SIGFPE
si_status    statut d'un processus fils pour SIGCHLD
si_value     valeur envoyee avec sigqueue()
Exemple mental :

Code: Select all

SIGSEGV arrive
        ↓
info->si_signo = SIGSEGV
info->si_addr  = adresse qui a cause la faute
info->si_code  = SEGV_MAPERR ou SEGV_ACCERR
C'est très utile pour faire des messages de crash plus précis.

17. si_code : comprendre l'origine exacte du signal

Le champ si_code indique la cause du signal.

Exemples généraux :

Code: Select all

SI_USER       signal envoye par kill()
SI_QUEUE      signal envoye par sigqueue()
SI_TIMER      signal genere par un timer POSIX
SI_ASYNCIO    signal genere par une completion AIO
SI_KERNEL     signal genere par le noyau
Pour SIGSEGV :

Code: Select all

SEGV_MAPERR   adresse non mappee
SEGV_ACCERR   permission invalide sur une adresse mappee
Exemple :

Code: Select all

int *p = NULL;
*p = 123;
Cela peut produire SEGV_MAPERR, car l'adresse NULL n'est pas mappée.

Autre exemple :

Code: Select all

char *s = "hello";
s[0] = 'H';
Cela peut produire SEGV_ACCERR, car la zone est souvent en lecture seule.

18. Exemple complet avec SA_SIGINFO pour SIGSEGV

Exemple pédagogique :

Code: Select all

#define _GNU_SOURCE
#include <signal.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <stdio.h>

static void handler(int sig, siginfo_t *info, void *context)
{
    const char msg[] = "SIGSEGV recu\n";

    write(STDERR_FILENO, msg, sizeof(msg) - 1);

    /* Pour un vrai programme robuste, eviter printf ici. */
    _exit(1);
}

int main(void)
{
    struct sigaction sa;

    memset(&sa, 0, sizeof(sa));

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

    if(sigaction(SIGSEGV, &sa, NULL) == -1){
        perror("sigaction");
        return 1;
    }

    int *p = NULL;
    *p = 123;

    return 0;
}
Ce handler reçoit siginfo_t, donc il pourrait lire info->si_addr pour connaître l'adresse fautive.

19. Afficher l'adresse fautive d'un SIGSEGV

Dans un exemple de debug, on peut afficher info->si_addr.

Attention : printf() n'est pas async-signal-safe, donc ce code est pédagogique, pas idéal pour un vrai crash handler robuste.

Code: Select all

#define _GNU_SOURCE
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

static void handler(int sig, siginfo_t *info, void *context)
{
    printf("Signal: %d\n", sig);
    printf("Adresse fautive: %p\n", info->si_addr);
    printf("si_code: %d\n", info->si_code);

    _exit(1);
}

int main(void)
{
    struct sigaction sa;

    memset(&sa, 0, sizeof(sa));

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

    sigaction(SIGSEGV, &sa, NULL);

    int *p = NULL;
    *p = 42;

    return 0;
}
Version mentale :

Code: Select all

si_addr = adresse qui a provoque la faute
si_code = type exact de faute
20. ucontext_t : récupérer le contexte CPU

Le troisième argument du handler avancé est :

Code: Select all

void *ucontext
Sur Linux, on peut souvent le caster en :

Code: Select all

ucontext_t *ctx = (ucontext_t *)ucontext;
Ce contexte peut contenir :
  • les registres CPU ;
  • l'instruction pointer ;
  • le stack pointer ;
  • le signal mask ;
  • l'état du thread au moment du signal.
Sur x86_64 Linux, on peut parfois lire RIP comme ceci :

Code: Select all

#define _GNU_SOURCE
#include <signal.h>
#include <ucontext.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>

static void handler(int sig, siginfo_t *info, void *context)
{
#if defined(__x86_64__)
    ucontext_t *ctx = (ucontext_t *)context;
    void *rip = (void *)ctx->uc_mcontext.gregs[REG_RIP];

    printf("Adresse fautive: %p\n", info->si_addr);
    printf("RIP: %p\n", rip);
#endif

    _exit(1);
}

int main(void)
{
    struct sigaction sa;

    memset(&sa, 0, sizeof(sa));

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

    sigaction(SIGSEGV, &sa, NULL);

    int *p = NULL;
    *p = 1;

    return 0;
}
Utilisation réelle :
  • debugger ;
  • crash reporter ;
  • runtime de langage ;
  • sandbox ;
  • analyse d'exceptions mémoire.
21. SIGCHLD et siginfo_t

SIGCHLD est envoyé à un processus parent quand un processus fils change d'état.

Avec siginfo_t, on peut savoir ce qui est arrivé au fils.

Valeurs importantes de si_code pour SIGCHLD :

Code: Select all

CLD_EXITED      le fils s'est termine normalement
CLD_KILLED      le fils a ete tue par un signal
CLD_DUMPED      le fils a termine avec core dump
CLD_STOPPED     le fils a ete stoppe
CLD_CONTINUED   le fils a repris son execution
Champs utiles :

Code: Select all

info->si_pid       PID du fils
info->si_status    code de sortie ou signal responsable
info->si_code      type d'evenement
Exemple mental :

Code: Select all

fils termine avec exit(7)
        ↓
parent recoit SIGCHLD
        ↓
si_code   = CLD_EXITED
si_status = 7
22. SIGFPE, SIGILL, SIGBUS et autres causes utiles

siginfo_t est aussi utile pour d'autres signaux de crash.

Pour SIGFPE, on peut avoir par exemple :

Code: Select all

FPE_INTDIV   division entiere par zero
FPE_INTOVF   overflow entier
FPE_FLTDIV   division flottante par zero
FPE_FLTOVF   overflow flottant
FPE_FLTUND   underflow flottant
Pour SIGILL :

Code: Select all

ILL_ILLOPC   opcode illegal
ILL_ILLOPN   operande illegal
ILL_ILLADR   mode d'adressage illegal
ILL_PRVOPC   opcode privilegie
ILL_PRVREG   registre privilegie
Pour SIGBUS :

Code: Select all

BUS_ADRALN   alignement invalide
BUS_ADRERR   adresse physique inexistante
BUS_OBJERR   erreur specifique a un objet
Ces valeurs servent surtout dans les crash handlers et les outils bas niveau.

23. sigqueue() et si_value

Avec kill(), on envoie seulement un signal.

Avec sigqueue(), on peut envoyer un signal avec une petite valeur attachée.

Prototype :

Code: Select all

#include <signal.h>

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

Code: Select all

union sigval {
    int   sival_int;
    void *sival_ptr;
};
Côté réception, avec SA_SIGINFO, on lit :

Code: Select all

info->si_value.sival_int
info->si_value.sival_ptr
Exemple d'envoi :

Code: Select all

union sigval val;

val.sival_int = 1234;

sigqueue(pid, SIGUSR1, val);
Exemple de handler :

Code: Select all

void handler(int sig, siginfo_t *info, void *context)
{
    int value;

    value = info->si_value.sival_int;
}
Attention : même si c'est pratique, il faut rester prudent avec les signaux comme mécanisme de communication.

24. Les appels système peuvent être interrompus par un signal

Point fondamental du chapitre : un signal peut interrompre un appel système bloquant.

Exemple :

Code: Select all

read(fd, buffer, sizeof(buffer));
Si read() attend des données et qu'un signal arrive, le handler peut s'exécuter. Ensuite, selon les cas, read() peut échouer avec :

Code: Select all

-1
errno = EINTR
EINTR signifie :
L'appel système a été interrompu par un signal avant de se terminer normalement.
Mauvais code :

Code: Select all

ssize_t n;

n = read(fd, buffer, sizeof(buffer));
if(n == -1){
    perror("read");
    return 1;
}
Pourquoi c'est fragile ?

Parce que read() peut échouer à cause d'un signal, pas à cause d'une vraie erreur grave.

25. Gérer correctement EINTR

La bonne logique consiste à recommencer l'appel si l'erreur est EINTR.

Exemple :

Code: Select all

#include <unistd.h>
#include <errno.h>

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;
}
Utilisation :

Code: Select all

char buffer[1024];
ssize_t n;

n = safe_read(STDIN_FILENO, buffer, sizeof(buffer));
if(n == -1){
    perror("read");
}
Schéma mental :

Code: Select all

read() attend
        ↓
signal arrive
        ↓
handler execute
        ↓
read() retourne -1 avec EINTR
        ↓
on recommence read()
26. Macro classique contre EINTR

On peut écrire une macro pour éviter de répéter la boucle.

Exemple :

Code: Select all

#define NO_EINTR(stmt) while((stmt) == -1 && errno == EINTR)
Utilisation :

Code: Select all

ssize_t cnt;

NO_EINTR(cnt = read(fd, buf, sizeof(buf)));

if(cnt == -1){
    perror("read");
}
Version plus lisible sans macro :

Code: Select all

do{
    cnt = read(fd, buf, sizeof(buf));
}while(cnt == -1 && errno == EINTR);
Pour apprendre, la version do/while est souvent plus claire.

27. SA_RESTART : relancer certains appels système automatiquement

Le flag SA_RESTART demande au noyau de relancer automatiquement certains appels système interrompus par un signal.

Exemple :

Code: Select all

sa.sa_flags = SA_RESTART;
Programme simple :

Code: Select all

#include <signal.h>
#include <unistd.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>

void handler(int sig)
{
    const char msg[] = "signal recu\n";
    write(STDOUT_FILENO, msg, sizeof(msg) - 1);
}

int main(void)
{
    struct sigaction sa;
    char buffer[128];
    ssize_t n;

    memset(&sa, 0, sizeof(sa));

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

    if(sigaction(SIGINT, &sa, NULL) == -1){
        perror("sigaction");
        return 1;
    }

    while(1){
        n = read(STDIN_FILENO, buffer, sizeof(buffer));
        if(n == -1){
            perror("read");
        }
    }

    return 0;
}
Avec SA_RESTART, certains read() bloquants peuvent reprendre automatiquement après le handler.

28. SA_RESTART ne marche pas pour tout

Gros piège : SA_RESTART ne garantit pas que tous les appels système seront relancés.

Le comportement dépend :
  • de l'appel système ;
  • du type de fichier ou périphérique ;
  • du système Unix/Linux ;
  • du handler ;
  • des options utilisées ;
  • du moment exact où l'interruption arrive.
Donc, même avec SA_RESTART, du code robuste doit souvent gérer EINTR.

Bonne règle :
SA_RESTART aide, mais ne remplace pas toujours la gestion de EINTR.
Exemples d'appels où EINTR est très important à connaître :

Code: Select all

read()
write()
wait()
waitpid()
accept()
connect()
select()
poll()
nanosleep()
29. Exemple complet : read() robuste contre EINTR

Code: Select all

#include <signal.h>
#include <unistd.h>
#include <string.h>
#include <stdio.h>
#include <errno.h>
#include <stdlib.h>

static volatile sig_atomic_t got_sigint = 0;

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

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

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

    return ret;
}

int main(void)
{
    struct sigaction sa;
    char buffer[128];
    ssize_t n;

    memset(&sa, 0, sizeof(sa));

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

    if(sigaction(SIGINT, &sa, NULL) == -1){
        perror("sigaction");
        return 1;
    }

    printf("Tape du texte. Ctrl+C change le flag.\n");

    while(!got_sigint){
        n = read_no_eintr(STDIN_FILENO, buffer, sizeof(buffer));
        if(n == -1){
            perror("read");
            break;
        }

        if(n == 0){
            break;
        }

        write(STDOUT_FILENO, buffer, n);
    }

    printf("fin du programme\n");

    return 0;
}
Ce code montre une vraie habitude système : toujours réfléchir à EINTR quand un appel peut bloquer.

30. Ne pas confondre signal bloqué et handler en cours

Quand un handler est en cours, le signal qui l'a déclenché est généralement automatiquement bloqué pendant l'exécution du handler.

Exemple :

Code: Select all

SIGINT arrive
        ↓
handler SIGINT commence
        ↓
SIGINT est temporairement bloque pendant ce handler
        ↓
handler termine
        ↓
SIGINT peut etre debloque
Cela évite souvent que le même handler se réinterrompe lui-même en boucle.

Mais ce comportement peut être modifié avec :

Code: Select all

SA_NODEFER
31. SA_NODEFER : ne pas bloquer automatiquement le signal courant

Le flag SA_NODEFER empêche le noyau de bloquer automatiquement le signal courant pendant son handler.

Exemple :

Code: Select all

sa.sa_flags = SA_NODEFER;
Cela signifie que le même signal peut interrompre son propre handler.

C'est dangereux si on ne maîtrise pas le contexte.

Schéma :

Code: Select all

SIGINT arrive
        ↓
handler SIGINT commence
        ↓
SIGINT arrive encore
        ↓
le meme handler peut etre rappele avant la fin du premier
À retenir :
SA_NODEFER est un flag avancé. Au début, il vaut mieux l'éviter.
32. SA_RESETHAND : handler utilisable une seule fois

Le flag SA_RESETHAND remet le comportement du signal à sa valeur par défaut après la première livraison.

Exemple :

Code: Select all

sa.sa_flags = SA_RESETHAND;
Mentalement :

Code: Select all

premier SIGINT  -> handler appele
ensuite SIGINT  -> comportement par defaut
Cela peut servir pour des comportements temporaires.

Mais dans la majorité des programmes simples, on ne l'utilise pas.

33. sa_mask : bloquer d'autres signaux pendant un handler

Le champ sa_mask de struct sigaction permet de bloquer certains signaux pendant l'exécution du handler.

Exemple :

Code: Select all

struct sigaction sa;

memset(&sa, 0, sizeof(sa));

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

sigaction(SIGINT, &sa, NULL);
Ici, quand le handler de SIGINT s'exécute, SIGTERM est temporairement bloqué.

Utilité : protéger une mini-section critique dans le handler.

Schéma :

Code: Select all

SIGINT arrive
        ↓
handler SIGINT commence
        ↓
SIGTERM est temporairement bloque
        ↓
handler termine
        ↓
ancien masque restaure
34. Exemple complet : SIGINT et SIGTERM propres

Code: Select all

#include <signal.h>
#include <unistd.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>

static volatile sig_atomic_t stop = 0;

void handler(int sig)
{
    if(sig == SIGINT){
        stop = 1;
    }
    else if(sig == SIGTERM){
        stop = 1;
    }
}

int main(void)
{
    struct sigaction sa;

    memset(&sa, 0, sizeof(sa));

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

    if(sigaction(SIGINT, &sa, NULL) == -1){
        perror("sigaction SIGINT");
        exit(EXIT_FAILURE);
    }

    if(sigaction(SIGTERM, &sa, NULL) == -1){
        perror("sigaction SIGTERM");
        exit(EXIT_FAILURE);
    }

    printf("programme lance. Ctrl+C ou SIGTERM pour arreter.\n");

    while(!stop){
        pause();
    }

    printf("arret propre demande\n");

    return 0;
}
Ce style est très fréquent : handler minimal + boucle principale responsable de l'arrêt propre.

35. Différence entre retourner du handler et terminer depuis le handler

Quand un handler se termine normalement, le programme reprend là où il avait été interrompu.

Schéma :

Code: Select all

code principal
        ↓
signal
        ↓
handler
        ↓
retour
        ↓
code principal reprend
Mais parfois, retourner n'est pas souhaité.

Exemples :
  • crash mémoire grave ;
  • état du programme corrompu ;
  • erreur fatale ;
  • handler SIGSEGV ;
  • besoin de quitter immédiatement.
Dans ce cas, on peut utiliser :

Code: Select all

_exit()
abort()
siglongjmp()
Mais chaque méthode a des conséquences.

Résumé :

Code: Select all

return du handler   -> reprise du programme interrompu
_exit()             -> terminaison immediate
abort()             -> terminaison anormale, souvent core dump
siglongjmp()        -> saut non local vers un point sauve
36. Exemple : mini crash handler pédagogique

Exemple simple avec SA_SIGINFO :

Code: Select all

#define _GNU_SOURCE
#include <signal.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>

static void crash_handler(int sig, siginfo_t *info, void *context)
{
    const char msg[] = "crash detecte\n";

    write(STDERR_FILENO, msg, sizeof(msg) - 1);
    _exit(128 + sig);
}

int main(void)
{
    struct sigaction sa;

    memset(&sa, 0, sizeof(sa));

    sa.sa_sigaction = crash_handler;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = SA_SIGINFO | SA_ONSTACK;

    sigaction(SIGSEGV, &sa, NULL);
    sigaction(SIGBUS, &sa, NULL);
    sigaction(SIGILL, &sa, NULL);
    sigaction(SIGFPE, &sa, NULL);

    int *p = NULL;
    *p = 1;

    return 0;
}
Ce n'est pas un crash reporter complet, mais ça montre la logique :
  • installer un handler avancé ;
  • utiliser SA_SIGINFO ;
  • utiliser éventuellement SA_ONSTACK ;
  • éviter le gros traitement dans le handler ;
  • terminer proprement avec _exit().
37. Comparaison avec Windows

Pour quelqu'un qui connaît Windows, on peut faire des rapprochements mentaux.

Code: Select all

Linux signal handler              Windows proche mentalement
-----------------------------------------------------------------------
SIGSEGV handler                   SEH / vectored exception handler
SIGINT handler                    console control handler
siginfo_t->si_addr                adresse fautive d'une Access Violation
ucontext_t                        contexte CPU / CONTEXT Windows
sigaltstack                       stack separee pour handler critique
SA_RESTART + EINTR                gestion des appels bloques interrompus
_exit()                           terminaison directe style ExitProcess bas niveau
Mais attention : ce ne sont pas des équivalents exacts.

Différence importante :
Sous Linux, les signaux sont un mécanisme Unix historique de notification asynchrone. Sous Windows, les exceptions, APC, events, console handlers et objets kernel sont des mécanismes séparés.
Pour SIGSEGV, l'analogie la plus proche est l'exception Access Violation.

38. Erreurs classiques à éviter

Erreur 1 : utiliser printf() dans un handler de production.

Code: Select all

void handler(int sig)
{
    printf("signal\n");
}
Erreur 2 : appeler malloc() ou free() dans un handler.

Code: Select all

void handler(int sig)
{
    char *p = malloc(100);
    free(p);
}
Erreur 3 : oublier que read() peut retourner -1 avec errno == EINTR.

Code: Select all

if(read(fd, buf, size) == -1){
    perror("read");
}
Erreur 4 : croire que les signaux standards sont comptés un par un.

Code: Select all

100 SIGUSR1 envoyes = 100 handlers appeles
Ce n'est pas garanti pour les signaux standards.

Erreur 5 : faire trop de travail dans le handler.

Mauvais réflexe :

Code: Select all

void handler(int sig)
{
    sauvegarder_fichier();
    fermer_socket();
    printf("fin\n");
    free(global_ptr);
    exit(0);
}
Meilleur réflexe :

Code: Select all

void handler(int sig)
{
    stop = 1;
}
39. Résumé API à retenir

Headers principaux :

Code: Select all

#include <signal.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <stdlib.h>
#include <setjmp.h>
#include <ucontext.h>
API importantes :

Code: Select all

sigaction()       installer un handler propre
sigemptyset()     initialiser un set vide
sigaddset()       ajouter un signal dans un set
sigprocmask()     modifier le masque de signaux
sigpending()      voir les signaux pending
sigaltstack()     installer une stack alternative
sigsetjmp()       sauvegarder un point de retour avec masque signal optionnel
siglongjmp()      revenir a ce point depuis un handler
write()           ecriture simple async-signal-safe
_exit()           terminaison directe adaptee aux handlers
kill()            envoyer un signal
raise()           s'envoyer un signal
sigqueue()        envoyer un signal avec une valeur
read()            peut etre interrompu avec EINTR
waitpid()         peut etre interrompu avec EINTR
Structures et types :

Code: Select all

struct sigaction
sigset_t
siginfo_t
stack_t
ucontext_t
sig_atomic_t
sigjmp_buf
Flags importants de sigaction :

Code: Select all

SA_RESTART     redemarre certains appels systeme interrompus
SA_SIGINFO     active le handler avance a 3 arguments
SA_ONSTACK     execute le handler sur la stack alternative
SA_NODEFER     ne bloque pas automatiquement le signal courant
SA_RESETHAND   remet l'action par defaut apres la premiere livraison
40. Résumé mental ultra important

À retenir par cœur :
  • Un handler peut interrompre ton programme à n'importe quel moment.
  • Dans un handler, il faut faire le minimum.
  • printf(), malloc(), free() et exit() sont à éviter dans un handler.
  • Pour communiquer avec le programme principal, utilise un volatile sig_atomic_t.
  • Sauvegarde/restaure errno si ton handler appelle une fonction qui peut le modifier.
  • SA_SIGINFO permet d'obtenir siginfo_t.
  • siginfo_t->si_addr est très utile pour SIGSEGV.
  • siginfo_t->si_code donne la cause précise du signal.
  • ucontext_t peut donner l'état CPU au moment du signal.
  • sigaltstack() permet d'avoir une stack spéciale pour certains handlers critiques.
  • Un appel système bloquant peut échouer avec errno == EINTR.
  • SA_RESTART aide, mais il faut quand même savoir gérer EINTR.
41. Mini glossaire

Signal handler

Fonction appelée quand un signal est livré.

Réentrant

Se dit d'une fonction qui peut être interrompue puis rappelée sans casser son état interne.

Async-signal-safe

Se dit d'une fonction officiellement sûre à appeler dans un signal handler.

sig_atomic_t

Type adapté aux lectures/écritures simples entre programme principal et signal handler.

errno

Variable contenant le code d'erreur de certains appels système ou fonctions libc.

_exit()

Terminaison directe du processus, plus adaptée qu'exit() dans un handler.

sigsetjmp() / siglongjmp()

Mécanisme de saut non local compatible avec la gestion du masque de signaux.

sigaltstack()

API permettant d'installer une stack alternative pour les handlers.

SA_ONSTACK

Flag indiquant qu'un handler doit utiliser la stack alternative.

SA_SIGINFO

Flag qui active un handler avancé recevant siginfo_t et ucontext.

siginfo_t

Structure contenant des informations détaillées sur l'origine et la cause d'un signal.

si_code

Champ de siginfo_t indiquant la cause précise du signal.

si_addr

Adresse fautive pour certains signaux comme SIGSEGV.

ucontext_t

Structure contenant le contexte d'exécution du thread au moment du signal.

EINTR

Erreur indiquant qu'un appel système a été interrompu par un signal.

SA_RESTART

Flag demandant au système de redémarrer automatiquement certains appels système interrompus.

42. Conclusion

Ce chapitre est une grosse étape en programmation système Linux. Il montre que les signaux ne sont pas juste des fonctions appelées quand on appuie sur Ctrl+C.

Un signal est une interruption logicielle côté user mode. Il peut arriver pendant que ton programme est dans un état fragile. Donc écrire un bon handler demande de respecter des règles strictes.

La mentalité professionnelle est :
  • installer les handlers avec sigaction() ;
  • garder les handlers très courts ;
  • éviter les fonctions non async-signal-safe ;
  • utiliser volatile sig_atomic_t pour signaler un événement ;
  • gérer errno correctement ;
  • utiliser SA_SIGINFO quand on veut diagnostiquer l'origine d'un signal ;
  • utiliser sigaltstack() pour les handlers critiques ;
  • gérer EINTR dans le code qui fait des appels système bloquants.
Phrase finale à retenir :
Un signal handler doit être traité comme du code exécuté en urgence dans un contexte instable : il doit être court, sûr, minimal, et laisser le vrai travail au programme principal.

Who is online

Users browsing this forum: No registered users and 1 guest