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 :
Dans le chapitre précédent, on voyait surtout les bases : signal, sigaction(), sigset_t, signal mask, pending signals.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 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.
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;
}
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
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
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()
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
- 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.
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 :
Il ne retient pas forcément le nombre exact d'arrivées.Ce signal est en attente.
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
Mauvaise idée :
Code: Select all
1 signal = 1 evenement exact
Code: Select all
1 signal = une notification indiquant qu'au moins un evenement est arrive
- 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.
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
Exemple de fonction naturellement plus sûre :
Code: Select all
int add(int a, int b)
{
return a + b;
}
Exemple de fonction potentiellement non réentrante :
Code: Select all
char *get_buffer(void)
{
static char buffer[128];
/* modification du buffer statique */
return buffer;
}
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
Dans un handler, la règle pratique est :
Fonctions souvent acceptées dans un handler :Appelle seulement des fonctions async-signal-safe, ou fais presque rien.
Code: Select all
write()
_exit()
sigaction()
sigprocmask()
kill()
getpid()
Code: Select all
printf()
fprintf()
sprintf()
malloc()
free()
exit()
pthread_mutex_lock()
fonctions stdio en general
fonctions complexes de la libc
Beaucoup de débutants font ça :
Code: Select all
void handler(int sig)
{
printf("signal recu\n");
}
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
Code: Select all
#include <unistd.h>
void handler(int sig)
{
const char msg[] = "signal recu\n";
write(STDOUT_FILENO, msg, sizeof(msg) - 1);
}
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.
- corruption mémoire ;
- deadlock ;
- comportement indéfini ;
- crash difficile à comprendre.
Code: Select all
void handler(int sig)
{
char *p = malloc(128);
if(p != NULL){
free(p);
}
}
Code: Select all
void handler(int sig)
{
flag = 1;
}
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.
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;
}
Code: Select all
got_sigint = 1;
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;
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 */
}
Important :
Par exemple, ceci n'est pas forcément sûr comme compteur parfait :sig_atomic_t garantit une lecture/ecriture simple. Cela ne veut pas dire que toutes les opérations complexes sont atomiques.
Code: Select all
flag++;
Pour un handler, préfère :
Code: Select all
flag = 1;
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
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;
}
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);
Parce que exit() fait du nettoyage libc :
- flush des buffers stdio ;
- appel des fonctions enregistrées avec atexit() ;
- nettoyage de structures internes.
Plus adapté :
Code: Select all
_exit(1);
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;
}
Code: Select all
_exit(status)
kill(getpid(), signal)
abort()
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()
Code: Select all
sigsetjmp()
siglongjmp()
- le programme enregistre un point de retour avec sigsetjmp() ;
- le handler peut revenir à ce point avec siglongjmp() ;
- on évite de retourner normalement du handler.
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;
}
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
Prototype :
Code: Select all
#include <signal.h>
int sigaltstack(const stack_t *ss, stack_t *old_ss);
Code: Select all
typedef struct {
void *ss_sp;
int ss_flags;
size_t ss_size;
} stack_t;
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
Code: Select all
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;
}
Utilité réelle :
- crash handler ;
- runtime de langage ;
- debugger ;
- système de diagnostic ;
- gestion plus propre d'un stack overflow.
Par défaut, un handler reçoit seulement le numéro du signal :
Code: Select all
void handler(int sig)
{
}
Code: Select all
void handler(int sig, siginfo_t *info, void *ucontext)
{
}
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);
Code: Select all
sa.sa_handler -> handler classique avec 1 argument
sa.sa_sigaction -> handler avance avec 3 arguments
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()
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
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
Code: Select all
SEGV_MAPERR adresse non mappee
SEGV_ACCERR permission invalide sur une adresse mappee
Code: Select all
int *p = NULL;
*p = 123;
Autre exemple :
Code: Select all
char *s = "hello";
s[0] = 'H';
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;
}
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;
}
Code: Select all
si_addr = adresse qui a provoque la faute
si_code = type exact de faute
Le troisième argument du handler avancé est :
Code: Select all
void *ucontext
Code: Select all
ucontext_t *ctx = (ucontext_t *)ucontext;
- les registres CPU ;
- l'instruction pointer ;
- le stack pointer ;
- le signal mask ;
- l'état du thread au moment du signal.
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;
}
- debugger ;
- crash reporter ;
- runtime de langage ;
- sandbox ;
- analyse d'exceptions mémoire.
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
Code: Select all
info->si_pid PID du fils
info->si_status code de sortie ou signal responsable
info->si_code type d'evenement
Code: Select all
fils termine avec exit(7)
↓
parent recoit SIGCHLD
↓
si_code = CLD_EXITED
si_status = 7
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
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
Code: Select all
BUS_ADRALN alignement invalide
BUS_ADRERR adresse physique inexistante
BUS_OBJERR erreur specifique a un objet
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);
Code: Select all
union sigval {
int sival_int;
void *sival_ptr;
};
Code: Select all
info->si_value.sival_int
info->si_value.sival_ptr
Code: Select all
union sigval val;
val.sival_int = 1234;
sigqueue(pid, SIGUSR1, val);
Code: Select all
void handler(int sig, siginfo_t *info, void *context)
{
int value;
value = info->si_value.sival_int;
}
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));
Code: Select all
-1
errno = EINTR
Mauvais code :L'appel système a été interrompu par un signal avant de se terminer normalement.
Code: Select all
ssize_t n;
n = read(fd, buffer, sizeof(buffer));
if(n == -1){
perror("read");
return 1;
}
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;
}
Code: Select all
char buffer[1024];
ssize_t n;
n = safe_read(STDIN_FILENO, buffer, sizeof(buffer));
if(n == -1){
perror("read");
}
Code: Select all
read() attend
↓
signal arrive
↓
handler execute
↓
read() retourne -1 avec EINTR
↓
on recommence read()
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)
Code: Select all
ssize_t cnt;
NO_EINTR(cnt = read(fd, buf, sizeof(buf)));
if(cnt == -1){
perror("read");
}
Code: Select all
do{
cnt = read(fd, buf, sizeof(buf));
}while(cnt == -1 && errno == EINTR);
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;
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;
}
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.
Bonne règle :
Exemples d'appels où EINTR est très important à connaître :SA_RESTART aide, mais ne remplace pas toujours la gestion de EINTR.
Code: Select all
read()
write()
wait()
waitpid()
accept()
connect()
select()
poll()
nanosleep()
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;
}
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
Mais ce comportement peut être modifié avec :
Code: Select all
SA_NODEFER
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;
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
32. SA_RESETHAND : handler utilisable une seule foisSA_NODEFER est un flag avancé. Au début, il vaut mieux l'éviter.
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;
Code: Select all
premier SIGINT -> handler appele
ensuite SIGINT -> comportement par defaut
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);
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
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;
}
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
Exemples :
- crash mémoire grave ;
- état du programme corrompu ;
- erreur fatale ;
- handler SIGSEGV ;
- besoin de quitter immédiatement.
Code: Select all
_exit()
abort()
siglongjmp()
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
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;
}
- installer un handler avancé ;
- utiliser SA_SIGINFO ;
- utiliser éventuellement SA_ONSTACK ;
- éviter le gros traitement dans le handler ;
- terminer proprement avec _exit().
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
Différence importante :
Pour SIGSEGV, l'analogie la plus proche est l'exception Access Violation.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.
38. Erreurs classiques à éviter
Erreur 1 : utiliser printf() dans un handler de production.
Code: Select all
void handler(int sig)
{
printf("signal\n");
}
Code: Select all
void handler(int sig)
{
char *p = malloc(100);
free(p);
}
Code: Select all
if(read(fd, buf, size) == -1){
perror("read");
}
Code: Select all
100 SIGUSR1 envoyes = 100 handlers appeles
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);
}
Code: Select all
void handler(int sig)
{
stop = 1;
}
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>
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
Code: Select all
struct sigaction
sigset_t
siginfo_t
stack_t
ucontext_t
sig_atomic_t
sigjmp_buf
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
À 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.
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.
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.
