J'étudiais la rentrée dans la programmation. Sur ce site d'IBM (vraiment bon). J'ai fondé un code, copié ci-dessous. C'est le premier code qui arrive sur le site Web.
Le code essaie de montrer les problèmes liés à l'accès partagé aux variables dans un développement non linéaire d'un programme texte (asynchronicité) en imprimant deux valeurs qui changent constamment dans un "contexte dangereux".
#include <signal.h>
#include <stdio.h>
struct two_int { int a, b; } data;
void signal_handler(int signum){
printf ("%d, %d\n", data.a, data.b);
alarm (1);
}
int main (void){
static struct two_int zeros = { 0, 0 }, ones = { 1, 1 };
signal (SIGALRM, signal_handler);
data = zeros;
alarm (1);
while (1){
data = zeros;
data = ones;
}
}
Les problèmes sont apparus lorsque j'ai essayé d'exécuter le code (ou mieux, n'apparaissait pas). J'utilisais gcc version 6.3.0 20170516 (Debian 6.3.0-18 + deb9u1) dans la configuration par défaut. La sortie erronée ne se produit pas. La fréquence d'obtention de «mauvaises» valeurs de paire est 0!
Que se passe-t-il après tout? Pourquoi n'y a-t-il pas de problème de ré-entrée en utilisant des variables globales statiques?
Réponses:
Ce n'est pas vraiment une réintégration ; vous n'exécutez pas une fonction deux fois dans le même thread (ou dans des threads différents). Vous pouvez l'obtenir via la récursivité ou en passant l'adresse de la fonction actuelle comme argument de pointeur de fonction de rappel à une autre fonction. (Et ce ne serait pas dangereux car ce serait synchrone).
Ceci est tout simplement de la course aux données vanille UB (Undefined Behavior) entre un gestionnaire de signal et le thread principal: seul
sig_atomic_t
est garanti sans danger pour cela . D'autres peuvent arriver à fonctionner, comme dans votre cas où un objet de 8 octets peut être chargé ou stocké avec une instruction sur x86-64, et le compilateur arrive à choisir cet asm. (Comme le montre la réponse de @ icarus).Voir Programmation MCU - L'optimisation C ++ O2 se casse en boucle - un gestionnaire d'interruption sur un microcontrôleur monocœur est fondamentalement la même chose qu'un gestionnaire de signal dans un programme à thread unique. Dans ce cas, le résultat de l'UB est qu'une charge s'est hissée hors d'une boucle.
Votre cas de test de déchirure se produisant réellement en raison de la course aux données UB a probablement été développé / testé en mode 32 bits, ou avec un compilateur plus ancien qui chargeait les membres de la structure séparément.
Dans votre cas, le compilateur peut optimiser les magasins hors de la boucle infinie car aucun programme sans UB n'a pu les observer.
data
n'est pas_Atomic
ouvolatile
, et il n'y a pas d'autres effets secondaires dans la boucle. Il n'y a donc aucun moyen pour un lecteur de se synchroniser avec cet écrivain. Cela se produit en fait si vous compilez avec l'optimisation activée ( Godbolt montre une boucle vide en bas de main). J'ai également changé la structure en deuxlong long
, et gcc utilise un seulmovdqa
magasin de 16 octets avant la boucle. (Ce n'est pas garanti atomique, mais c'est en pratique sur presque tous les processeurs, en supposant qu'il est aligné, ou sur Intel ne franchit tout simplement pas une limite de ligne de cache. Pourquoi l'affectation d'entiers sur une variable naturellement alignée atomique sur x86? )Donc, la compilation avec l'optimisation activée casserait également votre test et vous montrerait la même valeur à chaque fois. C n'est pas un langage d'assemblage portable.
volatile struct two_int
forcerait également le compilateur à ne pas les optimiser, mais ne le forcerait pas à charger / stocker la structure entière atomiquement. (Cela ne l' empêcherait pas non plus .) Notez que celavolatile
n'évite pas l' UB de course aux données, mais dans la pratique, cela suffit pour la communication entre les threads et comment les gens ont construit des atomes atomiques roulés à la main (avec asm en ligne) avant C11 / C ++ 11, pour les architectures CPU normales. Ils cache cohérent doncvolatile
est en pratique essentiellement similaire à_Atomic
avecmemory_order_relaxed
pour pur-charge et pure magasin, si elle est utilisée pour les types assez étroit que le compilateur utilisera une instruction unique afin de ne pas déchirer. Et bien sûrvolatile
n'a aucune garantie de la norme ISO C par rapport à l'écriture de code qui se compile dans le même asm using_Atomic
et mo_relaxed.Si vous aviez une fonction qui fonctionnait
global_var++;
sur unint
oulong long
que vous exécutez à partir de main et de manière asynchrone à partir d'un gestionnaire de signaux, ce serait un moyen d'utiliser la rentrée pour créer une UB de course de données.Selon la façon dont il a été compilé (vers une destination mémoire inc ou add, ou pour séparer load / inc / store), il serait atomique ou non par rapport aux gestionnaires de signaux dans le même thread. Voir Est-ce que num ++ peut être atomique pour 'int num'? pour en savoir plus sur l'atomicité sur x86 et en C ++. (C11
stdatomic.h
et l'_Atomic
attribut fournissent des fonctionnalités équivalentes austd::atomic<T>
modèle de C ++ 11 )Une interruption ou une autre exception ne peut pas se produire au milieu d'une instruction, donc un ajout de destination de mémoire est atomique wrt. le contexte bascule sur un processeur monocœur. Seul un écrivain DMA (cohérent en cache) peut "monter" un incrément à partir d'un
add [mem], 1
sanslock
préfixe sur un processeur monocœur. Il n'y a pas d'autres cœurs sur lesquels un autre thread pourrait s'exécuter.C'est donc similaire au cas des signaux: un gestionnaire de signaux s'exécute au lieu de l'exécution normale du thread qui gère le signal, il ne peut donc pas être géré au milieu d'une instruction.
la source
En regardant l' explorateur du compilateur godbolt (après avoir ajouté le manquant
#include <unistd.h>
), on voit que pour presque tous les compilateurs x86_64, le code généré utilise des mouvements QWORD pour charger leones
etzeros
dans une seule instruction.Le site IBM dit
On most machines, it takes several instructions to store a new value in data, and the value is stored one word at a time.
ce qui aurait pu être vrai pour les processeurs types en 2005, mais comme le montre le code, ce n'est plus le cas actuellement. Changer la structure pour avoir deux longs plutôt que deux pouces montrerait le problème.J'ai déjà écrit que c'était "atomique", ce qui était paresseux. Le programme ne fonctionne que sur un seul processeur. Chaque instruction se terminera du point de vue de ce cpu (en supposant qu'il n'y a rien d'autre qui altère la mémoire tel que dma).
Donc, au
C
niveau, il n'est pas défini que le compilateur choisira une seule instruction pour écrire la structure, et ainsi la corruption mentionnée dans le document IBM peut se produire. Les compilateurs modernes ciblant les processeurs actuels utilisent une seule instruction. Une seule instruction suffit pour éviter la corruption d'un programme à thread unique.la source
int
àlong long
et compilez en 32 bits. La leçon est que vous ne savez jamais si / quand il se cassera.long long
compile toujours en une instruction pour x86-64: 16 octetsmovdqa
. Sauf si vous désactivez l'optimisation, comme dans votre lien Godbolt. (Le-O0
mode par défaut de GCC est le mode débogage, qui est plein de bruit de stockage / rechargement et généralement pas intéressant à regarder.)