Codice di esempio IBM, le funzioni non rientranti non funzionano nel mio sistema


11

Stavo studiando il rientro in programmazione. Su questo sito di IBM (davvero buono). Ho fondato un codice, copiato di seguito. È il primo codice che viene visualizzato sul sito Web.

Il codice tenta di mostrare i problemi che coinvolgono l'accesso condiviso alla variabile in uno sviluppo non lineare di un programma di testo (asincronicità) stampando due valori che cambiano costantemente in un "contesto pericoloso".

#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;
   }
}

I problemi sono comparsi quando ho provato ad eseguire il codice (o meglio, non è apparso). Stavo usando gcc versione 6.3.0 20170516 (Debian 6.3.0-18 + deb9u1) nella configurazione predefinita. L'output errato non si verifica. La frequenza per ottenere valori di coppia "errati" è 0!

Cosa sta succedendo dopo tutto? Perché non ci sono problemi nel rientro utilizzando variabili globali statiche?


1
Assicurati che tutta l'ottimizzazione del compilatore sia disabilitata e riprova
roaima il

Supponevo che ... ma quali opzioni avrei cambiato? Non ne ho idea. :-(
Daniel Bandeira,

5
Sembra una domanda di programmazione (overflow dello stack). La dose non sembra ben posizionata qui. (Mi dispiace, con me c'erano meno siti secondari; è così ridotto. Ma è così.)
ctrl-alt-delor

1
Il codice di rientro più semplice è immutabile.
ctrl-alt-delor

Al primo momento, penso che la domanda sarebbe correlata all'ambiente gcc e Linux. Evoluzione, ad esempio, della pianificazione del sistema operativo (esecuzione di più messaggi di programma il segnale di interruzione successiva prima di chiamare la routine del gestore), ad esempio.
Daniel Bandeira,

Risposte:


12

Non è davvero un rientro ; non stai eseguendo una funzione due volte nello stesso thread (o in thread diversi). È possibile ottenerlo tramite la ricorsione o passando l'indirizzo della funzione corrente come un puntatore di funzione callback arg ad un'altra funzione. (E non sarebbe pericoloso perché sarebbe sincrono).

Questo è semplicemente un UB (Undefined Behaviour) di data race vaniglia tra un gestore di segnale e il thread principale: solo questo sig_atomic_tè garantito per questo . Altri potrebbero funzionare, come nel tuo caso in cui un oggetto a 8 byte può essere caricato o archiviato con un'istruzione su x86-64, e il compilatore sembra scegliere quell'asm. (Come mostra la risposta di @ icarus).

Vedere la programmazione MCU - l'ottimizzazione O ++ di C ++ si interrompe durante il ciclo - un gestore di interrupt su un microcontrollore single-core è sostanzialmente la stessa cosa di un gestore di segnali in un singolo programma thread. In tal caso, il risultato dell'UB è che un carico è stato sollevato da un circuito.

Il tuo caso di prova di lacerazione che si verifica effettivamente a causa dell'UB-data-race è stato probabilmente sviluppato / testato in modalità 32-bit, o con un compilatore più vecchio più stupido che caricava gli elementi struct separatamente.

Nel tuo caso, il compilatore può ottimizzare gli archivi dal ciclo infinito perché nessun programma privo di UB potrebbe mai osservarli. datanon è _Atomicovolatile e non ci sono altri effetti collaterali nel loop. Quindi non c'è modo che nessun lettore possa sincronizzarsi con questo scrittore. Questo infatti accade se si compila con l'ottimizzazione abilitata ( Godbolt mostra un loop vuoto nella parte inferiore di main). Ho anche cambiato la struttura in due long longe gcc usa un singolo movdqaarchivio a 16 byte prima del ciclo. (Questo non è garantito atomico, ma è in pratica su quasi tutte le CPU, supponendo che sia allineato, o su Intel semplicemente non attraversa un limite di cache-line. Perché l'assegnazione di numeri interi su un atomico variabile naturalmente allineato su x86? )

Quindi la compilazione con l'ottimizzazione abilitata potrebbe anche interrompere il test e mostrare sempre lo stesso valore. C non è un linguaggio di assemblaggio portatile.

volatile struct two_intforzerebbe anche il compilatore a non ottimizzarli, ma non lo forzerebbe a caricare / archiviare atomicamente l'intera struttura. (Non sarebbe smettere di farlo per entrambi, però.) Si noti che volatilenon senza evitare UB dati-gara, ma in pratica è sufficiente per la comunicazione inter-thread ed era come la gente costruito Atomics arrotolate a mano (con asm inline) prima di C11 / C ++ 11, per le normali architetture della CPU. Sono cache-coerente così volatileè , in pratica, in gran parte simile a _Atomicconmemory_order_relaxed per puro carico e puro-store, se utilizzato per tipi Limita sufficiente che il compilatore utilizzerà una singola istruzione in modo da non ottiene strappo. E naturalmentevolatilenon ha alcuna garanzia dallo standard ISO C rispetto al codice di scrittura che viene compilato nello stesso modo in cui si usa _Atomice mo_relaxed.


Se avessi una funzione che ha funzionato global_var++;su un into long longche esegui da main e in modo asincrono da un gestore di segnale, sarebbe un modo per utilizzare la re-immissione per creare un UB data-race.

A seconda di come è stato compilato (in una destinazione di memoria inc o add, o per separare load / inc / store) sarebbe atomico o meno rispetto ai gestori di segnali nello stesso thread. Vedi num ++ può essere atomico per 'int num'? per ulteriori informazioni sull'atomicità su x86 e in C ++. (C11 stdatomic.he l' _Atomicattributo forniscono funzionalità equivalenti al modello di C ++ 11 std::atomic<T>)

Una interruzione o altra eccezione non può avvenire nel mezzo di un'istruzione, quindi un'aggiunta di destinazione di memoria è wrt atomica. il contesto commuta su una CPU single-core. Solo un masterizzatore DMA (coerente con la cache) può "incrementare" un incremento da a add [mem], 1senza lockprefisso su una CPU single-core. Non ci sono altri core su cui potrebbe essere in esecuzione un altro thread.

Quindi è simile al caso dei segnali: un gestore di segnale esegue invece la normale esecuzione del thread che gestisce il segnale, quindi non può essere gestito nel mezzo di un'istruzione.


2
Sono stato spinto ad accettare la tua come la migliore risposta, nonostante la risposta di Icaru fosse sufficiente per me. I concetti chiari che ci hai detto mi danno un secchio di argomenti per studiare tutto il giorno (e oltre). In effetti, a prima vista non ho quasi quello che scrivi nei primi due paragrafi. Grazie! Se pubblichi articoli su Internet su computer e programmazione, forniscici il link!
Daniel Bandeira,

17

Osservando l' esploratore del compilatore godbolt (dopo aver aggiunto quello mancante #include <unistd.h>), si vede che per quasi tutti i compilatori x86_64 il codice generato utilizza mosse QWORD per caricare onese zerosin una singola istruzione.

        mov     rax, QWORD PTR main::ones[rip]
        mov     QWORD PTR data[rip], rax

Il sito IBM dice On most machines, it takes several instructions to store a new value in data, and the value is stored one word at a time.che potrebbe essere vero per cpus tipico nel 2005, ma come mostra il codice non è vero ora. Cambiare la struttura in modo che abbia due long piuttosto che due ints mostrerebbe il problema.

In precedenza avevo scritto che questo era "atomico" che era pigro. Il programma funziona solo su una singola CPU. Ogni istruzione verrà completata dal punto di vista di questa CPU (supponendo che non vi sia nient'altro che altera la memoria come dma).

Quindi a Clivello non è definito che il compilatore sceglierà una singola istruzione per scrivere la struttura, e quindi può verificarsi la corruzione menzionata nel documento IBM. I compilatori moderni destinati al cpus corrente usano una sola istruzione. Una singola istruzione è abbastanza buona da evitare la corruzione per un singolo programma thread.


3
Prova a cambiare il tipo di dati da inta long long, e compila a 32 bit. La lezione è che non sai mai se / quando si romperà.
ctrl-alt-delor

2
ciò significa che, nella mia macchina, l'assegnazione di questi due valori è un'operazione atomica? (considerando la compilazione per l'architettura x86_64)
Daniel Bandeira,

1
long longcompila ancora in un'istruzione per x86-64: 16 byte movdqa. A meno che non disabiliti l'ottimizzazione, come nel tuo link Godbolt. (L'impostazione predefinita di GCC è la -O0modalità di debug, che è piena di rumore di archivio / ricarica e di solito non è interessante da guardare.)
Peter Cordes

Ho cambiato il tipo in "long long" dopo aver letto tutti i commenti. Il risultato è stato interessante: i risultati attesi sono stati raggiunti e, istituendo alcuni contatori, è stato in grado di migliorare le concezioni di altri come come la velocità dei dati non corrispondenti è influenzata dal resto del codice. Grazie per tutto l'aiuto!
Daniel Bandeira,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.