Objectif du cours
Ce cours explique les signaux Linux en programmation système C : ce qu'est un signal, à quoi il sert, comment le recevoir, comment l'envoyer, comment le bloquer, comment installer un gestionnaire propre avec sigaction(), et comment comprendre les notions de signal mask, pending signals et signal sets.
L'idée principale à retenir est simple :
Par exemple :Un signal Linux est une notification logicielle envoyée par le noyau à un processus ou à un thread pour l'avertir qu'un événement s'est produit ou pour modifier son exécution.
- Ctrl+C envoie généralement SIGINT au programme au premier plan.
- kill PID envoie par défaut SIGTERM à un processus.
- kill -9 PID envoie SIGKILL, qui tue le processus immédiatement.
- Une faute mémoire provoque souvent SIGSEGV.
- Un timer expiré peut provoquer SIGALRM.
- La fin d'un processus fils provoque SIGCHLD chez le parent.
Un signal est une sorte d'alerte envoyée à un processus ou à un thread.
Il sert à dire :
Cette réaction peut être :Un événement vient d'arriver, il faut réagir.
- terminer le processus ;
- terminer le processus avec un core dump ;
- ignorer le signal ;
- stopper temporairement le processus ;
- reprendre un processus stoppé ;
- exécuter une fonction spéciale appelée signal handler.
Comparaison mentale :
Code: Select all
Windows IRP : requête I/O riche destinée à un driver.
Linux signal : notification asynchrone destinée à un processus ou un thread.
- le noyau ;
- un autre processus ;
- le processus lui-même ;
- le terminal ;
- une erreur matérielle ou mémoire ;
- un timer ;
- un événement lié à un processus fils.
Voici les signaux les plus importants à connaître au début.
Code: Select all
SIGINT interruption clavier, souvent Ctrl+C
SIGTERM demande propre de terminaison
SIGKILL terminaison forcée, impossible à intercepter
SIGSTOP suspension forcée, impossible à intercepter
SIGCONT reprise d'un processus stoppé
SIGSEGV accès mémoire invalide
SIGBUS erreur mémoire ou bus
SIGILL instruction illégale
SIGFPE erreur arithmétique
SIGPIPE écriture dans un pipe ou socket fermé
SIGCHLD un processus fils a changé d'état
SIGHUP terminal déconnecté, souvent utilisé pour recharger une configuration
SIGQUIT quit clavier, souvent Ctrl+\
SIGALRM expiration d'un timer alarm()
SIGUSR1 signal utilisateur libre
SIGUSR2 signal utilisateur libre
Code: Select all
SIGKILL
SIGSTOP
- capturés ;
- ignorés ;
- bloqués.
3. Actions par défaut des signaux
Quand un signal est délivré à un processus, le noyau applique une action.
Cette action peut être l'action par défaut ou une action définie par le programme.
Les grandes catégories d'actions par défaut sont :
- Term : terminer le processus.
- Core : terminer le processus et générer un core dump.
- Ign : ignorer le signal.
- Stop : stopper temporairement le processus.
- Cont : reprendre un processus stoppé.
Code: Select all
SIGTERM -> termine le processus par défaut
SIGINT -> termine le processus par défaut
SIGSEGV -> termine avec core dump par défaut
SIGCHLD -> souvent ignoré par défaut
SIGSTOP -> stoppe le processus
SIGCONT -> reprend le processus
Un signal handler est une fonction que ton programme installe pour réagir à un signal.
Exemple simple :
Code: Select all
void handler(int sig)
{
write(1, "signal recu\n", 12);
}
Mentalement :
Code: Select all
programme en cours d'exécution
↓
signal reçu
↓
le noyau interrompt le flux normal
↓
handler appelé
↓
retour au programme normal
5. L'ancienne API : signal()
Header :
Code: Select all
#include <signal.h>
Code: Select all
void (*signal(int sig, void (*handler)(int)))(int);
Code: Select all
signal(SIGINT, handler);
Code: Select all
#include <signal.h>
#include <unistd.h>
void handler(int sig)
{
write(1, "Ctrl+C intercepte\n", 18);
}
int main(void)
{
signal(SIGINT, handler);
while(1){
pause();
}
return 0;
}
Mais en code sérieux, il vaut mieux utiliser sigaction(), car signal() est plus ancien et peut avoir des comportements moins portables selon les systèmes.
6. Envoyer un signal : kill()
La fonction kill() sert à envoyer un signal à un processus.
Header :
Code: Select all
#include <signal.h>
#include <sys/types.h>
Code: Select all
int kill(pid_t pid, int sig);
Exemples :
Code: Select all
kill(1234, SIGTERM);
kill(1234, SIGKILL);
kill(1234, SIGUSR1);
- Si pid > 0, le signal est envoyé au processus dont le PID est donné.
- Si pid == 0, le signal est envoyé aux processus du même groupe de processus que l'appelant.
- Si pid == -1, le signal est envoyé à tous les processus que l'appelant a le droit de signaler.
- Si pid < -1, le signal est envoyé au groupe de processus dont l'ID est -pid.
Code: Select all
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
int main(int argc, char *argv[])
{
pid_t pid;
if(argc != 2){
printf("usage: %s <pid>\n", argv[0]);
return 1;
}
pid = atoi(argv[1]);
if(kill(pid, SIGTERM) == -1){
perror("kill");
return 1;
}
printf("SIGTERM envoye au processus %d\n", pid);
return 0;
}
Cas très important :
Code: Select all
kill(pid, 0);
Cela permet de vérifier si un processus existe et si on a le droit de lui envoyer un signal.
Résultat :
Code: Select all
0 -> le processus existe et on a la permission
EPERM -> le processus existe, mais on n'a pas la permission
ESRCH -> le processus n'existe pas
Code: Select all
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <sys/types.h>
int main(int argc, char *argv[])
{
pid_t pid;
if(argc != 2){
printf("usage: %s <pid>\n", argv[0]);
return 1;
}
pid = atoi(argv[1]);
if(kill(pid, 0) == 0){
printf("le processus existe et on peut lui envoyer un signal\n");
}
else{
if(errno == EPERM){
printf("le processus existe, mais permission refusee\n");
}
else if(errno == ESRCH){
printf("le processus n'existe pas\n");
}
else{
perror("kill");
}
}
return 0;
}
La fonction raise() permet à un processus de s'envoyer un signal à lui-même.
Prototype :
Code: Select all
#include <signal.h>
int raise(int sig);
Code: Select all
raise(SIGINT);
Code: Select all
kill(getpid(), SIGINT);
Code: Select all
#include <signal.h>
#include <unistd.h>
void handler(int sig)
{
write(1, "signal envoye a soi-meme\n", 26);
}
int main(void)
{
signal(SIGUSR1, handler);
raise(SIGUSR1);
return 0;
}
La fonction killpg() envoie un signal à un groupe de processus.
Prototype :
Code: Select all
#include <signal.h>
int killpg(pid_t pgrp, int sig);
Exemple :
Code: Select all
killpg(1234, SIGTERM);
10. Afficher la description d'un signal
Pour afficher un texte humain associé à un signal, on peut utiliser :
Code: Select all
strsignal()
psignal()
Code: Select all
#include <string.h>
#include <signal.h>
Code: Select all
char *strsignal(int sig);
void psignal(int sig, const char *msg);
Code: Select all
#include <signal.h>
#include <string.h>
#include <stdio.h>
int main(void)
{
printf("SIGINT: %s\n", strsignal(SIGINT));
printf("SIGSEGV: %s\n", strsignal(SIGSEGV));
return 0;
}
Code: Select all
#include <signal.h>
int main(void)
{
psignal(SIGINT, "description");
psignal(SIGSEGV, "description");
return 0;
}
Un sigset_t est un ensemble de signaux.
Définition mentale :
On l'utilise pour :sigset_t = une structure qui contient zéro, un ou plusieurs signaux.
- définir quels signaux bloquer ;
- définir quels signaux attendre ;
- définir quels signaux sont pending ;
- définir quels signaux bloquer pendant un handler.
Code: Select all
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset(sigset_t *set, int sig);
int sigdelset(sigset_t *set, int sig);
int sigismember(const sigset_t *set, int sig);
Avant d'utiliser un sigset_t, il faut l'initialiser.
Pour créer un ensemble vide :
Code: Select all
sigemptyset(&set);
Code: Select all
sigfillset(&set);
Code: Select all
#include <signal.h>
#include <stdio.h>
int main(void)
{
sigset_t set;
sigemptyset(&set);
sigaddset(&set, SIGINT);
sigaddset(&set, SIGTERM);
if(sigismember(&set, SIGINT) == 1){
printf("SIGINT est dans l'ensemble\n");
}
return 0;
}
Ajouter un signal :
Code: Select all
sigaddset(&set, SIGINT);
Code: Select all
sigdelset(&set, SIGINT);
Code: Select all
if(sigismember(&set, SIGINT) == 1){
printf("SIGINT est present\n");
}
Code: Select all
sigemptyset() -> vide l'ensemble
sigfillset() -> remplit avec tous les signaux
sigaddset() -> ajoute un signal
sigdelset() -> retire un signal
sigismember() -> teste la présence d'un signal
Chaque processus ou thread possède un signal mask.
Définition simple :
Si un signal est bloqué, il n'est pas délivré immédiatement. Il reste en attente.Le signal mask est l'ensemble des signaux actuellement bloqués pour un thread.
Exemple mental :
Code: Select all
SIGINT est bloque
↓
l'utilisateur fait Ctrl+C
↓
SIGINT arrive, mais n'est pas livré
↓
SIGINT devient pending
↓
on débloque SIGINT
↓
SIGINT est livré
Prototype :
Code: Select all
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
Valeurs importantes :
Code: Select all
SIG_BLOCK ajoute les signaux de set au masque
SIG_UNBLOCK retire les signaux de set du masque
SIG_SETMASK remplace complètement le masque par set
Code: Select all
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
int main(void)
{
sigset_t set;
sigemptyset(&set);
sigaddset(&set, SIGINT);
if(sigprocmask(SIG_BLOCK, &set, NULL) == -1){
perror("sigprocmask");
return 1;
}
printf("SIGINT bloque pendant 10 secondes\n");
sleep(10);
printf("deblocage de SIGINT\n");
if(sigprocmask(SIG_UNBLOCK, &set, NULL) == -1){
perror("sigprocmask");
return 1;
}
return 0;
}
16. Les signaux pending
Un signal pending est un signal arrivé mais pas encore délivré.
Cela arrive souvent quand le signal est bloqué.
API :
Code: Select all
#include <signal.h>
int sigpending(sigset_t *set);
Exemple :
Code: Select all
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
int main(void)
{
sigset_t blockSet;
sigset_t pendingSet;
sigemptyset(&blockSet);
sigaddset(&blockSet, SIGINT);
sigprocmask(SIG_BLOCK, &blockSet, NULL);
printf("SIGINT bloque. Appuyez sur Ctrl+C maintenant.\n");
sleep(5);
sigpending(&pendingSet);
if(sigismember(&pendingSet, SIGINT) == 1){
printf("SIGINT est pending\n");
}
else{
printf("SIGINT n'est pas pending\n");
}
sigprocmask(SIG_UNBLOCK, &blockSet, NULL);
return 0;
}
Point très important : les signaux standards ne sont pas vraiment comptés un par un.
Si un signal standard est bloqué et envoyé plusieurs fois, le noyau peut simplement retenir :
Il ne retient pas forcément combien de fois il est arrivé.Ce signal est pending.
Exemple mental :
Code: Select all
SIGUSR1 bloque
1000 SIGUSR1 envoyes
on debloque SIGUSR1
le programme peut ne recevoir qu'un seul SIGUSR1
Il ne signifie pas forcément :Ce signal est arrivé au moins une fois.
Les signaux temps réel ont un comportement plus adapté à la file d'attente, mais ce chapitre insiste surtout sur les signaux standards.Ce signal est arrivé exactement N fois.
18. sigaction() : l'API propre pour installer un handler
La fonction sigaction() est l'API moderne et recommandée pour changer le comportement d'un signal.
Prototype :
Code: Select all
#include <signal.h>
int sigaction(int sig, const struct sigaction *act, struct sigaction *oldact);
- le handler à appeler ;
- les signaux à bloquer pendant le handler ;
- des options de comportement via des flags.
Code: Select all
struct sigaction {
void (*sa_handler)(int);
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void);
};
Code: Select all
sa_handler
sa_mask
sa_flags
sa_handler
C'est la fonction appelée quand le signal est livré.
Exemple :
Code: Select all
sa.sa_handler = handler;
Code: Select all
SIG_DFL -> revenir au comportement par défaut
SIG_IGN -> ignorer le signal
C'est l'ensemble de signaux à bloquer pendant l'exécution du handler.
Exemple :
Code: Select all
sigemptyset(&sa.sa_mask);
sigaddset(&sa.sa_mask, SIGTERM);
sa_flags
Ce champ permet de modifier certains comportements.
Quelques flags importants :
Code: Select all
SA_RESTART relance certains appels système interrompus
SA_RESETHAND remet le comportement par défaut après la première livraison
SA_NODEFER ne bloque pas automatiquement le signal courant pendant son handler
SA_SIGINFO utilise un handler avancé avec des informations supplémentaires
20. Exemple propre avec sigaction()
Code: Select all
#include <signal.h>
#include <unistd.h>
#include <string.h>
#include <stdio.h>
void handler(int sig)
{
write(1, "SIGINT recu\n", 12);
}
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");
return 1;
}
while(1){
pause();
}
return 0;
}
- On crée une variable struct sigaction.
- On la met à zéro avec memset().
- On définit le handler avec sa.sa_handler.
- On initialise sa.sa_mask.
- On met SA_RESTART dans sa.sa_flags.
- On installe le comportement avec sigaction().
- Le programme dort avec pause() jusqu'à recevoir un signal.
Quand on installe un comportement pour un signal, on peut utiliser deux constantes importantes.
SIG_DFL
Revient au comportement par défaut du signal.
Exemple :
Code: Select all
sa.sa_handler = SIG_DFL;
sigaction(SIGINT, &sa, NULL);
Ignore le signal.
Exemple :
Code: Select all
sa.sa_handler = SIG_IGN;
sigaction(SIGINT, &sa, NULL);
Attention : SIGKILL et SIGSTOP ne peuvent pas être ignorés.
22. Attendre un signal : pause()
La fonction pause() suspend l'exécution du processus jusqu'à la livraison d'un signal.
Prototype :
Code: Select all
#include <unistd.h>
int pause(void);
Exemple :
Code: Select all
#include <signal.h>
#include <unistd.h>
#include <stdio.h>
void handler(int sig)
{
write(1, "signal recu\n", 12);
}
int main(void)
{
signal(SIGINT, handler);
printf("en attente d'un signal...\n");
pause();
printf("pause terminee\n");
return 0;
}
Code: Select all
pause()
↓
le processus dort
↓
un signal arrive
↓
le handler s'exécute
↓
pause() retourne -1 avec EINTR
Signal ignoré
Le signal arrive, mais le processus ne fait rien.
Exemple :
Code: Select all
SIG_IGN
Le signal arrive, mais il n'est pas livré immédiatement. Il reste en attente.
Exemple :
Code: Select all
sigprocmask(SIG_BLOCK, &set, NULL);
Le signal est arrivé, mais il attend d'être livré.
Exemple :
Code: Select all
sigpending(&set);
Code: Select all
ignore -> le signal est jeté ou sans effet
bloque -> le signal attend
pending -> le signal est en attente de livraison
Dans un programme multi-thread, les signaux deviennent plus subtils.
Un signal peut être :
- adressé au processus ;
- adressé à un thread précis ;
- bloqué différemment selon les threads.
Donc, dans un programme multi-thread, on préfère souvent bloquer les signaux dans tous les threads, puis créer un thread dédié qui les attend proprement avec des API adaptées comme sigwait().
Même si cette partie peut être approfondie plus tard, le point important est :
25. Comparaison avec WindowsLe masque de signaux est une propriété du thread, pas seulement du processus entier.
Les signaux Linux peuvent rappeler plusieurs mécanismes Windows, mais aucun équivalent n'est parfait.
Comparaison mentale :
Code: Select all
Linux signal Windows proche mentalement
--------------------------------------------------------------------------
SIGINT Console Ctrl Handler
SIGTERM demande de terminaison propre
SIGKILL TerminateProcess, mais pas exactement pareil
SIGSTOP / SIGCONT suspension / reprise
SIGSEGV exception Access Violation / SEH
SIGFPE exception arithmétique
SIGCHLD notification de fin/changement d'état d'un child process
SIGALRM timer notification
SIGUSR1 / SIGUSR2 événement personnalisé
sigprocmask blocage temporaire de notifications
pause attente passive d'une notification
Un signal est plus proche d'une notification asynchrone que d'un objet Event Windows.Windows sépare beaucoup les mécanismes : events, handles, APC, exceptions, console handlers, TerminateProcess, WaitForSingleObject, etc. Linux utilise les signaux comme mécanisme Unix historique plus général.
26. Programme complet : handler + sigaction + pause
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;
write(1, "SIGINT recu, arret demande\n", 28);
}
}
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);
}
printf("programme en attente. Appuyez sur Ctrl+C.\n");
while(!stop){
pause();
}
printf("fin propre du programme\n");
return 0;
}
Parce qu'une variable modifiée dans un handler doit être manipulée avec prudence. Le type sig_atomic_t est prévu pour être lu/écrit de manière atomique vis-à-vis des handlers de signaux.
27. Programme complet : bloquer SIGINT puis vérifier pending
Code: Select all
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main(void)
{
sigset_t blockSet;
sigset_t pendingSet;
sigemptyset(&blockSet);
sigaddset(&blockSet, SIGINT);
if(sigprocmask(SIG_BLOCK, &blockSet, NULL) == -1){
perror("sigprocmask");
exit(EXIT_FAILURE);
}
printf("SIGINT bloque pendant 5 secondes. Appuyez sur Ctrl+C.\n");
sleep(5);
if(sigpending(&pendingSet) == -1){
perror("sigpending");
exit(EXIT_FAILURE);
}
if(sigismember(&pendingSet, SIGINT) == 1){
printf("SIGINT est en attente\n");
}
else{
printf("SIGINT n'est pas en attente\n");
}
printf("deblocage de SIGINT\n");
if(sigprocmask(SIG_UNBLOCK, &blockSet, NULL) == -1){
perror("sigprocmask");
exit(EXIT_FAILURE);
}
printf("fin\n");
return 0;
}
- un signal bloqué ;
- un signal pending ;
- un signal livré après déblocage.
Code: Select all
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <sys/types.h>
int main(int argc, char *argv[])
{
pid_t pid;
int sig;
if(argc != 3){
printf("usage: %s <pid> <signal>\n", argv[0]);
return 1;
}
pid = atoi(argv[1]);
sig = atoi(argv[2]);
if(kill(pid, sig) == -1){
if(errno == ESRCH){
printf("processus introuvable\n");
}
else if(errno == EPERM){
printf("permission refusee\n");
}
else{
perror("kill");
}
return 1;
}
printf("signal %d envoye au processus %d\n", sig, pid);
return 0;
}
Code: Select all
./send_signal 1234 15
29. Ce qu'il faut éviter dans un handler
Un handler peut interrompre ton programme à n'importe quel moment.
Il faut donc éviter d'appeler des fonctions non sûres dans un handler.
À éviter dans un handler :
- printf()
- malloc()
- free()
- fonctions complexes de la libc
- verrous/mutex si tu ne maîtrises pas parfaitement le contexte
- mettre à jour une variable globale de type volatile sig_atomic_t ;
- utiliser write() pour afficher un message simple ;
- faire le vrai travail plus tard dans la boucle principale.
Code: Select all
static volatile sig_atomic_t gotSigint = 0;
void handler(int sig)
{
gotSigint = 1;
}
Code: Select all
if(gotSigint){
// traitement propre ici
}
Headers principaux :
Code: Select all
#include <signal.h>
#include <unistd.h>
#include <sys/types.h>
#include <errno.h>
Code: Select all
signal() ancienne API pour installer un handler
sigaction() API propre pour installer un comportement de signal
kill() envoyer un signal à un processus
raise() s'envoyer un signal à soi-même
killpg() envoyer un signal à un groupe de processus
pause() attendre un signal
sigprocmask() bloquer/débloquer des signaux
sigpending() récupérer les signaux pending
strsignal() obtenir une description textuelle d'un signal
psignal() afficher une description d'un signal
Code: Select all
sigemptyset() initialise un set vide
sigfillset() initialise un set plein
sigaddset() ajoute un signal dans un set
sigdelset() retire un signal d'un set
sigismember() teste si un signal est dans un set
Code: Select all
sigset_t
struct sigaction
sig_atomic_t
pid_t
Définition à retenir :
Le programme peut :Un signal Linux est une notification logicielle envoyée par le noyau à un processus ou à un thread pour l'avertir d'un événement ou modifier son exécution.
- laisser l'action par défaut ;
- ignorer le signal ;
- installer un handler ;
- bloquer temporairement le signal ;
- voir s'il est pending ;
- attendre passivement un signal.
Code: Select all
Signal envoyé
↓
Le noyau vérifie la cible et les permissions
↓
Le signal devient pending ou est livré directement
↓
Le signal est ignoré, bloque, stoppe, tue ou appelle un handler
↓
Le processus continue, s'arrête ou se termine selon le cas
- SIGKILL et SIGSTOP ne peuvent pas être capturés, ignorés ou bloqués.
- kill() envoie un signal, elle ne tue pas forcément.
- kill(pid, 0) permet de tester l'existence d'un processus.
- sigset_t représente un ensemble de signaux.
- sigprocmask() sert à bloquer ou débloquer des signaux.
- Un signal bloqué devient souvent pending.
- Les signaux standards ne sont pas forcément comptés un par un.
- sigaction() est préférable à signal().
- pause() endort le programme jusqu'à la réception d'un signal.
- Dans un handler, il faut éviter les fonctions non sûres comme printf().
Signal
Notification asynchrone envoyée à un processus ou thread.
Signal handler
Fonction appelée quand un signal est livré.
Disposition
Comportement associé à un signal : défaut, ignorer, handler.
Signal mask
Ensemble des signaux temporairement bloqués.
Pending signal
Signal arrivé mais pas encore livré.
sigset_t
Type représentant un ensemble de signaux.
sigaction
Structure/API moderne pour définir le comportement d'un signal.
SIG_DFL
Utiliser le comportement par défaut.
SIG_IGN
Ignorer le signal.
SA_RESTART
Flag qui permet de relancer certains appels système interrompus par un signal.
34. Conclusion
Les signaux sont un mécanisme central d'Unix/Linux. Ils permettent au noyau, au terminal, aux timers et aux autres processus de notifier un programme qu'un événement s'est produit.
Ils peuvent servir à terminer un programme, le stopper, le reprendre, signaler une erreur mémoire, indiquer qu'un fils est mort, ou transmettre une notification personnalisée.
Pour programmer proprement avec les signaux, il faut retenir trois idées :
- installer les handlers avec sigaction() ;
- utiliser sigset_t et sigprocmask() pour bloquer/débloquer les signaux ;
- ne pas faire de traitement complexe directement dans un handler.
Un signal est une alerte logicielle du noyau vers un processus ou un thread, et le programme peut choisir comment réagir à cette alerte.
