WRITE_ONCE in Linux-Kernel-Listen

WRITE_ONCE in Linux-Kernel-Listen


Ich lese die Linux-Kernel-Implementierung der doppelt verknüpften Liste. Ich verstehe die Verwendung des Makros 08 nicht . Es ist in der compiler.h wie folgt definiert:


#define WRITE_ONCE(x, val) x=(val)

Es wird sieben Mal in der Datei verwendet, z. B.


static inline void __list_add(struct list_head *new,
struct list_head *prev,
struct list_head *next)
{
next->prev = new;
new->next = next;
new->prev = prev;
WRITE_ONCE(prev->next, new);
}

Ich habe gelesen, dass es zur Vermeidung von Rennbedingungen verwendet wird.


Ich habe zwei Fragen:

1/ Ich dachte, Makros würden zur Kompilierzeit durch Code ersetzt. Wie unterscheidet sich dieser Code vom folgenden? Wie kann dieses Makro Rennbedingungen vermeiden?


static inline void __list_add(struct list_head *new,
struct list_head *prev,
struct list_head *next)
{
next->prev = new;
new->next = next;
new->prev = prev;
prev->next = new;
}

2/ Woher weiß ich, wann wir es verwenden sollten? Beispielsweise wird es für 15 verwendet aber nicht für 25 :


static inline void __list_splice(const struct list_head *list,
struct list_head *prev,
struct list_head *next)
{
struct list_head *first = list->next;
struct list_head *last = list->prev;
first->prev = prev;
prev->next = first;
last->next = next;
next->prev = last;
}

bearbeiten:

Hier ist eine Commit-Nachricht bezüglich dieser Datei und 33 , aber es hilft mir nichts zu verstehen...



Antworten:


Die erste Definition, auf die Sie sich beziehen, ist Teil des Kernel-Lock-Validators, auch bekannt als "lockdep". 40 (und andere) brauchen keine besondere Behandlung, aber der Grund dafür ist Gegenstand einer anderen Frage.


Die relevante Definition wäre hier, und ein sehr knapper Kommentar gibt ihren Zweck wie folgt an:



Aber was bedeuten diese Worte?



Das Problem


Das Problem ist eigentlich Plural:



  1. Read/Write "Tearing":Ersetzen eines einzelnen Speicherzugriffs durch viele kleinere. GCC kann (und tut es!) in bestimmten Situationen etwas wie 55 ersetzen mit zwei 16-Bit-Anweisungen zum sofortigen Speichern - anstatt vermutlich die Konstante in ein Register zu stellen und dann einen Speicherzugriff und so weiter. 69 würde es uns ermöglichen, GCC zu sagen:"Tu das nicht", etwa so:72



  2. C-Compiler garantieren nicht mehr, dass ein Wortzugriff atomar ist. Jedes Programm, das nicht Race-free ist, kann mit spektakulären Ergebnissen falsch kompiliert werden. Nicht nur das, ein Compiler kann sich entscheiden, nicht Halten Sie bestimmte Werte in Registern innerhalb einer Schleife, was zu mehreren Referenzen führt, die Code wie diesen durcheinander bringen können:





for(;;) {
owner = lock->owner;
if (owner && !mutex_spin_on_owner(lock, owner))
break;
/* ... */
}


  1. Ohne „Tagging“-Zugriffe auf Shared Memory können wir nicht solche unbeabsichtigten Zugriffe automatisch erkennen. Automatisierte Tools, die versuchen, solche Fehler zu finden, können sie nicht von absichtlich rassigen Zugriffen unterscheiden.



Die Lösung


Wir beginnen mit der Feststellung, dass der Linux-Kernel mit GCC erstellt werden muss. Daher müssen wir uns bei der Lösung nur um einen Compiler kümmern, und wir können seine Dokumentation als einzige Anleitung verwenden.


Für eine generische Lösung müssen wir Speicherzugriffe aller Größen handhaben. Wir haben alle Arten von spezifischen Breiten und alles andere. Wir bemerken auch, dass wir Speicherzugriffe, die sich bereits in kritischen Abschnitten befinden, nicht speziell markieren müssen (warum nicht? ).


Für Größen von 1, 2, 4 und 8 Bytes gibt es entsprechende Typen und 85 verbietet GCC insbesondere, die Optimierung anzuwenden, auf die wir in (1) verwiesen haben, sowie sich um andere Fälle zu kümmern (letzter Aufzählungspunkt unter "COMPILER BARRIERS"). Es verbietet GCC auch, die Schleife in (2) falsch zu kompilieren, da dies den 91 verschieben würde Zugriff über einen Sequenzpunkt, und das ist vom C-Standard nicht erlaubt. Linux verwendet einen sogenannten "flüchtigen Zugriff" (siehe unten), anstatt ein Objekt als flüchtig zu markieren. Wir könnten Lösen Sie unser Problem, indem Sie das spezifische Objekt als 104 markieren , aber das ist (fast?) nie eine gute Wahl. Es gibt viele Gründe, warum es schädlich sein könnte.


So wird im Kernel ein flüchtiger (Schreib-)Zugriff für einen 8-Bit breiten Typ implementiert:



*(volatile __u8_alias_t *) p = *(__u8_alias_t *) res;

Angenommen, wir wüssten es nicht genau was 112 tut - und das herauszufinden ist nicht einfach! (Siehe #5) - Eine andere Möglichkeit, dies zu erreichen, wäre, Speicherbarrieren zu platzieren:Dies ist genau das, was Linux tut, falls die Größe etwas anderes als 1,2,4 oder 8 ist, indem es auf 122 und Platzieren von Speicherbarrieren vor und nach dem Anruf. Speicherbarrieren lösen Problem (2) ebenfalls leicht, ziehen jedoch große Leistungseinbußen nach sich.


Ich hoffe, ich habe einen Überblick gegeben, ohne mich mit Interpretationen des C-Standards zu befassen, aber wenn Sie möchten, könnte ich mir die Zeit dafür nehmen.