Dans cet article de blog, Chris Down, ingénieur noyau à Meta, présente les dangers des signaux Linux dans les environnements de production Linux et explique pourquoi les développeurs et développeuses devraient les éviter le plus possible.
Un signal est un évènement généré par les systèmes Linux quand certaines conditions sont remplies. Les signaux peuvent être envoyés de différentes manières : du noyau à un processus, d’un processus à un autre processus ou d’un processus à lui-même. Après avoir reçu un signal, le processus peut effectuer des actions.
Les signaux ont toujours existé et sont un élément essentiel des environnements opérationnels basés sur Unix. Ils relient plusieurs composants indispensables du système d’exploitation (vidage mémoire, gestion du cycle de vie des processus, etc.) et sont globalement efficaces depuis plus de cinquante ans. C’est pourquoi les personnes qui suggèrent que leur utilisation pour la communication interprocessus pourrait comporter des risques sont regardées d’un mauvais œil. Pourtant, cet article recense certains problèmes de production causés par les signaux et propose des solutions et des alternatives.
Les signaux ont beaucoup d’avantages : ils sont standardisés, disponibles partout et ne nécessitent aucune dépendance supplémentaire en dehors de celles fournies par le système d’exploitation. Toutefois, leur sécurité est loin d’être garantie. Les signaux font un grand nombre de suppositions qu’il faut impérativement valider en fonction des critères recherchés, ou du moins, configurer correctement. Or, peu d’applications le font, même les plus connues. Elles s’exposent ainsi à de futurs incidents qui seront difficiles à corriger.
Nous allons étudier un incident qui s’est déroulé récemment dans l’environnement de production Meta pour souligner le danger des signaux. Nous reviendrons brièvement sur l’histoire de certains signaux et comment ils nous ont conduits à la situation actuelle, puis nous comparerons cela à nos besoins d’aujourd’hui, ainsi qu’aux problèmes que nous rencontrons en production.
Revenons quelque temps en arrière. L’équipe de LogDevice nettoyait sa base de code en supprimant les fonctionnalités et les lignes obsolètes. L’une des fonctionnalités abandonnées était un journal qui documentait certaines opérations réalisées par le service. Elle était devenue redondante et n’avait pas trouvé d’utilisateurs, d’où la décision de la supprimer. Vous pouvez voir cette modification sur cette page GitHub. Jusque-là, rien d’anormal.
La période qui avait directement suivi la modification était calme, l’environnement de production fonctionnait correctement et continuait de générer un trafic stable. Quelques semaines plus tard, l’équipe recevait un rapport signalant une perte fulgurante de nœuds de service. Le problème était lié au déploiement de la nouvelle version, mais l’équipe ne parvenait pas à en trouver la cause exacte. Quel changement avait provoqué un tel chamboulement ?
Après avoir exploré différentes pistes, l’équipe a déduit que la modification apportée au code évoquée plus tôt, provoquant la suppression de ces journaux, était à l’origine du problème. Pour quelles raisons ? Qu’est-ce qui n’allait pas dans ce code ? Si vous n’avez pas encore trouvé le problème, étudiez attentivement cette révision différentielle, car l’erreur ne saute pas aux yeux et aurait pu être commise par n’importe qui.
Logrotate est, pour ainsi dire, l’outil standard de rotation des journaux sur Linux. Créé il y a près de 30 ans, son fonctionnement est simple : il gère le cycle de vie des journaux en procédant à leur rotation et à leur suppression.
Il n’envoie pas lui-même de signaux, donc vous ne trouverez pas d’informations sur eux dans la documentation ou sur la page principale de Logrotate. Cependant, il peut exécuter arbitrairement des commandes avant ou après ses rotations. Voici un exemple de base issu de la configuration par défaut de Logrotate dans CentOS :
/var/log/cron /var/log/maillog /var/log/messages /var/log/secure /var/log/spooler { sharedscripts postrotate /bin/kill -HUP `cat /var/run/syslogd.pid 2> /dev/null` 2> /dev/null || true endscript }
Bien qu’elle semble un peu sommaire, partons du principe qu’elle fonctionne comme prévu. Elle indique à Logrotate d’envoyer SIGHUP
à l’identifiant de Page compris dans /var/run/syslogd.pid
, qui devrait correspondre à l’identifiant de Page de l’instance syslogd
en cours d’exécution, après chaque rotation des fichiers répertoriés.
Ce genre de configuration ne pose pas de problème pour une API publique stable comme syslog. Mais qu’en est-il pour un processus interne dans lequel la mise en œuvre de SIGHUP
dépend d’un détail variable ?
L’un des problèmes réside dans l’interprétation sémantique et la programmation des signaux. Alors que certains signaux ne se retrouvent pas dans l’espace utilisateur et n’ont qu’une seule signification, d’autres comme SIGKILL
et SIGSTOP
peuvent être interprétés différemment en fonction des développeur·ses d’application et des utilisateur·ices. Dans certains cas, la différence est purement académique, comme pour SIGTERM
, qui signifie généralement : « fermer proprement dès que possible ». Dans d’autres cas, comme pour SIGHUP
, la signification est plus équivoque.
SIGHUP
a été conçu pour les lignes série, afin d’indiquer que la connexion a été interrompue à l’autre bout de la ligne. Les habitudes ayant la vie dure, nous envoyons toujours SIGHUP
quand son équivalent moderne se présente : quand un pseudo terminal ou un terminal virtuel est fermé (d’où l’utilisation d’outils tels que nohup
pour le masquer).
Aux débuts d’Unix, les développeur·ses devaient mettre en place le rechargement d’un daemon. La procédure consistait généralement à rouvrir le fichier de configuration/journal sans redémarrer le système. C’est pourquoi les signaux semblaient un bon moyen d’y parvenir, sans créer de dépendances. Bien sûr, il n’existait aucun signal capable de réaliser cette tâche, mais ces daemons ne disposant d’aucun terminal de commande, ils n’avaient aucune raison de recevoir le signal SIGHUP
. Celui-ci semblait donc parfaitement convenir comme solution alternative, sans aucun gros inconvénient.
Pourtant, ce plan avait un petit défaut. L’état par défaut des signaux n’est pas « ignored », mais dépend de chaque signal. Cela signifie, par exemple, que les programmeur·ses n’ont pas à configurer manuellement SIGTERM
pour fermer leur application. Tant qu’ils et elles ne définissent pas d’autre gestionnaire de signaux, le noyau se charge de fermer leur programme, sans que du code ne soit ajouté dans l’espace utilisateur. Pratique !
Ce qui l’est moins, c’est que le comportement par défaut de SIGHUP
est de fermer le programme immédiatement. Cela ne pose pas de problème dans le cas du raccrochage initial, car ces applications n’étaient probablement plus utiles, mais cela n’est pas idéal pour les nouveaux usages.
Une solution pourrait être de supprimer toutes les instances susceptibles d’envoyer un signal SIGHUP
au programme. Malheureusement, c’est plus facile à dire qu’à faire dans les bases de code matures et volumineuses. SIGHUP
n’est pas un appel de communication interprocessus (IPC) étroitement contrôlé que vous pouvez facilement retrouver dans la base à l’aide d’une commande grep. Les signaux peuvent provenir de n’importe où, n’importe quand, et leur fonctionnement est rarement contrôlé (à l’exception du simple « êtes-vous cet utilisateur ou CAP_KILL
»). Au final, il est difficile de déterminer l’origine des signaux. Néanmoins, en explicitant davantage l’IPC, nous pouvons savoir si un signal est important ou s’il doit être ignoré.
Je suppose qu’à présent, vous commencez à deviner ce qu’il s’est passé. L’après-midi fatidique est arrivé pour l’équipe LogDevice : elle a publié sa version contenant le nouveau code. Tout se passait bien au début, mais dès le lendemain à minuit, la situation s’est mystérieusement dégradée. La coupable ? La strophe ci-dessous dans la configuration Logrotate de la machine, qui envoie au daemon logdevice un signal SIGHUP
qui n’est désormais plus pris en charge, et qui se révèle donc fatal :
/var/log/logdevice/audit.log { daily # [...] postrotate pkill -HUP logdeviced endscript }
Il est très facile de passer à côté d’une courte strophe dans une configuration Logrotate, en particulier quand vous supprimez une grosse fonctionnalité. Malheureusement, il est aussi difficile d’être sûr d’avoir éradiqué en une fois toute trace de son existence. Même dans des cas plus simples à vérifier que celui-ci, il est fréquent d’oublier des parties de code lors du nettoyage. Pourtant, ces oublis n’ont généralement pas de conséquences majeures, car le code restant n’est souvent plus opérationnel.
En théorie, l’incident est simple, tout comme sa solution : il suffit de ne pas envoyer le signal SIGHUP
et de répartir les actions LogDevice sur une plus longue période (autrement dit, ne pas l’exécuter à minuit pile). Toutefois, ce n’est pas pour ces nuances que nous avons choisi de parler de cet incident. Nous voulons nous en servir d’exemple afin d’encourager les développeur·ses à réserver l’utilisation des signaux en production aux cas les plus basiques et essentiels.
Tout d’abord, l’utilisation des signaux comme mécanisme pour gérer les modifications de l’état des processus dans le système d’exploitation est justifiée. Le signal SIGKILL
, par exemple, fonctionne exactement comme prévu et ne peut pas être contrôlé par un gestionnaire de signaux. Le comportement par défaut du noyau en réponse aux signaux SIGABRT
, SIGTERM
, SIGINT
, SIGSEGV
, SIGQUIT
et autres signaux similaires est également bien compris par les utilisateur·ices et les programmeur·ses.
Ces signaux ont tous comme effet d’évoluer vers un état de fin terminal au sein même du noyau lorsqu’ils ont été reçus. En d’autres termes, à partir du moment où vous recevez un signal SIGKILL
ou SIGTERM
sans gestionnaire de signaux dans l’espace utilisateur, aucune autre instruction de l’espace utilisateur n’est exécutée.
Il est important d’obtenir un état de fin terminal, car il reflète souvent une diminution de la complexité de la pile et du code en cours d’exécution. Les autres états révèlent plutôt une complexité grandissante, et sont plus difficiles à justifier, à mesure que le flux de code interfère avec la simultanéité.
Vous avez peut-être remarqué que nous n’avons pas parlé d’autres signaux qui génèrent un état terminal par défaut. Voici une liste de tous les signaux standard qui ont ce comportement par défaut (à l’exception des signaux de vidage mémoire tels que SIGABRT
ou SIGSEGV
, qui sont tous justifiés) :
Au premier abord, ils peuvent paraître inoffensifs, mais voici quelques points problématiques :
Nous en sommes déjà à un tiers des signaux terminaux qui sont pour le moins douteux, voire dangereux quand un programme doit être modifié. Pire, même les signaux soi-disant « définis par l’utilisateur » peuvent entraîner de gros problèmes si vous oubliez d’ajouter un SIG_IGN
explicite. Des signaux aussi inoffensifs que SIGUSR1
ou SIGPOLL
peuvent créer des incidents.
Le problème ne vient pas d’un manque de familiarité. Peu importe votre maîtrise des signaux, il reste très difficile d’écrire correctement du code comportant des signaux du premier coup, car les signaux sont bien plus complexes qu’ils ne paraissent.
Généralement, les programmeur·ses ne passent pas toute leur journée à réfléchir au fonctionnement interne des signaux. C’est pourquoi quand arrive le moment de gérer les signaux, ils et elles commettent souvent des erreurs d’implémentation.
Je ne parle même pas des cas « banals », comme les problèmes de sécurité d’une fonction de gestion des signaux, qui sont généralement réglés en envoyant une commande sig_atomic_t
ou en utilisant un code C++ atomic_signal_fence. En effet, ces cas sont assez bien documentés et restent gravés dans la mémoire de quiconque les a déjà rencontrés. Ce qui est plus délicat, c’est de raisonner en matière de flux du code des portions nominales d’un programme complexe quand il reçoit un signal. Il faut ainsi réfléchir constamment et explicitement aux signaux tout au long du cycle de vie de l’application : est-ce que SA_RESTART
est suffisant pour EINTR
? Quel flux appliquer si celui-ci se termine prématurément ? J’ai maintenant un programme en parallèle, qu’est-ce que ça implique ? Une alternative consiste à configurer un sigprocmask
ou un pthread_setmask
pour certaines parties du cycle de vie de l’application et espérer que le flux de code ne change jamais (ce qui est rarement le cas, compte tenu du rythme imposé dans le secteur). signalfd
ou l’exécution de sigwaitinfo
dans un thread dédié peuvent être d’une certaine aide, mais ces deux méthodes présentent suffisamment de cas problématiques et de préoccupations en matière de convivialité pour ne pas être recommandées.
Nous espérons que la plupart des programmeur·ses expérimenté·es ont aujourd’hui conscience qu’il est difficile de produire ne serait-ce qu’un exemple de code pouvant être exécuté sans risque. Or, gérer les signaux est en réalité bien plus difficile que d’écrire un code correct et sûr. Les gestionnaires de signaux doivent utiliser uniquement un code sans « lock » (exclusion mutuelle) avec des structures de données atomiques, respectivement, car le flux principal d’exécution est suspendu et nous ne savons pas quelles locks il contient, et car le flux principal d’exécution pourrait réaliser des opérations non atomiques. Ils doivent également être réentrants, c’est-à-dire qu’ils doivent être capables de s’imbriquer les uns dans les autres, car les gestionnaires de signaux peuvent se chevaucher si un signal est envoyé plusieurs fois (ou même au sein d’un seul signal, comme SA_NODEFER
). C’est l’une des raisons pour lesquelles vous ne pouvez pas utiliser de fonctions comme printf
ou malloc
dans un même gestionnaire de signaux, car elles dépendent d’exclusions mutuelles globales pour la synchronisation. Si vous possédiez cette lock lors de la réception du signal, puis procédez à appeler une fonction qui nécessite de nouveau cette lock, votre application sera alors bloquée. C’est un raisonnement très, très complexe. C’est pourquoi beaucoup de personnes gèrent les signaux avec un code de ce genre :
static volatile sig_atomic_t received_sighup; static void sighup(int sig __attribute__((unused))) { received_sighup = 1; } static int configure_signal_handlers(void) { return sigaction( SIGHUP, &(const struct sigaction){.sa_handler = sighup, .sa_flags = SA_RESTART}, NULL); } int main(int argc, char *argv[]) { if (configure_signal_handlers()) { /* failed to set handlers */ } /* usual program flow */ if (received_sighup) { /* reload */ received_sighup = 0; } /* usual program flow */ }
Bien que ce type de code, signalfd
ou toute autre tentative de gestion asynchrone des signaux semblent simples et fiables, ils ignorent tous que le point d’interruption est tout aussi important que les actions réalisées après réception du signal. Supposons que le code de votre espace utilisateur réalise des opérations E/S ou modifie les métadonnées des objets provenant du noyau (comme les nœuds index ou les descripteurs de fichiers). Au moment de l’interruption, vous serez probablement dans la pile d’un espace du noyau. Voici à quoi peut ressembler un thread qui tente de fermer un descripteur de fichier :
# cat /proc/2965230/stack [<0>] schedule+0x43/0xd0 [<0>] io_schedule+0x12/0x40 [<0>] wait_on_page_bit+0x139/0x230 [<0>] filemap_write_and_wait+0x5a/0x90 [<0>] filp_close+0x32/0x70 [<0>] __x64_sys_close+0x1e/0x50 [<0>] do_syscall_64+0x4e/0x140 [<0>] entry_SYSCALL_64_after_hwframe+0x44/0xa9
Ici, __x64_sys_close
est le variant x86_64 de l’appel système close
, qui ferme un descripteur de fichier. À ce moment de l’exécution, nous attendons la mise à jour du stockage de sauvegarde (wait_on_page_bit
). Les E/S étant considérablement plus lentes que les autres opérations, schedule
est un moyen d’indiquer au planificateur du processeur du noyau que nous allons réaliser une opération avec une forte latence (comme des E/S disque ou réseau) et qu’il devrait temporairement remplacer le processus actuel par un autre. Ainsi, nous signalons au noyau qu’il peut choisir un processus qui utilisera véritablement le processeur, plutôt que perdre du temps avec un processus qui doit être interrompu tant qu’il n’a pas reçu de réponse d’une opération susceptible de prendre beaucoup de temps.
Imaginez que nous envoyons un signal au processus qui était en cours d’exécution. Le signal que nous avons envoyé contient un gestionnaire d’espace utilisateur dans le thread de réception, donc nous reprendrons dans l’espace utilisateur. L’une des réactions du noyau pourrait être d’essayer de sortir de la programmation prévue dans schedule
, de dérouler davantage la pile et de renvoyer une erreur ESYSRESTART
ou EINTR
dans l’espace utilisateur pour indiquer que nous avons été interrompus. Mais le descripteur de fichier est-il bientôt fermé ? Quel est son état actuel ?
À présent que nous sommes de retour dans l’espace utilisateur, nous allons exécuter le gestionnaire de signal. Quand le gestionnaire de signal est fermé, nous allons propager l’erreur dans le wrapper close
de la bibliothèque libc de l’espace utilisateur, puis dans l’application qui, en théorie, devrait pouvoir gérer la situation rencontrée. Nous précisons « en théorie », car il est très difficile de prédire la réaction à ces signaux, et certains services en production ne gèrent pas très bien ces cas particuliers. Dans les applications où l’accent n’est pas mis sur l’intégrité des données, cela ne pose pas nécessairement de problème. En revanche, dans les applications de production qui accordent une importance à l’intégrité et à la cohérence des données, cela pose un problème majeur : le noyau ne donne pas de détails qui nous permettraient de comprendre jusqu’où il est allé, ce qu’il a accompli ou non, et ce que nous devrions faire pour remédier à la situation. Pire encore, si close
renvoie EINTR
, l’état du descripteur de fichier devient non spécifié :
“If close() is interrupted by a signal [...] the state of [the file descriptor] is unspecified.”
Bonne chance pour trouver le raisonnement qui permettra de gérer cette situation en toute sécurité dans votre application. En général, gérer EINTR
est compliqué même dans les appels système qui se comportent normalement. Il existe beaucoup de petits problèmes qui, cumulés, expliquent pourquoi SA_RESTART
est insuffisant. Les appels système ne peuvent pas tous être redémarrés, et vous ne pouvez pas demander aux développeur·ses de votre application de comprendre toutes les façons dont les signaux peuvent être reçus pour chaque appel système de chaque site d’appel. Extrait du manuel man 7 signal
:
“The following interfaces are never restarted after being interrupted by a signal handler, regardless of the use of SA_RESTART; they always fail with the error EINTR [...]”
(Les interfaces suivantes ne sont jamais redémarrées après avoir été interrompues par un gestionnaire de signaux, même quand SA_RESTART est utilisé ; elles se soldent toujours par un échec avec une erreur EINTR [...].) De même, vous ne pouvez pas utiliser sigprocmask
et vous attendre à ce que le flux du code reste statique. Les développeur·ses ne passent pas tout leur temps à réfléchir aux implications de la gestion des signaux ou à essayer de produire/conserver un code qui traite correctement les signaux. Il en va de même si vous utilisez un thread dédié avec sigwaitinfo
: GDB et d’autres outils similaires ne parviendront peut-être pas à débuguer le processus. Des flux de code ou gestionnaires de signaux contenant de légers défauts peuvent vite provoquer des bugs, des plantages, des corruptions difficiles à débuguer, des blocages et bon nombre de problèmes qui vous mèneront directement dans les bras de votre outil de gestion des incidents préféré.
Si vous pensiez que ces paragraphes sur la simultanéité, la réentrance et l’atomicité étaient déjà complexes, je vous laisse imaginer le casse-tête quand on ajoute le multithreading dans l’histoire. Et pourtant, il est important d’y penser, car de nombreuses applications complexes exécutent implicitement plusieurs threads distincts, comme dans jemalloc ou GLib. Il existe même certaines bibliothèques qui installent elles-mêmes des gestionnaires de signaux, ajoutant de la complexité à un problème déjà complexe.
Le manuel man 7 signal
contient les informations suivantes sur le sujet :
“A signal may be generated (and thus pending) for a process as a whole (e.g., when sent using kill(2)) or for a specific thread [...] If more than one of the threads has the signal unblocked, then the kernel chooses an arbitrary thread to which to deliver the signal.”
(Un signal peut être généré [et donc, mis en attente] pour l’ensemble d’un processus [par exemple, quand il est envoyé avec kill(2)] ou pour un thread spécifique [...]. Si le signal n’est pas bloqué pour plusieurs threads, le noyau choisit un thread au hasard pour délivrer le signal.) En résumé, pour la plupart des signaux, le noyau envoie le signal à n’importe quel thread qui ne bloque pas ce signal avec sigprocmask
. SIGSEGV, SIGILL et les signaux du même genre ressemblent à des interruptions, et dirigent explicitement le signal vers le thread incriminé. Cependant, contrairement à la croyance populaire, la plupart des signaux ne peuvent pas être explicitement envoyés vers un seul thread d’un groupe de threads, même avec tgkill
ou pthread_kill
.
Cela signifie que vous ne pouvez pas changer arbitrairement les caractéristiques globales de la gestion des signaux quand vous avez un ensemble de threads. Si un service doit périodiquement bloquer un signal avec sigprocmask
dans le thread principal, vous devez trouver un moyen externe de communiquer aux autres threads comment ils devraient réagir dans cette situation. Autrement, le signal pourrait être intercepté par un autre thread et disparaître à jamais. Bien entendu, vous pouvez éviter cela en bloquant les signaux des threads enfants, mais vous risqueriez alors de rendre la situation encore plus complexe s’ils ont besoin de gérer eux-mêmes leurs signaux, même pour des choses aussi basiques que waitpid
.
Comme tout ce que nous décrivons ici, ces problèmes ne sont pas techniquement insurmontables. Toutefois, ignorer les contraintes et les potentielles conséquences néfastes (bugs, confusion, voire pire) de la synchronisation complexe requise pour que tout fonctionne correctement relèverait de la négligence.
Les signaux sont diffusés de manière asynchrone dans le noyau. L’appel système kill
renvoie une réponse dès que le signal en attente est enregistré pour le processus ou l’instruction task_struct
du thread en question. La diffusion du signal dans les délais souhaités n’est ainsi aucunement garantie, même en absence de blocage.
Même si le signal est effectivement diffusé dans les délais, il n’existe aucun moyen de prévenir l’expéditeur du signal de l’état de sa requête d’action. Pour cette raison, vous ne devez pas utiliser les signaux pour communiquer les actions importantes. Celles-ci seront simplement déclenchées, puis oubliées, en l’absence de mécanisme pour signaler l’échec ou la réussite de l’envoi et des actions correspondantes. Comme nous l’avons vu plus haut, même les signaux les plus inoffensifs peuvent être dangereux s’ils ne sont pas configurés dans l’espace utilisateur.
N’importe quelle personne utilisant Linux a déjà été confrontée à un processus qu’elle souhaite arrêter, mais qui ne répond à aucun signal, même à ceux réputés fatals comme SIGKILL
. Ce problème s’explique en partie par une mauvaise compréhension de l’objectif de kill(1) : il ne sert pas à forcer la fermeture des processus, mais à mettre en attente une requête demandant la réalisation d’une action destinée au noyau (sans aucune indication sur le moment où elle sera traitée).
Le but de l’appel système kill
est de mettre le signal en attente dans les métadonnées des tâches du noyau, ce qu’il fait même quand une tâche SIGKILL ne ferme pas le processus. Dans le cas de SIGKILL
en particulier, le noyau garantit qu’aucune instruction supplémentaire ne sera exécutée en mode utilisateur ; en revanche, vous pouvez toujours être amené·e à exécuter des instructions pour réaliser des actions en mode noyau afin d’éviter une corruption des données ou la libération de ressources. C’est pourquoi l’envoi est considéré comme réussi, même quand l’état est « D » (veille sans interruption). L’appel « kill » n’échoue que si vous avez fourni un signal non valide, si vous n’avez pas l’autorisation d’envoyer ce signal ou si le pid destinataire du signal n’existe pas et ne peut donc pas être utilisé pour propager de manière fiable les états non terminaux aux applications.
L’un des meilleurs moyens d’éviter tout problème de gestion des signaux est de ne pas les gérer du tout. Toutefois, pour les applications qui doivent traiter les états de signaux tels que SIGTERM
, nous conseillons des API de haut niveau telles que folly::AsyncSignalHandler
, qui corrigent de manière intuitive un certain nombre de défauts.
sigprocmask
, afin de réduire la quantité de code à vérifier lors des contrôles réguliers des signaux. N’oubliez pas qu’en cas de changement des flux de code ou des stratégies de threading, le masque peut réagir de manière inattendue.signal(SIGHUP, SIG_IGN); signal(SIGQUIT, SIG_IGN); signal(SIGUSR1, SIG_IGN); signal(SIGUSR2, SIG_IGN);
Même dans les programmes bien conçus, il est extrêmement compliqué de comprendre le comportement des signaux. D’autre part, leur utilisation présente un risque inutile dans les applications qui ont accès à d’autres alternatives. En règle générale, n’utilisez pas les signaux pour communiquer avec la partie de votre programme dédiée à l’espace utilisateur. À la place, configurez le programme de sorte qu’il gère lui-même les évènements de manière transparente (avec inotify, par exemple) ou utilisez une méthode de communication avec l’espace utilisateur capable de signaler les erreurs à l’expéditeur, qui peut être énumérée et démontrée au moment de la compilation, comme Thrift, gRPC ou autre outil similaire.
J’espère vous avoir démontré que sous leur apparence simple et banale, les signaux sont significativement plus complexes. Leur prétendue simplicité en a fait l’API idéale pour les espaces utilisateur des logiciels. Or, leur utilisation nécessite une multitude de décisions implicites de conception qui ne sont pas adaptées à la plupart des cas d’utilisation de notre époque.
Ne vous méprenez pas : l’utilisation des signaux est justifiée dans certains cas. En l’absence d’espace utilisateur, les signaux sont pratiques pour communiquer au noyau l’état souhaité pour un processus, par exemple, l’arrêt d’un processus. Cependant, il est difficile de rédiger du premier coup un code correct quand les signaux risquent d’être piégés dans l’espace utilisateur.
Les signaux ont beaucoup d’atouts : ils sont standardisés, disponibles partout et ne nécessitent pas de dépendances. Toutefois, ils comportent leur lot d’inconvénients qui ne font que s’accumuler proportionnellement à l’ampleur des projets. J’espère que cet article vous aura donné des pistes pour contourner ce problème et atteindre vos objectifs d’une manière plus sûre, plus simple et plus intuitive.
Pour en savoir plus sur Meta Open Source, consultez notre site Open Source, abonnez-vous à notre chaîne YouTube, ou suivez-nous sur Twitter, Facebook et LinkedIn.