IBM Beispielcode, nicht wiedereintretende Funktionen funktionieren in meinem System nicht

11

Ich habe den Wiedereintritt in die Programmierung studiert. Auf dieser Seite von IBM (wirklich gut). Ich habe einen Code gegründet, der unten kopiert wurde. Es ist der erste Code, der auf der Website veröffentlicht wird.

Der Code versucht, die Probleme beim gemeinsamen Zugriff auf Variablen in einer nichtlinearen Entwicklung eines Textprogramms (Asynchronität) aufzuzeigen, indem zwei Werte gedruckt werden, die sich in einem "gefährlichen Kontext" ständig ändern.

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

Die Probleme traten auf, als ich versuchte, den Code auszuführen (oder besser gesagt, nicht). Ich habe gcc Version 6.3.0 20170516 (Debian 6.3.0-18 + deb9u1) in der Standardkonfiguration verwendet. Die fehlgeleitete Ausgabe tritt nicht auf. Die Häufigkeit, mit der "falsche" Paarwerte erhalten werden, ist 0!

Was ist denn los? Warum gibt es kein Problem beim Wiedereintritt mit statischen globalen Variablen?

Daniel Bandeira
quelle
1
Stellen Sie sicher, dass alle Compiler-Optimierungen deaktiviert sind, und versuchen Sie es erneut
roaima
Ich nahm das an ... aber welche Optionen würde ich ändern? Ich habe keine Ahnung. :-(
Daniel Bandeira
5
Dies sieht aus wie eine Programmierfrage (Stapelüberlauf). Es scheint hier nicht gut platziert zu sein. (Entschuldigung, ich hatte weniger Unterseiten; es ist so zerschnitten. Aber so ist es.)
Strg-Alt-Delor
1
Der einfachste Wiedereintrittscode ist unveränderlich.
Strg-Alt-Delor
Im ersten Moment denke ich, dass die Frage mit der gcc- und Linux-Umgebung zusammenhängen würde. Entwickeln Sie zum Beispiel die Planung des Betriebssystems (Ausführen von mehr Programmtext als Nach-Unterbrechungssignal vor dem Aufrufen der Handler-Routine).
Daniel Bandeira

Antworten:

12

Das ist nicht wirklich Eingang ; Sie führen eine Funktion nicht zweimal im selben Thread (oder in verschiedenen Threads) aus. Sie können dies durch Rekursion oder Übergabe der Adresse der aktuellen Funktion als Rückruffunktionszeigerarg an eine andere Funktion erhalten. (Und es wäre nicht unsicher, weil es synchron wäre).

Dies ist einfach nur Vanilla Data-Race UB (Undefined Behaviour) zwischen einem Signalhandler und dem Haupt-Thread: Nur dies sig_atomic_tist garantiert sicher . Andere funktionieren möglicherweise, wie in Ihrem Fall, in dem ein 8-Byte-Objekt mit einer Anweisung auf x86-64 geladen oder gespeichert werden kann, und der Compiler wählt zufällig diesen asm. (Wie die Antwort von @ icarus zeigt).

Siehe MCU-Programmierung - Unterbrechung der C ++ O2-Optimierung während der Schleife - Ein Interrupt-Handler auf einem Single-Core-Mikrocontroller ist im Grunde dasselbe wie ein Signal-Handler in einem Single-Threaded-Programm. In diesem Fall ist das Ergebnis des UB, dass eine Last aus einer Schleife gehoben wurde.

Ihr Testfall des Zerreißens aufgrund eines Datenrassen-UB wurde wahrscheinlich im 32-Bit-Modus oder mit einem älteren dümmeren Compiler entwickelt / getestet, der die Strukturelemente separat geladen hat.

In Ihrem Fall kann der Compiler die Speicher aus der Endlosschleife heraus optimieren, da kein UB-freies Programm sie jemals beobachten könnte. dataist nicht _Atomicodervolatile , und es gibt keine anderen Nebenwirkungen in der Schleife. Es gibt also keine Möglichkeit, dass ein Leser mit diesem Schreiber synchronisiert. Dies geschieht tatsächlich, wenn Sie mit aktivierter Optimierung kompilieren ( Godbolt zeigt eine leere Schleife am unteren Rand von main). Ich habe auch die Struktur in zwei geändert long long, und gcc verwendet einen einzelnen movdqa16-Byte-Speicher vor der Schleife. (Dies ist nicht garantiert atomar, aber es ist in der Praxis auf fast allen CPUs, vorausgesetzt, es ist ausgerichtet, oder Intel überschreitet lediglich keine Cache-Zeilengrenze. Warum ist die Ganzzahlzuweisung für eine natürlich ausgerichtete Variable atomar auf x86? )

Das Kompilieren mit aktivierter Optimierung würde also auch Ihren Test unterbrechen und Ihnen jedes Mal den gleichen Wert anzeigen. C ist keine tragbare Assemblersprache.

volatile struct two_intwürde den Compiler auch zwingen, sie nicht zu optimieren, würde ihn aber nicht zwingen, die gesamte Struktur atomar zu laden / speichern. (Es wäre nicht aufhören es von so entweder zu tun, wenn.) Beachten Sie, dass volatilesich keine Daten-Rennen UB vermeiden, aber es ist ausreichend für inter-thread Kommunikation in der Praxis und war , wie die Menschen von Hand gerollt atomics gebaut (zusammen mit Inline - asm) vor C11 / C ++ 11 für normale CPU-Architekturen. Sie sind Cache-kohärente so volatileist in der Praxis meist ähnlich wie _Atomicmitmemory_order_relaxed rein Last und pure-Speicher, wenn für Typen verwendeten schmal genug , dass der Compiler einen einzigen Befehl verwenden , damit Sie nicht Zerreißen erhalten. Und natürlichvolatileEs gibt keine Garantien des ISO C-Standards für das Schreiben von Code, der mit _Atomicund mo_relaxed auf dieselbe Weise kompiliert wird.


Wenn Sie eine Funktion hätten, die global_var++;auf einem intoder von einem Signalhandler long longaus asynchron und asynchron ausgeführt wird, wäre dies eine Möglichkeit, die erneute Eingabe zum Erstellen eines Datenrassen-UB zu verwenden.

Abhängig davon, wie es kompiliert wurde (zu einem Speicherziel inkl. Hinzufügen oder hinzufügen oder zum Laden / Inkl / Speichern), wäre es in Bezug auf Signalhandler im selben Thread atomar oder nicht. Siehe Kann num ++ für 'int num' atomar sein? Weitere Informationen zur Atomizität unter x86 und in C ++. (C11s stdatomic.hund _AtomicAttribute bieten äquivalente Funktionen wie die std::atomic<T>Vorlage von C ++ 11 )

Ein Interrupt oder eine andere Ausnahme kann nicht in der Mitte eines Befehls auftreten, daher ist das Hinzufügen eines Speicherziels atomar. Kontext schaltet eine Single-Core-CPU ein. Nur ein (Cache-kohärenter) DMA-Writer kann ein Inkrement von a add [mem], 1ohne lockPräfix auf einer Single-Core-CPU "betreten". Es gibt keine anderen Kerne, auf denen ein anderer Thread ausgeführt werden könnte.

Es ist also ähnlich wie bei Signalen: Ein Signalhandler wird anstelle der normalen Ausführung des Threads ausgeführt, der das Signal verarbeitet, sodass er nicht in der Mitte eines Befehls verarbeitet werden kann.

Peter Cordes
quelle
2
Ich war gezwungen, Ihre als beste Antwort zu akzeptieren, obwohl mir die Antwort von Icaru ausreichte. Die klaren Konzepte, die Sie uns erzählt haben, geben mir einen Eimer mit Themen, die ich den ganzen Tag (und weiter) studieren kann. Tatsächlich habe ich auf den ersten Blick kaum das, was Sie in den ersten beiden Absätzen schreiben. Vielen Dank! Wenn Sie im Internet Artikel über Computer und Programmierung veröffentlichen, geben Sie uns den Link!
Daniel Bandeira
17

Wenn man sich den Godbolt- Compiler-Explorer ansieht (nachdem man den fehlenden hinzugefügt hat #include <unistd.h>), sieht man, dass für fast jeden x86_64-Compiler der generierte Code QWORD-Verschiebungen verwendet, um das onesund zerosin einer einzelnen Anweisung zu laden .

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

Auf der IBM Website wird angegeben, On most machines, it takes several instructions to store a new value in data, and the value is stored one word at a time.was für einen typischen CPU im Jahr 2005 möglicherweise zutraf, aber wie der Code zeigt, ist dies derzeit nicht der Fall. Das Ändern der Struktur auf zwei Longs anstelle von zwei Ints würde das Problem anzeigen.

Ich habe vorher geschrieben, dass dies "atomar" war, was faul war. Das Programm läuft nur auf einer einzigen CPU. Jede Anweisung wird aus der Sicht dieser CPU abgeschlossen (vorausgesetzt, es gibt nichts anderes, was den Speicher verändert, wie z. B. dma).

Auf der CEbene ist also nicht definiert, dass der Compiler eine einzelne Anweisung zum Schreiben der Struktur auswählt, sodass die im IBM Dokument erwähnte Beschädigung auftreten kann. Moderne Compiler, die auf aktuelle CPUs abzielen, verwenden eine einzige Anweisung. Eine einzelne Anweisung ist gut genug, um eine Beschädigung für ein einzelnes Thread-Programm zu vermeiden.

icarus
quelle
3
Versuchen Sie, den Datentyp von auf intzu ändern long longund auf 32 Bit zu kompilieren. Die Lektion ist, dass Sie nie wissen, ob / wann es brechen wird.
Strg-Alt-Delor
2
das heißt, in meiner Maschine ist die Zuweisung dieser beiden Werte eine atomare Operation? (unter Berücksichtigung der Kompilierung für die x86_64-Architektur)
Daniel Bandeira
1
long longKompiliert immer noch zu einem Befehl für x86-64: 16-Byte movdqa. Es sei denn, Sie deaktivieren die Optimierung wie in Ihrem Godbolt-Link. (Die Standardeinstellung von GCC ist der -O0Debug-Modus, der voller Speicher- / Neuladgeräusche ist und normalerweise nicht interessant anzusehen ist.)
Peter Cordes
Ich habe den Typ nach dem Lesen aller Kommentare in "long long" geändert. Das Ergebnis war interessant: Die erwarteten Ergebnisse wurden erzielt, und durch das Einrichten einiger Zähler konnten andere Vorstellungen dahingehend verbessert werden, wie die Rate nicht übereinstimmender Daten durch den Rest des Codes beeinflusst wird. Vielen Dank für jede Hilfe!
Daniel Bandeira