Ich habe 2 Fragen zu folgendem (sinnlosen) Demonstrationscode für in
**C99!**
1
intflag,counter;
2
3
interruptfoo(void)
4
{
5
if(!flag){/* challenge a: counter muss sicher for flag gesetzt werden */
6
counter++;
7
flag=1;
8
}
9
}
10
11
intmain(void)
12
{
13
for(;;){
14
if(flag){/* challenge b: Verwendung von counter bevor flag gelöscht wird */
15
(Verwendungvoncounter);
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)
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.
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).
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.
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
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.
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.
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
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.
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.
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?
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.
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.
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.
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.
Du hast das Problem nicht verstanden.
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...
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.
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?
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
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).
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?
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.
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.
Bruno V. schrieb:> 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.
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
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?:
Falk B. schrieb:> 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.
Bruno V. schrieb:> 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...)
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
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.
Bruno V. schrieb:> Windriver
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.
Oliver
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.
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.
Udo K. schrieb:> da die externe Funktion doSomething() die flag Variable ja> ändern könnte
Wenn der Compiler die Übersetzungseinheiten nur vorkompiliert und in der
eigentlichen Stufe alle zusammen betrachtet, kann das ins Auge gehen.
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.
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.
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.
(prx) A. K. schrieb:> Wenn der Compiler die Übersetzungseinheiten nur vorkompiliert und in der> eigentlichen Stufe alle zusammen betrachtet, kann das ins Auge gehen.
Ja das geht ins Auge. Ist in C nicht notwendig und führt in die
Debugging Hölle.
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.
Udo K. schrieb:> 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.
Udo K. schrieb:> (prx) A. K. schrieb:>> Wenn der Compiler die Übersetzungseinheiten nur vorkompiliert und in der>> eigentlichen Stufe alle zusammen betrachtet, kann das ins Auge gehen.>> 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.
Oliver
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!
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.
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.
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.
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.
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).
(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.
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)
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ürfenDaniel 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.
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.
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
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.
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.
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
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.
Oliver S. schrieb:> 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.
Jepp. Und asm ist in allen C-Standards seit C89/C90 vertreten:
https://en.cppreference.com/w/c/language/asm.html
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.
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").