Implementieren kritischer Abschnitte in ARM Cortex A9

15

Ich portiere einen älteren Code von einem ARM926-Kern auf CortexA9. Dieser Code ist barmetallisch und enthält keine benutzerdefinierten Betriebssystem- oder Standardbibliotheken. Ich habe einen Fehler, der anscheinend mit einer Rennsituation zusammenhängt, die durch eine kritische Unterteilung des Codes verhindert werden sollte.

Ich möchte ein Feedback zu meiner Vorgehensweise, um festzustellen, ob meine kritischen Abschnitte für diese CPU möglicherweise nicht korrekt implementiert sind. Ich benutze GCC. Ich vermute, es liegt ein subtiler Fehler vor.

Gibt es auch eine OpenSource-Bibliothek, die diese Arten von Grundelementen für ARM enthält (oder sogar eine gute, leichte Spinlock- / Semephore-Bibliothek)?

#define ARM_INT_KEY_TYPE            unsigned int
#define ARM_INT_LOCK(key_)   \
asm volatile(\
    "mrs %[key], cpsr\n\t"\
    "orr r1, %[key], #0xC0\n\t"\
    "msr cpsr_c, r1\n\t" : [key]"=r"(key_) :: "r1", "cc" );

#define ARM_INT_UNLOCK(key_) asm volatile ("MSR cpsr_c,%0" : : "r" (key_))

Der Code wird wie folgt verwendet:

/* lock interrupts */
ARM_INT_KEY_TYPE key;
ARM_INT_LOCK(key);

<access registers, shared globals, etc...>

ARM_INT_UNLOCK(key);

Die Idee des "Schlüssels" besteht darin, geschachtelte kritische Abschnitte zuzulassen, die am Anfang und Ende von Funktionen verwendet werden, um wiedereintrittsfähige Funktionen zu erstellen.

Vielen Dank!

CodePoet
quelle
1
bitte Bezug auf infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.dht0008a/... tut es nicht btw in eingebettet nh. mache es zu einer Funktion wie es der Artikel tut.
Jason Hu
Ich weiß nichts über ARM, aber ich würde erwarten, dass Sie für Mutex (oder jede Kreuzthread- oder Kreuzprozess-Synchronisierungsfunktion) den "Speicher" -Clobber verwenden sollten, um sicherzustellen, dass a) alle derzeit in Registern zwischengespeicherten Speicherwerte gelöscht werden Zurück in den Speicher, bevor der ASM ausgeführt wird. b) Alle Werte im Speicher, auf die nach dem ASM zugegriffen wird, werden neu geladen. Beachten Sie, dass das Ausführen eines Anrufs (wie von HuStmpHrrr empfohlen) diesen Clobber implizit für Sie ausführen sollte.
Auch wenn ich immer noch kein ARM spreche, sehen Ihre Einschränkungen für "key_" nicht korrekt aus. Da Sie sagen, dass dies für die erneute Eingabe verwendet werden soll, erscheint es verdächtig, es im Schloss als "= r" zu deklarieren. '=' bedeutet, dass Sie beabsichtigen, es zu überschreiben, und der vorhandene Wert ist unwichtig. Es ist wahrscheinlicher, dass Sie beabsichtigt haben, "+" zu verwenden, um Ihre Absicht anzuzeigen, den vorhandenen Wert zu aktualisieren. Und noch einmal zum Entsperren: Wenn Sie es als Eingabe auflisten, wird gcc mitgeteilt, dass Sie nicht beabsichtigen, es zu ändern, aber wenn ich mich nicht irre, tun Sie es (ändern Sie es). Ich denke, dies sollte auch als "+" - Ausgabe aufgeführt werden.
1
+1 für die Codierung in der Baugruppe für einen so hochspezifizierten Kern. Wie auch immer, könnte dies mit Privilegien zusammenhängen?
Dzarda
Ich bin mir ziemlich sicher, dass Sie es verwenden ldrexund strexrichtig machen müssen. Auf der folgenden Webseite erfahren Sie, wie Sie einen Spinlock verwenden ldrexund streximplementieren.

Antworten:

14

Der schwierigste Teil bei der Behandlung eines kritischen Abschnitts ohne Betriebssystem besteht darin, nicht den Mutex zu erstellen, sondern herauszufinden, was passieren soll, wenn Code eine Ressource verwenden möchte, die derzeit nicht verfügbar ist. Die Anweisungen load-exclusive und conditional-store-exclusive machen es ziemlich einfach, eine "Swap" -Funktion zu erstellen, die bei einem Zeiger auf eine Ganzzahl einen neuen Wert atomar speichert, aber das zurückgibt, was die Ganzzahl, auf die gezeigt wurde, enthielt:

int32_t atomic_swap(int32_t *dest, int32_t new_value)
{
  int32_t old_value;
  do
  {
    old_value = __LDREXW(&dest);
  } while(__STREXW(new_value,&dest);
  return old_value;
}

Bei einer Funktion wie der obigen kann man leicht einen Mutex über etwas wie eingeben

if (atomic_swap(&mutex, 1)==0)
{
   ... do stuff in mutex ... ;
   mutex = 0; // Leave mutex
}
else
{ 
  ... couldn't get mutex...
}

In Ermangelung eines Betriebssystems liegt die Hauptschwierigkeit häufig im Code "konnte kein Mutex erhalten". Wenn eine Unterbrechung auftritt, während eine durch Mutex geschützte Ressource belegt ist, muss möglicherweise der Unterbrechungsbehandlungscode ein Flag setzen und einige Informationen speichern, um anzugeben, was er tun möchte, und anschließend über einen main-ähnlichen Code verfügen, der den Code erhält Mutex-Prüfung, wann immer der Mutex freigegeben wird, um festzustellen, ob ein Interrupt etwas tun wollte, während der Mutex gehalten wurde, und in diesem Fall die Aktion für den Interrupt auszuführen.

Obwohl es möglich ist, Probleme mit Interrupts zu vermeiden, die durch Mutex geschützte Ressourcen verwenden möchten, indem Interrupts einfach deaktiviert werden (und in der Tat kann das Deaktivieren von Interrupts die Notwendigkeit anderer Mutex-Typen beseitigen), ist es im Allgemeinen wünschenswert, das Deaktivieren von Interrupts nicht länger als nötig zu vermeiden.

Ein nützlicher Kompromiss kann darin bestehen, ein Flag wie oben beschrieben zu verwenden, aber den Hauptzeilencode, der die Mutex-Deaktivierungs-Interrupts auslösen soll, zu verwenden und das oben genannte Flag unmittelbar vorher zu überprüfen (Interrupts nach dem Auslösen des Mutex wieder zu aktivieren). Ein solcher Ansatz erfordert nicht, dass Interrupts sehr lange deaktiviert bleiben, schützt jedoch vor der Möglichkeit, dass zwischen dem Zeitpunkt, zu dem das Flag angezeigt wird, und dem Zeitpunkt, zu dem das Flag angezeigt wird, die Gefahr besteht, dass der Hauptzeilencode das Flag des Interrupts nach dem Freigeben des Mutex testet Handelt es dagegen, wird es möglicherweise von einem anderen Code beeinträchtigt, der den Mutex erfasst und freigibt und auf das Interrupt-Flag einwirkt. Wenn der Hauptcode das Interrupt-Flag nach dem Freigeben des Mutex nicht testet,

In jedem Fall ist es am wichtigsten, ein Mittel zu haben, mit dem Code, der versucht, eine durch Mutex geschützte Ressource zu verwenden, wenn sie nicht verfügbar ist, den Versuch wiederholen kann, sobald die Ressource freigegeben ist.

Superkatze
quelle
7

Dies ist eine schwierige Methode, um kritische Abschnitte zu erstellen. Interrupts deaktivieren. Es funktioniert möglicherweise nicht, wenn Ihr System Datenfehler hat / behandelt. Dies erhöht auch die Interrupt-Latenz. Die Linux-Datei irqflags.h enthält einige Makros, die damit umgehen. Die cpsieund cpsidAnweisungen können nützlich sein; Sie speichern jedoch keinen Status und ermöglichen keine Verschachtelung. cpsbenutzt kein Register.

Für die Cortex-A- Serie ldrex/strexsind sie effizienter und können einen Mutex für den kritischen Abschnitt bilden, oder sie können mit sperrfreien Algorithmen verwendet werden, um den kritischen Abschnitt zu entfernen.

In gewissem Sinne ldrex/strexscheint das ein ARMv5 zu sein swp. Ihre praktische Umsetzung ist jedoch sehr viel komplexer. Sie benötigen einen funktionierenden Cache und der Zielspeicher ldrex/strexmuss sich im Cache befinden. Die ARM-Dokumentation zum ldrex/strexist ziemlich nebulös, da Mechanismen auf Nicht-Cortex-A-CPUs funktionieren sollen. Für den Cortex-A ist der Mechanismus zum Synchronisieren des lokalen CPU-Cache mit anderen CPUs derselbe, der zum Implementieren der ldrex/strexAnweisungen verwendet wird. Bei der Cortex-A-Serie entspricht das Reservegranual (Größe des ldrex/strexreservierten Speichers) einer Cache-Zeile. Sie müssen den Speicher auch an der Cache-Zeile ausrichten, wenn Sie mehrere Werte ändern möchten, z. B. bei einer doppelt verknüpften Liste.

Ich vermute, es liegt ein subtiler Fehler vor.

mrs %[key], cpsr
orr r1, %[key], #0xC0  ; context switch here?
msr cpsr_c, r1

Sie müssen sicherstellen, dass die Sequenz niemals vorab gelesen werden kann . Andernfalls erhalten Sie möglicherweise zwei Schlüsselvariablen mit aktivierten Interrupts, und die Freigabe der Sperre ist falsch. Sie können die swpAnweisung mit dem Schlüsselspeicher verwenden, um die Konsistenz auf dem ARMv5 sicherzustellen. Diese Anweisung wird jedoch auf dem Cortex-A nicht mehr empfohlen, ldrex/strexda sie für Systeme mit mehreren CPUs besser funktioniert.

All dies hängt von der Art der Planung Ihres Systems ab. Es hört sich so an, als hätten Sie nur Hauptleitungen und Interrupts. Die Grundelemente für kritische Abschnitte müssen häufig über einige Hooks für den Scheduler verfügen, je nachdem, auf welcher Ebene (System / Benutzerbereich / usw.) der kritische Abschnitt ausgeführt werden soll.

Gibt es auch eine OpenSource-Bibliothek, die diese Arten von Grundelementen für ARM enthält (oder sogar eine gute, leichte Spinlock- / Semephore-Bibliothek)?

Dies ist auf tragbare Weise schwierig zu schreiben. Das heißt, solche Bibliotheken können für bestimmte Versionen von ARM-CPUs und für bestimmte Betriebssysteme existieren.

Kunstloser Lärm
quelle
2

Ich sehe mehrere mögliche Probleme mit diesen kritischen Abschnitten. Es gibt Vorbehalte und Lösungen für all diese Probleme, aber als Zusammenfassung:

  • Es hindert den Compiler nicht daran, Code aus Optimierungs- oder anderen zufälligen Gründen über diese Makros zu verschieben.
  • Sie speichern und stellen einige Teile des Prozessorstatus wieder her, den der Compiler erwartet (sofern nicht anders angegeben).
  • Es hindert nichts daran, dass ein Interrupt in der Mitte der Sequenz auftritt und den Status zwischen dem Zeitpunkt des Lesens und dem Zeitpunkt des Schreibens ändert.

Zunächst benötigen Sie auf jeden Fall einige Compiler-Speicherbarrieren . GCC implementiert diese als Clobbers . Im Grunde ist dies eine Möglichkeit, dem Compiler mitzuteilen, "Nein, Sie können Speicherzugriffe nicht über diese Inline-Assembly verschieben, da dies das Ergebnis der Speicherzugriffe beeinflussen kann." Insbesondere benötigen Sie sowohl für das Start- als auch für das Endmakro sowohl Clobbers "memory"als auch "cc"Clobbers. Dadurch wird verhindert, dass andere Dinge (wie Funktionsaufrufe) auch relativ zur Inline-Assembly neu angeordnet werden, da der Compiler weiß, dass sie möglicherweise über Speicherzugriffe verfügen. Ich habe gesehen, dass GCC for ARM den Status in den Zustandscoderegistern in der Inline-Assembly mit "memory"Clobbern hält, also brauchst du den "cc"Clobber definitiv .

Zweitens speichern und stellen diese kritischen Abschnitte viel mehr wieder her als nur, ob Interrupts aktiviert sind. Insbesondere wird der größte Teil des CPSR (Current Program Status Register) gespeichert und wiederhergestellt (der Link bezieht sich auf Cortex-R4, da ich kein nettes Diagramm für einen A9 gefunden habe, es aber identisch sein sollte). Es gibt subtile Einschränkungen, um welche Teile des Staates tatsächlich geändert werden können, aber es ist hier mehr als notwendig.

Dazu gehören unter anderem die Bedingungscodes (in denen die Ergebnisse von Anweisungen wie cmpgespeichert werden, damit nachfolgende bedingte Anweisungen auf das Ergebnis einwirken können). Der Compiler wird dadurch definitiv verwirrt. Dies ist mit dem "cc"oben erwähnten Clobber leicht lösbar . Dies führt jedoch dazu, dass Code jedes Mal fehlschlägt, sodass es nicht so klingt, als würden Sie Probleme damit sehen. Etwas wie eine tickende Zeitbombe, könnte der Compiler in diesem modifizierenden zufälligen anderen Code dazu führen, dass er etwas anderes macht, was dadurch kaputt geht.

Dadurch wird auch versucht, die IT-Bits zu speichern / wiederherzustellen, die zur Implementierung der Thumb-bedingten Ausführung verwendet werden . Beachten Sie, dass dies keine Rolle spielt, wenn Sie niemals Thumb-Code ausführen. Ich habe nie herausgefunden, wie die Inline-Assembly von GCC mit den IT-Bits umgeht, abgesehen von der Schlussfolgerung, dass dies nicht der Fall ist. Der Compiler darf also niemals eine Inline-Assembly in einen IT-Block einfügen und erwartet immer, dass die Assembly außerhalb eines IT-Blocks endet. Ich habe noch nie gesehen, dass GCC Code generiert hat, der gegen diese Annahmen verstößt, und ich habe einige recht komplizierte Inline-Assemblierungen mit intensiver Optimierung durchgeführt, daher bin ich mir ziemlich sicher, dass sie zutreffen. Das heißt, es wird wahrscheinlich nicht wirklich versucht, die IT-Bits zu ändern. In diesem Fall ist alles in Ordnung. Der Versuch, diese Bits zu ändern, wird als "architektonisch unvorhersehbar" eingestuft.Es könnte also alle Arten von schlechten Dingen tun, wird aber wahrscheinlich überhaupt nichts tun.

Die letzte Kategorie von Bits, die gespeichert / wiederhergestellt werden (abgesehen von denjenigen, die Interrupts tatsächlich deaktivieren), sind die Modusbits. Diese werden sich wahrscheinlich nicht ändern, daher spielt es wahrscheinlich keine Rolle, aber wenn Sie einen Code haben, der absichtlich den Modus ändert, können diese Interrupt-Abschnitte Probleme verursachen. Der Wechsel zwischen privilegiertem und Benutzermodus ist der einzige Fall, den ich erwarten würde.

Drittens gibt es nichts , einen Interrupt zu verhindern , dass zu ändern andere Teile CPSR zwischen der MRSund MSRin ARM_INT_LOCK. Solche Änderungen können überschrieben werden. In den meisten vernünftigen Systemen ändern asynchrone Interrupts nicht den Status des Codes, den sie unterbrechen (einschließlich CPSR). Wenn dies der Fall ist, ist es sehr schwierig zu überlegen, welcher Code verwendet wird. Es ist jedoch möglich (das Ändern des FIQ-Deaktivierungsbits erscheint mir am wahrscheinlichsten), daher sollten Sie überlegen, ob Ihr System dies tut.

So würde ich diese in einer Weise implementieren, die alle potenziellen Probleme angeht, auf die ich hingewiesen habe:

#define ARM_INT_KEY_TYPE            unsigned int
#define ARM_INT_LOCK(key_)   \
asm volatile(\
    "mrs %[key], cpsr\n\t"\
    "ands %[key], %[key], #0xC0\n\t"\
    "cpsid if\n\t" : [key]"=r"(key_) :: "memory", "cc" );
#define ARM_INT_UNLOCK(key_) asm volatile (\
    "tst %[key], #0x40\n\t"\
    "beq 0f\n\t"\
    "cpsie f\n\t"\
    "0: tst %[key], #0x80\n\t"\
    "beq 1f\n\t"\
    "cpsie i\n\t"
    "1:\n\t" :: [key]"r" (key_) : "memory", "cc")

Stellen Sie sicher , mit zu kompilieren , -mcpu=cortex-a9weil zumindest einige GCC - Versionen (wie bei mir) standardmäßig auf einem älteren ARM CPU , die nicht unterstützt cpsieund cpsid.

Ich habe andsanstelle von nur andin verwendet, ARM_INT_LOCKdamit es eine 16-Bit-Anweisung ist, wenn dies in Thumb-Code verwendet wird. Der "cc"Clobber ist sowieso notwendig, es ist also streng genommen ein Vorteil in Bezug auf Leistung / Codegröße.

0und 1sind lokale Bezeichnungen als Referenz.

Diese sollten genauso verwendbar sein wie Ihre Versionen. Das ARM_INT_LOCKist genauso schnell / klein wie dein Original. Unglücklicherweise konnte ich mir keine Möglichkeit einfallen lassen, mit ARM_INT_UNLOCKso wenigen Anweisungen sicher zu sein.

Wenn Ihr System Einschränkungen hat, wenn IRQs und FIQs deaktiviert sind, kann dies vereinfacht werden. Wenn sie zum Beispiel immer zusammen deaktiviert sind, können Sie eins cbz+ cpsie ifwie folgt kombinieren :

#define ARM_INT_UNLOCK(key_) asm volatile (\
    "cbz %[key], 0f\n\t"\
    "cpsie if\n\t"\
    "0:\n\t" :: [key]"r" (key_) : "memory", "cc")

Wenn Sie sich überhaupt nicht für FIQs interessieren, können Sie sie auch ganz deaktivieren oder aktivieren.

Wenn Sie wissen, dass nichts anderes jemals eines der anderen Statusbits in CPSR zwischen dem Sperren und Entsperren ändert, können Sie auch mit etwas weitermachen, das Ihrem ursprünglichen Code sehr ähnlich ist, außer mit beiden "memory"und "cc"Clobbers in beiden ARM_INT_LOCKundARM_INT_UNLOCK

Brian Silverman
quelle