"Memory Barrier" in C99

OP #8011598
Lesenswert?

Ich habe 2 Fragen zu folgendem (sinnlosen) Demonstrationscode für in C99!

1
int flag, counter;
2

3
interrupt foo(void)
4
{
5
    if(!flag) { /* challenge a: counter muss sicher for flag gesetzt werden */
6
        counter++;
7
        flag = 1;
8
    }
9
}
10

11
int main(void)
12
{
13
    for(;;) {
14
       if(flag) {/* challenge b: Verwendung von counter bevor flag gelöscht wird */
15
           (Verwendung von counter);
16
           flag = 0; 
17
       }
18
       doSomething();
19
    } 
20
}
  • wie stelle ich sicher, dass flag erst "nach" counter gesetzt wird?
    • "asm" dazwischen oder Funktionsaufruf?
    • (implizites) Sprachmittel?
    • oder ist bei beiden ein Lock/Unlock zwingend (z.B. DI/EI)
  • ist "volatile" für flag oder counter zwingend notwendig? ("doSomething" außerhalb der Übersetzungseinheit)
: Bearbeitet durch User
#8011614
Lesenswert?

Volatile ist nicht genug hier. Das macht nur dem Compiler bewusst, dass die Variable auch außerhalb der Context geändert werden kann. Deswegen wird praktisch beim jeden lese zugriff auf die Variable noch einmal gelesen. Bzw schreiben wird auch sofort abgesetzt. Aber Vorsicht, der Compiler weiß nichts von dem Cache. Der kann immer noch Probleme machen. Und der Compiler weiss auch nicht wie der CPU aufgebaut ist, ob der intern nochmal schreibefehle speichern kann. Es ist oft so, dass der RAM ECC bits hat, und eine Granularität von 8,16 oder 32 bytes sogar. Das heisst, es lohnt sich nicht sofort einen Read Modify Write zu machen, um 8 bits zu schreiben, weil der über den Bus gehen muss und letztendlich werden im RAM doch 8 bytes geändert. Manche CPUs sammeln hier schreibbefehle.

In Embedded bereicht hast du sehr oft MSYNC() und ISYCH() makros im Compiler abstraktion header file. Diese makros müssen auf den jeweiligen asm befehle gemappt werden. Je nach Architektur kann es anders heissen.

MSYNCH macht sicher dass alle Schreibaufträge die bisher ausgeführt worden sind auf den Bus geschickt werden. Manche CPUs haben einen DataStorage Unit was einige Schreibbefehle speichern kann (wegen granularität des RAMs). Das heisst, wenn man etwas schreibt, kann sein dass das screiben gar nicht sofort ausgeführt wird. Manche Kontroller setzten den nie ab....

ISYNC macht sicher, dass alle Befehle bis zum ISYNC komplett abgearbeitet werden. Sprich alles ist durch den Pipeline und ist ausgeführt.

Da ich paranoid bin, würde ich so machen: ISYNC() <= stellt sicher dass alle Befehle davor durch den CPU abgearbeitet worden sind MSYNC() <= stellt sicher dass alle Schreibaufträge auf den Bus gelandet sind ISYNC() <= stellt sicher dass der msynch auch abgearbeitet ist. Vermutlich braucht man das nicht, aber schaden kann es ja nicht. Am Besten noch darauf achten dass der Cache auch write trough ist, und beim lesen entweder über nichtgecachte Addresse gehen oder den Cache invalidieren.

#8011628
Lesenswert?

Wenn das normale Variablen sind und der Code auf einem CPU Core läuft, reicht volatile. Wenn main und foo auf unterschiedlichen CPU Cores laufen können, brauchst du sowas wie atomic_thread_fence aus C11. Auf Architekturen wie x86 kompiliert atomic_thread_fence zu nichts. Hast du kein C11, musst du das äquivalent mit inline Assembly einfügen.

Sehe gerade C11 hat atomic_signal_fence für den Fall, den man auch mit volatile lösen könnte.

: Bearbeitet durch User
Gast #8011635
Lesenswert?

Bruno V. schrieb:

ist "volatile" für flag oder counter zwingend notwendig?

Volatile nützt dir da nichts. Du brauchst sowas wie Semaphore statt dem "flag" oder EnterCriticalSections/LeaveCriticalSection (Windows) oder du muss die Interrupts im kritischen Code ausschalten (Mikrocontroller ohne OS).

#8011648
Lesenswert?

Udo K. schrieb:

Volatile nützt dir da nichts. Du brauchst sowas wie Semaphore statt dem "flag" oder EnterCriticalSections/LeaveCriticalSection (Windows) oder du muss die Interrupts im kritischen Code ausschalten (Mikrocontroller ohne OS).

So wie ich den Beispielcode verstehe, geht es nicht darum zu verhindern, dass foo aufgerufen wird bis flag den richtigen Wert hat, sondern darum zu verhindern, dass foo counter benutzt, wenn flag != 0. foo darf ruhig aufgerufen werden und nichts machen.

#8011650
Lesenswert?

Torsten R. schrieb:

Mit volatile wirst Du sicher erreichen können, dass der Compiler Zugriffe auf die Variablen nicht in der Reihenfolge ändert.

volatile ist nur und ausschließlich dafür gedacht, Speicherstellen mit Seiteneffekten (Hardwareregister o.ä.) dem Compiler nahezubringen.

Anwendungen von volatile für irgend etwas anderes, insbesondere zur Synchronisation von irgendwas, sind prinzipiell grundlegend falsch.

Wenn die Variablen im Codes des TO keine Seiteneffekte haben (keine Haedwareregister, kein Multithreading/-tasking), muß man sowieso die Anforderung bezgl. der zwingenden Reihenfolge der Operationen hinterfragen. Egal, wie der Compiler die Zugriffe auch umordnet, im Programm wird sich das nicht bemerkbar machen.

Ansonsten: https://stackoverflow.com/questions/19965076/gcc-memory-barrier-sync-synchronize-vs-asm-volatile-memory

Oliver

Gast #8011651
Lesenswert?

Daniel G. schrieb:

foo darf ruhig aufgerufen werden und nichts machen.

Klar darf foo aufgerufen werden. Der kritische Bereich ist ja die Abfrage ob flag gesetzt ist. Davor gehört ein EnterCriticalSection und danach ein LeaveCriticalSection oder aber er muss flag durch eine Semaphore Variable ersetzen.

(Firma: Torrox.de) #8011653
Lesenswert?

Oliver S. schrieb:

Anwendungen von volatile für irgend etwas anderes, insbesondere zur Synchronisation von irgendwas, sind prinzipiell grundlegend falsch.

Das ist so nicht ganz korrekt. Wenn Du (mit C99) keine anderes Mittel hast, dann musst Du Mittel verwenden, die a) funktionieren, b) aber nicht von der Sprache garantiert sind.

Du must an der Stelle einfach unterscheiden, welche Garantien Du von welcher Ebene bekommst. Interrupts gibt es ja auch nicht erst seit dem Jahr 2000. Und es hat schon vor 2000 zuverlässig funktioniert, dann aber eben nicht mit Mitteln der Sprache (C99). Dazu gab es schon immer Erweiterungen im Compiler, die Dir die benötigten Garantien geben (und das kann sehr wohl in vielen Fällen einfach ein volatile sein) z.b. irgendwelche Intrinsicts. Üblicherweise gibt es für die Kombination aus Hardware und Compiler eine Dokumentation, die beschreibt, was zu tun ist, damit man die gewünschten Garantien bekommt.

Neuere Varianten von C und C++ haben Möglichkeiten, die gewünschten Garantien einzufordern.Aber der OP hat ja explizit nach C99 gefragt.

#8011667
Lesenswert?

Torsten R. schrieb:

Das ist so nicht ganz korrekt. Wenn Du (mit C99) keine anderes Mittel hast, dann musst Du Mittel verwenden, die a) funktionieren, b) aber nicht von der Sprache garantiert sind.

Was einen natürlich auf sehr dünnes Eis bringen kann. Wenn’s irgendwo dokumentiert ist, daß der Compiler aus praktischer Vorsicht volatile-Zugriffe nicht umordnet, ok, aber nur „funktioniert halt“ ist schwierig.

Oliver

(Firma: Torrox.de) #8011690
Lesenswert?

Oliver S. schrieb:

Was einen natürlich auf sehr dünnes Eis bringen kann. Wenn’s irgendwo dokumentiert ist, daß der Compiler aus praktischer Vorsicht volatile-Zugriffe nicht umordnet, ok, aber nur „funktioniert halt“ ist schwierig.

Es ist aber das einzige Eis, dass es vor 2000 gab. Und auch "früher" wurde schon funktionierende Software in C geschrieben.

Es geht um die Kombination aus Compiler und Hardware. Und es geht nicht darum, dass es irgendwie funktioniert, sondern dass der (in der Regel) Hardware-Hersteller garantiert, dass bestimmte Hochsprachenkonstrukte mit bestimmten Compilern funktionieren. Vor 2000 wurde ja auch nicht alles an Firmware in Assembler geschrieben.

(Firma: 1984now) #8011753
Lesenswert?

Oliver S. schrieb:

volatile ist nur und ausschließlich dafür gedacht, Speicherstellen mit Seiteneffekten (Hardwareregister o.ä.) dem Compiler nahezubringen.

Nein. volatile ist dafür gedacht, dem Compiler nahezubringen, dass der Inhalt einer Speicherstelle sich durch ein Ereignis ändern könnte, was unabhängig vom aktuell durch den Compiler behandelten Code geschieht.

Das kann natürlich eine hardware-induzierte Änderung in Hardwareregistern sein. Aber nicht nur. Eine Änderung durch eine ISR betrifft das ganz genau so. Denn auch die passiert außerhalb des aktuellen Compiler-Scopes. Und natürlich auch eine Änderung durch konkurrierende Cores.

Was du meinst, sind zusätzliche Rücksichten, die man nehmen muss. Also Sachen, zu deren Behandlung volatile alleine nicht genügt. Das sind dann im einfachsten Fall Sachen, die sich mit atomics erschlagen lassen. Das ist sozusagen die nächste Eskalationsstufe. Darüber kommen dann noch die Sachen, die aus der Existenz von Caches oder Pipelines resultieren. Dafür braucht es dann weitere Maßnahmen.

Was du offensichtlich nicht verstanden hast: all die höheren Eskalationen implizieren die jeweils geringeren. Nur weil die sie nicht explizit hinschreiben mußt, heißt das nicht, dass sie nicht implizit Teil der höheren Konstrukte wären.

Du bist ein typischen Hochsprachen-Programmierer. Du verstehst nur die Form, nicht wirklich die Funktion der von dir verwendeten Konstrukte.

OP #8011781
Lesenswert?

Vielen Dank, für die Antworten bisher. Ein wenig Kontext:

Der Beispielcode würde so OK sein, wenn die Abfrage in der Reihenfolge erfolgt. Beide "Threads" spielen dann Ping-Pong mit "flag" als binärem Token. Ob foo "umsonst" aufgerufen wird (mit flag noch immer gesetzt) spielt keine Rolle.

Alles auf einem Prozessor, so dass im Zweifel volatile und DI/EI reichen. Wüsste nur gerne, ob es notwendig ist.

Ich verstehe volatile mit dem Link von Oliver so: wenn, müssen beide Variablen volatile sein, da der Compiler "non volatile" Zugriffe "drumherum" verschieben darf. Beispiel: Wenn nur flag volatile ist, kann der Compiler es nach der Abfrage sofort setzen und counter danach behandeln.

Nehmen wir an, flag und counter sind volatile. Reicht das? Bei jedem Durchlauf (in beiden Funktionen) wird dann A) flag abgefragt B) counter nur bearbeitet, wenn flag "richtig" war C) flag erst am Ende umgestellt

Darf der Prozessor den Code noch umsortieren?

#8011786
Lesenswert?

Nein, mit volatile darf der Compiler die Zugriffe nicht umsortieren.

Das volatile schützt dich aber auch davor, dass der Compiler Annahmen dazu macht, welchen Wert die Variablen haben. Wenn der Compiler weiß, dass doSomething flag nicht ändert, würde er vermutlich den if(flag) Block vor die Schleife ziehen. Dass foo als Interrupt-Handler zu einem beliebigen Zeitpunkt aufgerufen werden kann, weiß er nicht.

Gast #8011793
Lesenswert?

Bruno V. schrieb:

Nehmen wir an, flag und counter sind volatile. Reicht das? Bei jedem Durchlauf (in beiden Funktionen) wird dann A) flag abgefragt B) counter nur bearbeitet, wenn flag "richtig" war C) flag erst am Ende umgestellt

Glaube nicht das volatile da was nützt.

Die Abfrage von flag passiert ja bevor flag invertiert wird.
Da würde ein volatile nur bewirken das flag neu aus dem RAM gelesen wird und eventuelles Vorwissen des Compilers über den Wert von flag verworfen wird.
Der Compiler hat aber zu dem Zeitpunkt gar kein Vorwissen, da flag das erste Mal in der Funktion eingelesen wird.

Für das Setzen von flag macht volatile auch keinen Unterschied. Auch wenn flag volatile ist, kann der Compiler den Code umordnen.

was aber einen Unterschied macht ist das flag eine globale Variable ist. Wenn du also anstatt "counter++" in Zeile 6 eine Funktion IncrementCounter() aufrufst, die in einem anderen Modul definiert ist, dann darf der Compiler flag erst nach dem Funktionsaufruf setzen, da er annehmen muss dass IncrementCounter() den Wert der globalen Variable flag verwendet. Das wäre eine saubere Lösung, die mit allen C Versionen bis K&R funktioniert.

(Firma: 1984now) #8011795
Lesenswert?

Daniel G. schrieb:

Udo K. schrieb:

Der kritische Bereich ist ja die Abfrage ob flag gesetzt ist.

Es ist schwierig sich eine CPU-Architektur vorzustellen, auf der das Ändern einer Variable von 0 nach 1 oder von 1 nach 0 nicht atomar passiert.

Nö, nicht wirklich. Das ist auf AVR8 z.B. keine Problem. Also vielmehr: kann durchaus ein Problem sein. Nämlich dann wenn die Variable ein Integertyp mit mehr als einem Byte Breite ist.

Ja OK. Wenn es nur um die Werte 0 und 1 geht, ist das kein Problem. Aber wenn es z.B. auch um den Wechsel zwischen 0 und -1 (oder irgendwas anderes, was einfach nur nicht null ist, also z.B. bool-true) geht, dann schon...

#8011800
Lesenswert?

Udo K. schrieb:

Du hast das Problem nicht verstanden.

Dann erleuchte mich. Zeige mir, wo in dem obigen Beispiel ohne EnterCriticalSection/LeaveCriticalSection o.ä. ein Zugriff aus einem Interrupt oder anderem Thread dazwischenfunken kann und etwas ungewolltes passiert. Wir können gerne auf Assemblerebene runtergehen.

: Bearbeitet durch User
OP #8011819
Lesenswert?

Daniel G. schrieb:

Wenn der Compiler weiß, dass doSomething flag nicht ändert, würde er vermutlich den if(flag) Block vor die Schleife ziehen.

ja, deshalb

Bruno V. schrieb:

("doSomething" außerhalb der Übersetzungseinheit)

Ich hätte interrupt foo1(void) und interrupt foo2(void) nehmen sollen. Auch, weil dann nicht klar ist, welches foo das andere unterbrechen kann.

Daniel G. schrieb:

wo in dem obigen Beispiel ohne EnterCriticalSection/LeaveCriticalSection o.ä. ein Zugriff aus einem Interrupt oder anderem Thread dazwischenfunken kann und etwas ungewolltes passiert.

ohne volatile für beide (und ohne Funktionsaufruf dazwischen): wenn flag direkt nach der Abfrage gelöscht wird, weil der Compiler die Zuweisungen neu ordnet.

Udo K. schrieb:

anstatt "counter++" in Zeile 6 eine Funktion IncrementCounter()... Das wäre eine saubere Lösung, die mit allen C Versionen bis K&R funktioniert.

Wimre habe wir stattdessen früher ein "Assembler NOP" in C eingefügt. Die genauen Zusicherungen dazu kenne ich nicht mehr. Darf mit so einem Nop nach counter++ nicht sogar das volatile entfallen?

: Bearbeitet durch User
#8011847
Lesenswert?

Ob S. schrieb:

Das kann natürlich eine hardware-induzierte Änderung in Hardwareregistern sein. Aber nicht nur. Eine Änderung durch eine ISR betrifft das ganz genau so. Denn auch die passiert außerhalb des aktuellen Compiler-Scopes. Und natürlich auch eine Änderung durch konkurrierende Cores.

Genau diese Meinung hat dazu geführt, daß C++ volatile wegen der Vielzahl falscher Benutzung mehr oder weniger ganz aus der Sprache streichen wollte. Das haben die zum Glück dann nicht gemacht, aber nein, außer den Hardware-induzierten Änderungen gibt es keinen Anwendungsfall für volatile.

Oliver

#8011849
Lesenswert?

Bruno V. schrieb:

Wimre habe wir stattdessen früher ein "Assembler NOP" in C eingefügt. Die genauen Zusicherungen dazu kenne ich nicht mehr. Darf mit so einem Nop nach counter++ nicht sogar das volatile entfallen?

Das NOP ist dem Compiler völlig egal. Das entscheidende ist, daß dieses Assembler-NOP auch eine memory barrier eingefügt hat. Das NOP kann man dann in der Assembleranweisung auch ganz weglassen.

Oliver S. schrieb:

Ansonsten: https://stackoverflow.com/questions/19965076/gcc-memory-barrier-sync-synchronize-vs-asm-volatile-memory

Und weils so schön ist, hier noch die allumfassende Weisheit zum Thema:

https://www.kernel.org/doc/html/v6.6/core-api/wrappers/memory-barriers.html#

Oliver

: Bearbeitet durch User
#8011855
Lesenswert?

Daniel G. schrieb:

Wenn das normale Variablen sind und der Code auf einem CPU Core läuft, reicht volatile.

Das ist nicht richtig.

Bei 8 Bit AVR können Zugriffe auf die beiden Bytes des 16 Bit Counter unterbrochen werden. Um dies zu verhindern kann man ggf. interrupts temporär blockieren. Da C99 dafür keinen Befehl hat, weicht man auf hardwarespezifische Befehle aus, hier cli() und sei(). Und ja, dafür gibt es im avr-gcc auch Makros (irgendwas mit "atomic" im Namen).

: Bearbeitet durch User
#8011860
Lesenswert?

Ich bin ja schon eine ziemliche Weile aus dem Thema raus. Gibt es in irgendwelchen neueren C und C++ Standards keine offiziellen Mechanismen, die über das aus einem anderen Zeitalter stammende "volatile" hinausgehen, und beispielsweise atomar zu behandelnde Daten betreffen?

: Bearbeitet durch User
#8011861
Lesenswert?

Nemopuk schrieb:

Bei 8 Bit AVR können Zugriffe auf die beiden Bytes des 16 Bit Counter unterbrochen werden.

Um dies zu verhindern, hat das Beispiel die flag Variable, die sich, wie oben bereits erwähnt, effektiv immer nur in einem Bit, also atomar, ändert.

Bruno V. schrieb:

("doSomething" außerhalb der Übersetzungseinheit)

Sich darauf zu verlassen, dass der Compiler deshalb nicht weiß, ob doSomething flag verändert, geht nur so lange gut, bis jemand flag static macht oder Link-Time-Optimization einschaltet.

: Bearbeitet durch User
#8011862
Lesenswert?

PS: Hier ist zwar explizit nach C99 gefragt, aber einen etwas grösseren Scope kann man dem Thema m.E. einräumen. Ist ja abgesehen von der Version des Standards eine allgemein gehaltene Frage, ohne Bezug auf irgendeine Plattform, weder Compiler noch Hardware. Bei C99 entsteht das Problem, dass man nicht völlig plattformunabhängig antworten kann.

: Bearbeitet durch User
#8011969
Lesenswert?

Daniel G. schrieb:

Nemopuk schrieb:

Bei 8 Bit AVR können Zugriffe auf die beiden Bytes des 16 Bit Counter unterbrochen werden.

Um dies zu verhindern, hat das Beispiel die flag Variable, die sich, wie oben bereits erwähnt, effektiv immer nur in einem Bit, also atomar, ändert.

Nein. Es geht um Hardwareregister von Timern etc. Davon gibt es mehrere. Wenn nun ein Zugriff in Hauptprogramm und in der ISR auf solche Register erfolgt, kann es krachen. Hier braucht man atomaren Zugriff. Ähnliches gilt für normale 8 Bot Portzugriffe, wenn die nichtatomare Read-Modify-Write Zugriffe machen. Siehe

https://www.mikrocontroller.net/articles/Interrupt#Atomarer_Datenzugriff

1
    ATOMIC_BLOCK(ATOMIC_RESTORESTATE) {
2
        tmp = TNCT1;
3
    }
: Bearbeitet durch User
#8011982
Lesenswert?

(prx) A. K. schrieb:

Ich bin ja schon eine ziemliche Weile aus dem Thema raus. Gibt es in irgendwelchen neueren C und C++ Standards keine offiziellen Mechanismen, die über das aus einem anderen Zeitalter stammende "volatile" hinausgehen, und beispielsweise atomar zu behandelnde Daten betreffen?

C: https://en.cppreference.com/w/c/atomic/memory_order.html https://en.cppreference.com/w/c/language/atomic.html

C++ vergleichbar…

Oliver

OP #8012008
Lesenswert?

Oliver S. schrieb:

Ob S. schrieb:

... hardware-induzierte Änderung in Hardwareregistern

... Eine Änderung durch eine ISR betrifft das ganz genau so.

aber nein, außer den Hardware-induzierten Änderungen gibt es keinen Anwendungsfall für volatile.

Konkrete Frage: subsummierst Du eine Variable, die in einem Interrupt geändert wird, auch als "HW-induziert"? Ohne volatile könnte der Compiler while(!f) doch zur Endlosschleife machen, oder?:

1
volatile char f;
2
interrupt foo(void) {f = 1;}
3

4
void foo2(void) {
5
   while(!f){} /* warte auf foo */
6
   ...
7
}
: Bearbeitet durch User
#8012084
Lesenswert?

Bruno V. schrieb:

Ohne volatile könnte der Compiler while(!f) doch zur Endlosschleife machen, oder?

Kann er nicht nur, darf er und macht er.

Er macht es nicht, wenn du die Variable volatile machst oder wenn du in die Schleife eine memory barrier einfügst, oder wenn du irgend eine dafür vorgesehene Möglichkeit deiner toolchain nutzt. Das Problem Atomic löst volatile und auch eine memory barrier nicht, bei den anderen Varianten kommt es darauf an.

Da der Anwendungsfall aber bis auf „sinnlosen Code“ nicht weiter definiert ist, und auch die Einschränkung auf c99 ziemlich willkürlich erscheint, musst du halt die Doku deiner Toolchain und deiner Harware lesen, und klären, was dein Problem überhaupt ist.

https://en.cppreference.com/w/c/language/atomic.html

„The volatile types do not provide inter-thread synchronization, memory ordering, or atomicity.“

Oliver

: Bearbeitet durch User
OP #8012097
Lesenswert?

Oliver S. schrieb:

Kann er nicht nur, darf er und macht er.

Er macht es nicht, wenn du die Variable volatile machst

OK. Es ging um Deine Aussage hier

Oliver S. schrieb:

Genau diese Meinung hat dazu geführt, daß C++ volatile wegen der Vielzahl falscher Benutzung mehr oder weniger ganz aus der Sprache streichen wollte. Das haben die zum Glück dann nicht gemacht, aber nein, außer den Hardware-induzierten Änderungen gibt es keinen Anwendungsfall für volatile.

Ich hoffe, ich verstehe Dich richtig, dass mein Beispiel eine weitere, legitime Anwendung (atomarer Zugriff vorausgesetzt) von volatile ist ODER Du Interrupts generell als "HW induziert" siehst. Beides ist OK für mich. Danke.

(Es handelt sich übrigens um einen NXP e200z7, 266MHz mit Windriver Compiler. Daher war ich entsprechend überrascht. Mein Part hat mit dem parallel vorhandenen Safety-Doppelkern nix zu tun.

: Bearbeitet durch User
#8012106
Lesenswert?

Oliver S. schrieb:

„The volatile types do not provide inter-thread synchronization, memory ordering, or atomicity.“

Volatile garantiert es nicht, aber die Variablen können diese Eigenschaften trotzdem oder zumindest in beschränktem Umfang haben. Z.B. ist memory ordering selbst auf ARM gegeben, wenn alle Zugriffe vom gleichen CPU Core aus geschehen. Und atomicity ist für einfaches lesen und setzen (einzeln, nicht in kombination) zumindest für einzelne Bytes immer gegeben.

Gast #8012130
Lesenswert?

Volatile hat nichts mit Hardware Register oder mit Interrupts oder mit dem Cache zu tun.

Volatile sagt dem Compiler nur, dass er bei jeder Verwendung einer Variable diese neu aus dem RAM lesen (schreiben) muss.
Ohne volatile könnte der Compiler die Variable in einem Register zwischenspeichern oder sie nur einmal am Ende eines Blocks schreiben.

Für den Code in der main Schleife (Zeile 14) ist volatile flag nicht notwendig, da die externe Funktion doSomething() die flag Variable ja ändern könnte, und der Compiler daher maximal das if(flag){flag=0} zu flag=0 verändern darf.

Volatile garantiert aber nicht dass if(!flag){counter++;flag=1;} (Zeile 6,7) so ausgeführt wird. Der Compiler könnte den Code zu if(!flag){ flag=1;counter++;} ändern. Wenn man das sicher verhindern will, dann kann man eine externe Nop() Funktion nach counter++ aufrufen.

#8012150
Lesenswert?

Udo K. schrieb:

Volatile garantiert aber nicht dass if(!flag){counter++;flag=1;} (Zeile 6,7) so ausgeführt wird. Der Compiler könnte den Code zu if(!flag){ flag=1;counter++;} ändern.

Nein, volatile garantiert, dass es nicht umsortiert wird. Der N1256 C99 Draft sagt zu volatile (u.a.):

"Furthermore, at every sequence point the value last stored in the object shall agree with that prescribed by the abstract machine, except as modified by the unknown factors mentioned previously."

counter++; und flag=1; sind Expression Statements und haben somit nach Annex C einen Sequence Point zwischen sich.

OP #8012189
Lesenswert?

Udo K. schrieb:

Volatile garantiert aber nicht dass if(!flag){counter++;flag=1;} (Zeile 6,7) so ausgeführt wird. Der Compiler könnte den Code zu if(!flag){ flag=1;counter++;} ändern.

In Ergänzung zu Daniel: Wenn nur eine Variable volatile ist, ja. Wenn beide volatile sind: nein. Zumindest, wenn der Compiler dafür sorgt, dass der Prozessor später nicht noch umsortiert.

Gast #8012208
Lesenswert?

Daniel G. schrieb:

"Furthermore, at every sequence point the value last stored in the object shall agree with that prescribed by the abstract machine, except as modified by the unknown factors mentioned previously."

Das hast du nicht richtig verstanden.

Das gilt nur für die abstrakte C-Standard-Maschine. Wenn der Compiler garantieren kann, dass die Variable am Ende der Funktion richtig im Speicher steht, muss er sie nicht 3x abspeichern blos weil irgendwo flag=1;flag=2;flag=3; steht.

#8012219
Lesenswert?

Falk B. schrieb:

Es geht um Hardwareregister von Timern etc. Davon gibt es mehrere. Wenn nun ein Zugriff in Hauptprogramm und in der ISR auf solche Register erfolgt, kann es krachen.

Das kann es nur, wenn die Hardwareregister breiter sind als die native Busbreite des µC - wenn das also z.B. ein 16-Bit-Register ist, und das auf einem 8-Bit-AVR angesteuert werden soll.

Darin unterscheidet sich das Register in keiner Weise von einer Variablen, die größer ist als die native Busbreite; ein Zugriff auf einen 16-Bit-Int kann also genau dasselbe Problem hervorrufen.

Ist aber das Register nicht größer, ist der Zugriff sowieso atomar.

#8012231
Lesenswert?

Harald K. schrieb:

Es geht um Hardwareregister von Timern etc. Davon gibt es mehrere. Wenn nun ein Zugriff in Hauptprogramm und in der ISR auf solche Register erfolgt, kann es krachen.

Das kann es nur, wenn die Hardwareregister breiter sind als die native Busbreite des µC - wenn das also z.B. ein 16-Bit-Register ist, und das auf einem 8-Bit-AVR angesteuert werden soll.

Das meinte ich ja, stand auch als Zitat drüber.

Darin unterscheidet sich das Register in keiner Weise von einer Variablen, die größer ist als die native Busbreite; ein Zugriff auf einen 16-Bit-Int kann also genau dasselbe Problem hervorrufen.

Jain. Bei den Hardwareregistern ist es ein unsichtbares, temporäres Register zum atomaren Lesen/Schreiben in Richtung Hardware, was aber bezüglich Software und ISRs eben NICHT atomar wirkt!

Ist aber das Register nicht größer, ist der Zugriff sowieso atomar.

Nö. Ein Read Modify Write Zugriff, welcher eben NICHT sbi/cbi nutzt, um einzelne Bits zu ändern, ist es auch nicht! Nur komplette, einfache Bytezugriffe beim Lesen oder Schreiben!

Gast #8012236
Lesenswert?

Udo K. schrieb:

Daniel G. schrieb:

"Furthermore, at every sequence point the value last stored in the object shall agree with that prescribed by the abstract machine, except as modified by the unknown factors mentioned previously."

Das hast du nicht richtig verstanden.

Ich hatte dich falsch zitiert und das volatile vor dem Zitat übersehen.

Wenn die Variable volatile ist, dann muss der Compiler die Reihenfolge einhalten. Die Reihenfolge in counter++;flag=1; ist also sicher wenn beide Variablen volatile sind.

Gast #8012246
Lesenswert?

Oliver S. schrieb:

Ja das geht ins Auge. Ist in C nicht notwendig und führt in die Debugging Hölle.

Das ist in C genauso erforderlich wie in allen anderen Programmiersprachen auch.

Link-Time Optimierung bringt in normalem C fast nichts. Das kommt aus der C++ Welt, wo es hunderte winzige Getter und Setter Funktionen gibt und viele verschachtelte Klassen, die wenig machen. Dazu die Standard Template Libs. Alles Inline zu deklarieren geht auch schlecht weil der Code dann nicht mehr lesbar ist und der Modul Gedanke sinnlos wird. Anwender einer Lib sollen ja nicht in die Funktionen reinschauen.

Aber versuche mal so ein Programm von dem du nur einen Core-Dump vom Kunden und (hoffentlich) ein Symbolfile hast zu debuggen. Wenn Funktionen teils geinlined werden, teils mit anderen Funktionen mit identischem Asm-Code zusamengelegt werden, wo unterschiedliche Funktionsaufrufe unterschiedlich optimiert werden. Das ist die Debugging Hölle.

Persönliche Seite #8012573
Lesenswert?

Daniel G. schrieb:

Z.B. ist memory ordering selbst auf ARM gegeben, wenn alle Zugriffe vom gleichen CPU Core aus geschehen.

Ich denke nicht. In den Load-Store-Units der Applikationsprozessoren findest du mehrere Slots, die nach bestimmten Regeln umsortiert werden dürfen (siehe Architecture Reference Manual). Selbst auf Cortex-M Prozessoren ließen sich dank der Harvard-Architektur noch Szenarien konstruieren, die sich in diesem Zusammenhang kritisch verhalten könnten.

#8013684
Lesenswert?

Marcus H. schrieb:

Ich denke nicht. In den Load-Store-Units der Applikationsprozessoren findest du mehrere Slots, die nach bestimmten Regeln umsortiert werden dürfen (siehe Architecture Reference Manual).

Ich würde sogar noch weiter gehen und behaupten, dass auf ARM ein Load von normal RAM genau das sieht, was der letzte Store in Program Order davor an die Adresse geschrieben hat. Schweinereien mit der Page Table und dem Cache zwischen Store und Load mal außen vor gelassen.

#8013697
Lesenswert?

Es kann davon abhängen, ob Adresse und Breite der Zugriffe identisch sind, oder sich lediglich überlappen.

Es kann davon abhängen, ob man sich auf die Sicht eines einzelnen Cores/Threads beschränkt, ob auch die Sicht eines zweiten auf die Zugriffe des ersten eine Rolle spielt, oder die Sicht des RAMs gemeint ist (DMA).

: Bearbeitet durch User
OP #8013767
Lesenswert?

(prx) A. K. schrieb:

ob man sich auf die Sicht eines einzelnen Cores/Threads beschränkt, ob auch die Sicht eines zweiten auf die Zugriffe des ersten eine Rolle spielt, oder die Sicht des RAMs gemeint ist (DMA).

Mein Prozessor hat zwar 2 Cores (einer sogar doppelt), aber mich interessiert nur 1 Core. Ich habe "Threads" oben geschrieben, meinte damit aber

  • 2 konkurrierende Kontexte
  • egal ob Interrupts oder Tasks des OS
  • wobei beide "Seiten" von je genau einem Kontext aufgerufen werden, also auch nicht gegen sich selbst gemutexed oder was auch immer werden müssen.
#8013869
Lesenswert?

Daniel G. schrieb:

Ich würde sogar noch weiter gehen und behaupten, dass auf ARM ein Load von normal RAM genau das sieht, was der letzte Store in Program Order davor an die Adresse geschrieben hat. Schweinereien mit der Page Table und dem Cache zwischen Store und Load mal außen vor gelassen.

Ja natürlich. Alles andere wäre auch ganz großer Murks, wie sollte man so ein Teil sonst programmieren, wenn man nach dem Schreiben nicht das gleiche wieder zurücklesen würde. (Es geht hier erstmal gar nicht darum ob die Daten auch wirklich im RAM angekommen sind oder nicht, Die CPU hat auch interne Buffer wo Writes drinnen landen können)

Persönliche Seite #8014051
Lesenswert?

Marcus H. schrieb:

Daniel G. schrieb: Z.B. ist memory ordering selbst auf ARM gegeben, wenn alle Zugriffe vom gleichen CPU Core aus geschehen.

Ich denke nicht. In den Load-Store-Units der Applikationsprozessoren findest du mehrere Slots, die nach bestimmten Regeln umsortiert werden dürfen

Daniel G. schrieb:

Ich würde sogar noch weiter gehen und behaupten, dass auf ARM ein Load von normal RAM genau das sieht, was der letzte Store in Program Order davor an die Adresse geschrieben hat.

Ja, natürlich. Mir ging es auch eher auf diesen Punkt im Ausgangspost des Threads: «wie stelle ich sicher, dass flag erst "nach" counter gesetzt wird?»

Das sind zwei unterschiedliche Adressen, deren Zugriffsreihenfolge in der Hardware nicht bei jeder Zielarchitektur garantiert ist. Mit Bordmitteln der Sprache C geht das jedenfalls nicht zuverlässig.

Beitrag #8014191 wurde vom Autor gelöscht.
#8014313
Lesenswert?

Marcus H. schrieb:

as sind zwei unterschiedliche Adressen, deren Zugriffsreihenfolge in der Hardware nicht bei jeder Zielarchitektur garantiert ist. Mit Bordmitteln der Sprache C geht das jedenfalls nicht zuverlässig.

Das hat mit C gar nix zu tun. Der interne RAM ist bei ARM z.B. üblicherweise als "Normal" in der MPU/MMU getaggt. D.h. am Memory Port der CPU kommen die Schreibzugriffe in irgend einer Reihenfolge raus. Es kann sogar sein, das die gar nicht rauskommen wenn sie zwischenzeitlich obsolet sind (Write Back Cache z.B.) Und es wird noch verrückter, der Speichercontroller darf die Zugriffe auch nochmal umsortieren so wie es ihm am besten passt.

Es spielt aber gar keine Rolle, was außen passiert. Wenn im Assembler/Maschinencode zunächst "Schreibe Adresse A", danach optional "Schreibe Adresse B" und schließlich "Lese A (oder B)" kommt dann wird der Lesezugriff genau den jeweils vorher geschriebenen Wert zurückliefern. Und zwar völlig unabhängig davon ob der Wert überhaupt im RAM angekommen ist, noch im Cache oder im Writebuffer rumschimmelt oder der Schreibzugriff im User Kontext und der Lesezugriffe aus dem Interruptkontext erfolgt oder umgekehrt oder ob der Speichercontroller intern umsortiert hat oder nicht.

Alles andere wäre komplett sinnfrei, eine CPU die nicht das tut was man ihr sagt oder erst mit einer undefinierten Verzögerung ist nutzlos, man müsste überall memory barriers einfügen.

"volatile" bezieht sich nur auf den C-Compiler und bewirkt (u.a.) das Zugriffe auf zwei volatile Variablen im generierten Assembler/Maschinencode nicht vertauscht werden dürfen und diese auch nicht wegoptimiert werden dürfen.

#8014393
Lesenswert?

Andreas M. schrieb:

Das hat mit C gar nix zu tun.

Doch, hat es. Wenn man in C programmiert, hat man halt nur die Mittel der Sprache zur Verfügung.

Alles andere wäre komplett sinnfrei, eine CPU die nicht das tut was man ihr sagt oder erst mit einer undefinierten Verzögerung ist nutzlos, man müsste überall memory barriers einfügen.

Es ist nicht ganz klar, ob du das Problem überhaupt verstanden hast. Erstens tut beim Programmieren in C eine CPU nie das, was man ihr sagt, sondern höchstens etwas, was für dich von außen so aussieht, als ob sie tut, was du ihr gesagt hast. Das ist ein signifikanter Unterschied. Programmierst du in C, so basiert das ganze zudem auf dem Model, das C dafür definiert.

Zum anderen gilt das alles nur und ausschließlich für eine Task alleine, und dort für einen Thread. Da ist es eine Selbstverständlichkeit, daß dein „Schreibe A, dann B“ usw. so ausgeführt wird, daß es für sich so aussieht, als ob es so passiert.

Der ganze Spaß mit Memory-Barriers, Synchronisation, und allem anderen beginnt erst, wenns über Thread- oder Taskgrenzen hinweg geht. Denn dann kommt genau das ins Spiel:

Andreas M. schrieb:

Der interne RAM ist bei ARM z.B. üblicherweise als "Normal" in der MPU/MMU getaggt. D.h. am Memory Port der CPU kommen die Schreibzugriffe in irgend einer Reihenfolge raus. Es kann sogar sein, das die gar nicht rauskommen wenn sie zwischenzeitlich obsolet sind (Write Back Cache z.B.) Und es wird noch verrückter, der Speichercontroller darf die Zugriffe auch nochmal umsortieren so wie es ihm am besten passt.

Dann schreibt Task 1 erst A, dann B, und Task 2 wartet auf die Zugriffsreihenfolge, und soll dann was tun, wenn das in der Reihenfolge passiert. Nur passiert das für die nie.

Die Lösung ist dann ganz einfach:

Andreas M. schrieb:

man müsste überall memory barriers einfügen.

Genau so ist es.

Modernes C und C++ haben einen ganzen Stall voll von Atomic- und Memorysynchronisationsmethoden, um mit solchen Situationen umgehen zu können. C99 ist da halt auf das angewiesen, was der RTOS/- Speziallib-/Systembetreuer an Werkzeugen bereitstellt. Die Sprache selbst kann da nicht.

Oliver

: Bearbeitet durch User
#8014485
Lesenswert?

Oliver S. schrieb:

Der ganze Spaß mit Memory-Barriers, Synchronisation, und allem anderen beginnt erst, wenns über Thread- oder Taskgrenzen hinweg geht. Denn dann kommt genau das ins Spiel:

Nein. Das ist Falsch. Für Synchronisation über Threads und Tasks sind Memory Barriers auf CPU Ebene weder gedacht noch notwendig. Lies die Doku von Intel, ARM oder von wem auch immer dazu. Memory Barriers werden ausschließlich dann benötigt wenn weitere Bus-Teilnehmer (z.B. Peripherien, DMA Controller, manchmal auch bei weiteren CPU Cores) beteiligt sind.

Was für Synchronisation hingegen gebraucht wird - wenn man C/c++ benutzt - ist ein Hinweis an den Compiler das bestimmte Zugriffe nicht vertauscht oder wegoptimiert werden dürfen. Und genau das erreicht man z.B. mit volatile oder aber auch mit memory clobbern.

Persönliche Seite #8014549
Lesenswert?

Andreas M. schrieb:

Und genau das erreicht man z.B. mit volatile oder aber auch mit memory clobbern.

Nur dass Memory Clobber kein C99 Sprachmittel ist.

Am ehesten entspricht das einem Funktionsaufruf den der Compiler nicht wegoptimieren kann und dessen Inhalt er nicht kennt. Auch dafür gibt es keine C99 Sprachmittel, sondern man muss dann per Compileroption dafür sorgen, dass entsprechende Funktionen siese Eigenschaft haben, etwa indem man interprozedurale Optimierungen deaktiviert.

#8014572
Lesenswert?

Andreas M. schrieb:

Für Synchronisation über Threads und Tasks sind Memory Barriers auf CPU Ebene weder gedacht noch notwendig. Lies die Doku von Intel, ARM oder von wem auch immer dazu. Memory Barriers werden ausschließlich dann benötigt wenn weitere Bus-Teilnehmer (z.B. Peripherien, DMA Controller, manchmal auch bei weiteren CPU Cores) beteiligt sind.

Wenn da ein BS läuft, daß alles, was irgendwie parallel läuft, also Prozesse/Tasks/..., völlig unbekümmert auf die vorhandenen CPU-Cores verteilt, braucht es zur Synchronisation alles, was es halt so braucht. Das dürfte beim TO nicht der Fall sein, aber im sonstigen Leben schon.

Oliver

#8014662
Lesenswert?

Johann L. schrieb:

Am ehesten entspricht das einem Funktionsaufruf den der Compiler nicht wegoptimieren kann und dessen Inhalt er nicht kennt. Auch dafür gibt es keine C99 Sprachmittel,

volatile gibt es auch in c99 und ist für die meisten Fälle ausreichend.

Oliver S. schrieb:

braucht es zur Synchronisation alles, was es halt so braucht.

Nun der Satz ist zwar irgendwie richtig, bringt aber leider niemanden weiter. Zum Essen machen brauchst du auch alles was es dazu so braucht.

#8015184
Lesenswert?

Aber:

Unlike in C++, inline assembly is treated as an extension in C. It is conditionally supported and implementation defined, meaning that it may not be present and, even when provided by the implementation, it does not have a fixed meaning.

Das ist also ein "kann", kein "muss". Und wenn man weiterliest, stößt man schnell auf Einschränkungen, da werden konkrete verbreitete Compiler genannt, die /gar keinen/ Inline-Assembler unterstützen.

Beitrag #8015296 wurde von einem Moderator gelöscht.
Persönliche Seite #8015467
Lesenswert?

Rick schrieb:

Und asm ist in allen C-Standards seit C89/C90 vertreten: https://en.cppreference.com/w/c/language/asm.html

Das bezieht sich aber ausschließlich auf die Syntax asm("text") und nicht auf eine irgendwie festgelegte Semantik. Die natürlich auch nicht vom C/C++ Standard kommen kann.

asm("text") ist auch relativ nutzlos weil keine Operanden angegeben oder Seiteneffekte beschrieben werden können, firmal existiert also noch nicht einmal eine GCC Memory Barrier wie asm("" ::: "memory").

Antwort schreiben

Bitte melde dich an, um einen Beitrag zu schreiben.

oder

Mit Google-Account einloggen

Die Registrierung ist kostenlos und dauert nur eine Minute.

Jetzt registrieren