Mit volatile wirst Du sicher erreichen können, dass der Compiler Zugriffe auf die Variablen nicht in der Reihenfolge ändert. Wenn Deine Hardware zusätzlich memory barrier braucht, damit dies sicher gestellt ist, wird der Compiler diese sicher nicht einfügen.
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.
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.
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).
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.
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.
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.
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.
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.
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.
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.
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
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.
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.
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...
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.
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.
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?
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.
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.
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).
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?
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.
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.
Beide "Threads" spielen dann Ping-Pong mit "flag" als binärem
Token. O
Threads sind ein anderes Thema. Du hast eine Threading library (z.B. pthread) und die legt fest, was zu tun ist, damit eine Änderung eines threads für einen anderen thread sichtbar wird.
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
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?
... 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?:
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.
Aber das obige Beispiel greift aus foo nicht auf Hardwareregister zu. counter ist eine normale int-Variable.
Ohne volatile könnte der
Compiler while(!f) doch zur Endlosschleife machen, oder?:
Das kommt darauf an, was in dem while steht. Wenn da nämlich Funktionsaufrufe drinnen stehen deren Inhalt der Compiler nicht kennt, könnte es ja sein, das diese "f" verändern. Dann (f ist hier nicht static...)
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.
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.
ist doch nicht nur der Compiler, da wird doch das zugehörige RTOS im Spiel sein. Da würde ich mal in die Doku schauen, was das so für die Aufgabenstellung im Angebot hat.
„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.
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.
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.
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.
"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.
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.
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.
Wenn flag volatile ist, muss er jeden Schreibzugriff machen.
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!
"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.
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.
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.
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.
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).
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.
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)
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.
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.
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:
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.
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.
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.
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.
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.
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.
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.
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").