L’art de gérer les signaux
Je ne sais pas si vous êtes déjà au courant, mais les signaux UNIX font parti des plus vilaines erreurs informatiques. Source de confusion et d’erreurs c’est un casse tête habituel des néophytes.
Dans la plupart de mes applications je laissais les signaux par défaut car j’avais pas spécialement de traitement spécifique à faire. Dans mon travail actuel je développe une application qui utilise plusieurs threads et je dois aussi gérer des signaux pour arrêter certains périphériques physiques lorsque mon application s’arrête.
Mélanger signaux et threads est souvent aussi risqué qu’allumer une cigarette à la station service.
Le problème principal
Un signal est par définition appelé à n’importe quel moment et sur n’importe quel thread de l’application. Une approche simpliste qu’on pourrait se dire est : « et si je faisais une pile d’évènements dans mon handler ? ». Cependant, comme l’application est multithread, il va falloir protéger l’accès à la pile d’évènements dans mon signal.
En pseudo code imaginons :
static pthread_mutex_t mutex;
static EventList list;
static void add(int type)
{
(&mutex);
pthread_mutex_lock(&list, type);
event_append(&mutex);
pthread_mutex_unlock}
static void handler(int signum)
{
// Captation d'un signal USR1 pour recharger l'application.
if (signum == SIGUSR1)
(EVENT_RELOAD);
add}
Cette fonction add
pourrait être appelé par d’autres
threads et ensuite parcourue par la boucle principale du programme.
Ainsi chaque thread protège l’accès à la liste global ainsi que le
signal. Il s’agit là pourtant d’une très mauvaise idée. En effet, un
signal peut arriver à n’importe quel moment, y compris dans une
situation critique. Cela signifie que la fonction handler
peut être appelée exactement au moment où le verrou est déjà verrouillé
par le thread courant !

Bloquer les signaux
Les fonctions de l’API POSIX nous permettent de bloquer des signaux afin de ne pas les recevoir de manière asynchrone pour le processus ou pour chaque thread. Il nous reste alors deux solutions plutôt communes :
- Créer un thread qui capte tous les signaux.
- Bloquer tous les signaux sauf dans le thread principal et les
débloquer à un moment précis en utilisant
pselect
ou ppoll.
Mon application étant basée sur poll
nous allons
l’adapter pour que les signaux ne soient plus jamais présent dans les
threads.
Première étape : initialisation
La fonction pthread_sigmask nous permet de bloquer des signaux pour chaque thread nouvellement créé. Cela s’applique aussi aux threads créés par les threads eux mêmes.
Nous utilisons donc sigaddset
pour définir les signaux qui nous intéressent. Dans notre cas nous
allons bloquer SIGINT
, SIGTERM
et
SIGUSR1
.
;
sigset_t sigs
(&sigs);
sigemptyset(&sigs, SIGINT);
sigaddset(&sigs, SIGTERM);
sigaddset(&sigs, SIGUSR1);
sigaddset
// Application des signaux bloqués pour les threads prochainement créés.
(SIG_BLOCK, &sigs, NULL); pthread_sigmask
Deuxième étape : création des handlers
Maintenant que nos threads sont libérés de ces trois signaux, nous pouvons définir des handlers qui vont bien avec.
struct sigaction sa = {0};
(&sa.sa_mask);
sigemptyset
// Arrêt du programme sur ces deux signaux.
.sa_handler = stop;
sa(SIGINT, &sa, NULL);
sigaction(SIGTERM, &sa, NULL);
sigaction
// Rechargement du programme sur ce signal utilisateur.
.sa_handler = reload;
sa(SIGUSR1, &sa, NULL); sigaction
Les fonctions stop
et reload
sont
maintenant garanties de ne jamais s’exécuter dans un thread mais nous
n’avons pas fini car pour le moment les signaux sont aussi bloqués pour
le thread principal et il nous faut donc les débloquer. Pour ce faire,
nous utilisons ppoll
. Cette fonction est analogue à
pselect
, c’est à dire qu’elle surveille des descripteurs
mais permet aussi à des signaux de s’exécuter précisément pendant
l’appel de cette fonction.
Troisième étape : utilisation de ppoll
On doit donc donner une liste vide ou une liste ne contenant pas les
trois signaux qui nous intéressent. Dans notre cas nous pouvons donc
réinitialiser la liste des signaux à débloquer pendant l’appel à
ppoll
.
(&sigs);
sigemptyset
for (;;) {
static const struct timespec ts = { .tv_sec = 1 };
// On admet fds comme un tableau pollfd définit ailleurs et fds_count son
// nombre d'entrées.
if (ppoll(fds, fds_count, &ts, &sigs) < 0 && errno != EINTR)
(1);
exit}
Et ainsi notre application appelle les fonctions des signaux exactement quand nous le souhaitons.
Exemple complet
Voici un exemple de programme qui créé 4 threads qui ne font rien
mais dont le thread principal capte SIGUSR1
pour indiquer
un rechargement. Les signaux SIGINT
et SIGTERM
coupent le programme.
#include <errno.h>
#include <poll.h>
#include <pthread.h>
#include <signal.h>
#include <stdatomic.h>
#include <stdio.h>
#include <stdlib.h>
// Déconseillé mais pour le moment c'est suffisant.
static atomic_int jesus_returns = 0;
static void *
(void *data)
routine{
// Fonction du thread, ne fait rien qu'attendre.
while (!jesus_returns) {
("Thread is waiting...");
puts(NULL, 0, 2000);
poll}
return NULL;
}
static void
(int n)
stop{
("The signal %d was received!\n", n);
printf= 1;
jesus_returns }
static void
(int n)
reload{
("Reload signal %d received!\n", n);
printf}
int
(void)
main{
[4];
pthread_t threads;
sigset_t sigsstruct timespec ts = { .tv_sec = 1 };
struct sigaction sa = {0};
// On bloque SIGINT, SIGTERM et SIGUSR1 sur tous les threads.
(&sigs);
sigemptyset(&sigs, SIGINT);
sigaddset(&sigs, SIGTERM);
sigaddset(&sigs, SIGUSR1);
sigaddset(SIG_BLOCK, &sigs, NULL);
pthread_sigmask
// On assigne des handlers des familles.
(&sa.sa_mask);
sigemptyset.sa_handler = stop;
sa(SIGINT, &sa, NULL);
sigaction(SIGTERM, &sa, NULL);
sigaction.sa_handler = reload;
sa(SIGUSR1, &sa, NULL);
sigaction
// Réinitialisation des signaux pour les autoriser dans ppoll.
(&sigs);
sigemptyset
// Création de threads inutiles.
for (size_t i = 0; i < 4; ++i)
(&threads[i], NULL, routine, NULL);
pthread_create
while (!jesus_returns) {
// Pas de descripteurs de fichiers, on fait juste un ppoll.
if (ppoll(NULL, 0, &ts, &sigs) < 0 && errno != EINTR) {
("ppoll");
perror(1);
exit}
}
for (size_t i = 0; i < 4; ++i)
(threads[i], NULL);
pthread_join}
Note : pour le moment ppoll
n’est pas encore dans la
norme POSIX mais le sera dans la prochaine version. Il faut peut-être
compiler avec -D_GNU_SOURCE
sur glibc pour avoir la
déclaration de la fonction.