Isso não é realmente reentrância ; você não está executando uma função duas vezes no mesmo thread (ou em threads diferentes). Você pode obtê-lo por recursão ou passando o endereço da função atual como um argumento de retorno de chamada de função para outra função. (E não seria inseguro porque seria síncrono).
Este é apenas um simples UB (Undefined Behavior) entre um manipulador de sinal e o thread principal: somente isso sig_atomic_té garantido . Outros podem funcionar, como no seu caso, onde um objeto de 8 bytes pode ser carregado ou armazenado com uma instrução em x86-64, e o compilador escolhe esse asm. (Como mostra a resposta da @ icarus).
Veja Programação MCU - a otimização do C ++ O2 é interrompida durante o loop - um manipulador de interrupção em um microcontrolador de núcleo único é basicamente a mesma coisa que um manipulador de sinal em um único programa encadeado. Nesse caso, o resultado do UB é que uma carga foi içada de um loop.
Seu caso de teste de rasgo realmente acontecendo por causa da UB de corrida de dados provavelmente foi desenvolvido / testado no modo de 32 bits ou com um compilador mais antigo que carregou os membros da estrutura separadamente.
No seu caso, o compilador pode otimizar os armazenamentos a partir do loop infinito, porque nenhum programa livre de UB poderia observá-los. datanão é _Atomicouvolatile e não há outros efeitos colaterais no loop. Portanto, não há como qualquer leitor sincronizar com este escritor. Isso de fato acontece se você compilar com a otimização ativada ( Godbolt mostra um loop vazio na parte inferior do main). Também mudei a estrutura para dois long longe o gcc usa um único movdqaarmazenamento de 16 bytes antes do loop. (Isso não é garantido atômico, mas é na prática em quase todas as CPUs, supondo que esteja alinhado ou na Intel simplesmente não ultrapasse o limite da linha de cache. Por que a atribuição de número inteiro em uma variável atômica naturalmente alinhada no x86? )
Portanto, compilar com a otimização ativada também interromperia o teste e mostraria sempre o mesmo valor. C não é uma linguagem assembly portátil.
volatile struct two_inttambém forçaria o compilador a não otimizá-lo, mas não forçaria o carregamento / armazenamento de toda a estrutura atomicamente. (Também não impediria que isso acontecesse.) Observe que volatileisso não evita o UB de corrida de dados, mas, na prática, é suficiente para a comunicação entre threads e foi como as pessoas construíram átomos enrolados à mão (junto com asm inline) antes de C11 / C ++ 11, para arquiteturas normais de CPU. Eles são cache-coerente, volatileé na prática, principalmente semelhante ao _Atomiccommemory_order_relaxed por puro-carga e puro-store, se usado para tipos de estreitar o suficiente para que o compilador irá utilizar uma única instrução para que você não obter lacrimejamento. E clarovolatilenão possui nenhuma garantia do padrão ISO C vs. código de gravação que é compilado da mesma maneira usando _Atomice mo_relaxed.
Se você tivesse uma função executada global_var++;em intou long longexecutada a partir de main e assincronamente a partir de um manipulador de sinal, essa seria uma maneira de usar a reentrada para criar UB de corrida de dados.
Dependendo de como foi compilado (para um destino de memória incluído ou adicionado, ou para separar carga / inc / armazenamento), seria atômico ou não em relação aos manipuladores de sinal no mesmo encadeamento. Consulte Num ++ pode ser atômico para 'int num'? para saber mais sobre atomicidade em x86 e em C ++. (O C11 stdatomic.he o _Atomicatributo fornecem funcionalidade equivalente ao modelo do C ++ 11 std::atomic<T>)
Uma interrupção ou outra exceção não pode acontecer no meio de uma instrução, portanto, um add de destino de memória é wrt atômico. contexto alterna em uma CPU de núcleo único. Somente um gravador de DMA (coerente em cache) poderia "avançar" em um incremento de um add [mem], 1sem lockprefixo em uma CPU de núcleo único. Não há outros núcleos nos quais outro thread possa estar sendo executado.
Portanto, é semelhante ao caso dos sinais: um manipulador de sinal é executado em vez da execução normal do encadeamento que manipula o sinal, portanto, não pode ser manipulado no meio de uma instrução.