So implementieren Sie kritische Abschnitte auf ARM Cortex A9

Ich portiere Legacy-Code von einem ARM926-Core auf CortexA9. Dieser Code ist Bare-Metal und enthält kein Betriebssystem oder Standardbibliotheken, die alle benutzerdefiniert sind. Ich habe einen Fehler, der anscheinend mit einer Racebedingung zusammenhängt, die durch kritische Abschnitte des Codes verhindert werden sollte.

Ich möchte ein Feedback zu meinem Ansatz, um zu sehen, ob meine kritischen Abschnitte für diese CPU möglicherweise nicht korrekt implementiert sind. Ich verwende GCC. Ich vermute einen subtilen Fehler.

Gibt es auch eine Open-Source-Bibliothek, die diese Art von Primitiven 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, verschachtelte kritische Abschnitte zuzulassen, und diese werden am Anfang und am Ende von Funktionen verwendet, um wiedereintrittsfähige Funktionen zu erstellen.

Vielen Dank!

Bitte verweisen Sie auf infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.dht0008a/… tun Sie es übrigens nicht in eingebettetem Asm. Machen Sie es zu einer Funktion wie der Artikel.
Ich weiß nichts über ARM, aber ich würde erwarten, dass Sie für Mutex (oder jede Cross-Thread- oder Cross-Process-Sync-Funktion) den "Speicher"-Clobber verwenden sollten, um sicherzustellen, dass a) alle derzeit in Registern zwischengespeicherten Speicherwerte geleert werden zurück in den Speicher, bevor der asm ausgeführt wird, und b) alle Werte im Speicher, auf die zugegriffen wird, nachdem der asm neu geladen wird. Beachten Sie, dass das Ausführen eines Anrufs (wie von HuStmpHrrr empfohlen) implizit diese Arbeit 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 den Wiedereintritt verwendet werden soll, erscheint es verdächtig, es als "= r" in der Sperre zu deklarieren. '=' bedeutet, dass Sie beabsichtigen, es zu überschreiben, und der vorhandene Wert ist unwichtig. Es scheint wahrscheinlicher, dass Sie '+' verwenden wollten, um anzuzeigen, dass Sie den vorhandenen Wert aktualisieren möchten. Und wieder zum Entsperren sagt das Auflisten als Eingabe gcc, dass Sie nicht beabsichtigen, es zu ändern, aber wenn ich mich nicht irre, tun Sie es (ändern). Ich vermute, dies sollte auch als '+'-Ausgang aufgeführt werden.
+1 für die Codierung in der Montage für einen so hochspezifizierten Kern. Wie auch immer, könnte dies mit den Privilegmodi zusammenhängen?
Ich bin mir ziemlich sicher, dass Sie es richtig verwenden ldrexund verwenden müssen. strexHier ist eine Webseite , die Ihnen zeigt, wie Sie ein Spinlock verwenden ldrexund implementieren.strex
Sind Sie auf einem einzigen Kern und wollen nur Schutz davor, sich selbst vorzubeugen? Andernfalls, wenn Sie versuchen, zwischen mehreren Kernen oder DMA-Peripheriegeräten zu synchronisieren, wird das Drehen von Interrupts überhaupt nicht funktionieren - Sie benötigen die oben genannten exklusiven Funktionen mit geeigneten Barrieren und einer sorgfältigen Überlegung über die Cache-Kohärenz.
Der Code sieht gut aus. Was lässt Sie glauben, dass diese 4 Zeilen an den Millionen von Code, die Sie portiert haben, schuld sind?
Scheint, als ob die Frage eher für SO als für EE geeignet ist
Können Sie Testfälle schreiben - zum Beispiel eine Aufgabe mit niedriger Priorität ausführen, die in einen Puffer schreibt, und dann eine Aufgabe mit höherer Priorität starten, die ihn unterbricht? Drehen Sie irgendwo eine Stecknadel um, wenn der Streit stattfindet? Entfernen Sie Ihren kritischen Abschnitt und beobachten Sie, wie die Dinge nach Süden gehen, und setzen Sie ihn dann wieder ein, um das Problem zu beheben?

Antworten (5)

Der schwierigste Teil beim Umgang mit einem kritischen Abschnitt ohne ein Betriebssystem besteht nicht darin, den Mutex tatsächlich zu erstellen, sondern herauszufinden, was passieren soll, wenn der Code eine Ressource verwenden möchte, die derzeit nicht verfügbar ist. Die ausschließlichen Lade- und Bedingungsspeicher-exklusiven Anweisungen machen es ziemlich einfach, eine „Swap“-Funktion zu erstellen, die bei einem Zeiger auf eine ganze Zahl atomar einen neuen Wert speichert, aber zurückgibt, was die ganze Zahl, auf die gezeigt wurde, enthalten hatte:

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 oft im Code „Mutex konnte nicht abgerufen werden“. Wenn ein Interrupt auftritt, wenn eine Mutex-geschützte Ressource beschäftigt ist, kann es erforderlich sein, dass der Interrupt-Behandlungscode ein Flag setzt und einige Informationen speichert, um anzugeben, was er tun wollte, und dann einen beliebigen main-ähnlichen Code zu haben, der die erfasst mutex prüft, wann immer es den Mutex freigeben wird, um zu sehen, ob ein Interrupt etwas tun wollte, während der Mutex gehalten wurde, und wenn ja, die Aktion im Namen des Interrupts auszuführen.

Obwohl es möglich ist, Probleme mit Interrupts zu vermeiden, die mutexgeschützte Ressourcen verwenden möchten, indem Sie einfach Interrupts deaktivieren (und tatsächlich kann das Deaktivieren von Interrupts die Notwendigkeit für jede andere Art von Mutex beseitigen), ist es im Allgemeinen wünschenswert, Interrupts nicht länger als nötig zu deaktivieren.

Ein nützlicher Kompromiss kann darin bestehen, ein Flag wie oben beschrieben zu verwenden, aber den Hauptleitungscode zu haben, der die Mutex-Deaktivierungs-Interrupts freigibt, und das oben genannte Flag kurz davor zu überprüfen (Interrupts nach dem Freigeben des Mutex wieder zu aktivieren). Ein solcher Ansatz erfordert nicht, dass Interrupts sehr lange deaktiviert bleiben, schützt aber vor der Möglichkeit, dass, wenn der Hauptcode das Flag des Interrupts nach dem Freigeben des Mutex testet, die Gefahr besteht, dass zwischen dem Zeitpunkt, an dem er das Flag sieht, und dem Zeitpunkt, an dem es angezeigt wird darauf einwirkt, könnte es durch anderen Code, der den Mutex erfasst und freigibt, unterbunden werden und auf das Interrupt-Flag einwirken; wenn der Hauptleitungscode 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 Mutex-geschützte Ressource zu verwenden, wenn sie nicht verfügbar ist, seinen Versuch wiederholen kann, sobald die Ressource freigegeben wird.

Dies ist eine schwerfällige Methode, um kritische Abschnitte zu bearbeiten. Interrupts deaktivieren. Es funktioniert möglicherweise nicht, wenn Ihr System Datenfehler aufweist/handhabt. Es erhöht auch die Interrupt-Latenz. Die Linux-irqflags.h hat einige Makros, die damit umgehen. Die Anweisungen cpsieund cpsidkönnen nützlich sein; Sie speichern jedoch keinen Zustand und lassen keine Verschachtelung zu. cpsverwendet kein Register.

Bei der Cortex-A- Serie ldrex/strexsind sie effizienter und können arbeiten, um einen Mutex für den kritischen Abschnitt zu bilden, oder sie können mit lock-freien Algorithmen verwendet werden, um den kritischen Abschnitt loszuwerden.

In gewisser Weise ldrex/strexwirken sie wie ein ARMv5 swp. In der Praxis sind sie jedoch wesentlich komplexer umzusetzen. Sie benötigen einen funktionierenden Cache und der Zielspeicher ldrex/strexmuss sich im Cache befinden. Die ARM-Dokumentation auf dem ldrex/strexist ziemlich nebulös, da sie möchten, dass Mechanismen auf Nicht-Cortex-A-CPUs funktionieren. Für den Cortex-A ist der Mechanismus, um den lokalen CPU-Cache mit anderen CPUs synchron zu halten, jedoch der gleiche, der zum Implementieren der ldrex/strexAnweisungen verwendet wird. Bei der Cortex-A-Serie ist das Reserve-Granual (Größe des ldrex/strexreservierten Speichers) dasselbe wie eine Cache-Zeile; Sie müssen den Speicher auch an der Cache-Zeile ausrichten, wenn Sie beabsichtigen, mehrere Werte zu ändern, z. B. bei einer doppelt verknüpften Liste.

Ich vermute einen subtilen Fehler.

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

Sie müssen sicherstellen, dass die Sequenz niemals vorweggenommen 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, aber diese Anweisung ist auf dem Cortex-A zugunsten von veraltet, ldrex/strexda sie für Systeme mit mehreren CPUs besser funktioniert.

All dies hängt davon ab, welche Art von Planung Ihr System hat. Es hört sich so an, als hätten Sie nur Hauptleitungen und Unterbrechungen. Sie benötigen häufig die Primitiven des kritischen Abschnitts , um einige Hooks zum Scheduler zu haben, je nachdem, mit welchen Ebenen (System/Benutzerbereich/usw.) Sie den kritischen Abschnitt verwenden möchten.

Gibt es auch eine Open-Source-Bibliothek, die diese Art von Primitiven für ARM enthält (oder sogar eine gute leichte Spinlock-/Semephore-Bibliothek)?

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

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

  • Nichts hindert den Compiler daran, Code über diese Makros zu verschieben, aus Optimierungsgründen oder aus anderen Gründen.
  • Sie speichern und stellen einige Teile des Prozessorzustands wieder her, den der Compiler von der Inline-Assemblierung erwarten lässt (sofern nicht anders angegeben).
  • Es gibt nichts, was verhindert, dass ein Interrupt mitten in der Sequenz auftritt und den Zustand zwischen dem Lesen und dem Schreiben ändert.

Zunächst einmal benötigen Sie auf jeden Fall einige Compiler-Speicherbarrieren . GCC implementiert diese als Clobber . Im Grunde ist dies eine Möglichkeit, dem Compiler mitzuteilen: "Nein, Sie können Speicherzugriffe nicht über dieses Stück Inline-Assembly verschieben, da dies das Ergebnis der Speicherzugriffe beeinflussen könnte." Insbesondere benötigen Sie sowohl für die Start- als auch für die Endmakros Both "memory"und "cc"Clobbers. Diese verhindern, dass auch andere Dinge (wie Funktionsaufrufe) relativ zur Inline-Assembly neu geordnet werden, da der Compiler weiß, dass sie möglicherweise Speicherzugriffe haben. Ich habe GCC für ARM-Haltezustand in Zustandscoderegistern über Inline-Assembly mit "memory"Clobbers gesehen, also brauchen Sie definitiv den "cc"Clobber.

Zweitens speichern und wiederherstellen diese kritischen Abschnitte viel mehr als nur, ob Interrupts aktiviert sind. Insbesondere speichern und stellen sie den größten Teil des CPSR (Current Program Status Register) wieder her (der Link gilt für Cortex-R4, da ich kein schönes Diagramm für einen A9 finden konnte, aber es sollte identisch sein). Es gibt subtile Einschränkungen , bei denen Teile des Zustands tatsächlich geändert werden können, aber hier ist es 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 reagieren können). Der Compiler wird dadurch definitiv verwirrt. Dies ist mit dem "cc"oben erwähnten Klobber leicht lösbar. Dies führt jedoch jedes Mal dazu, dass der Code fehlschlägt, sodass es sich nicht so anhört, als würden Sie Probleme sehen. Es ist jedoch eine Art tickende Zeitbombe, da das Ändern von zufälligem anderem Code dazu führen kann, dass der Compiler etwas anderes macht, was dadurch unterbrochen wird.

Dadurch wird auch versucht, die IT-Bits zu speichern/wiederherzustellen, die verwendet werden, um die bedingte Ausführung von Thumb zu implementieren . 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, außer zu dem Schluss, dass dies nicht der Fall ist, was bedeutet, dass der Compiler die Inline-Assembly niemals in einen IT-Block einfügen darf und immer erwartet, dass die Assembly außerhalb eines IT-Blocks endet. Ich habe noch nie gesehen, dass GCC Code generiert, der gegen diese Annahmen verstößt, und ich habe einige ziemlich komplizierte Inline-Assemblierungen mit starker Optimierung durchgeführt, daher bin ich mir ziemlich sicher, dass sie gelten. Das bedeutet, dass es wahrscheinlich nicht wirklich versuchen wird, die IT-Bits zu ändern, in diesem Fall ist alles in Ordnung. Der Versuch, diese Bits zu ändern, wird als „architektonisch unvorhersehbar“ eingestuft., also könnte es alle möglichen schlimmen Dinge tun, aber wahrscheinlich wird es überhaupt nichts tun.

Die letzte Kategorie von Bits, die gespeichert/wiederhergestellt werden (neben denen zum tatsächlichen Deaktivieren von Interrupts), sind die Modusbits. Diese werden sich wahrscheinlich nicht ändern, also wird es wahrscheinlich keine Rolle spielen, aber wenn Sie Code haben, der absichtlich den Modus ändert, könnten diese Interrupt-Abschnitte Probleme verursachen. Das Wechseln zwischen dem privilegierten und dem Benutzermodus ist der einzige Fall, in dem ich dies erwarten würde.

Drittens hindert nichts einen Interrupt daran, andere Teile von CPSR zwischen MRSund MSRin zu ändern ARM_INT_LOCK. Solche Änderungen könnten überschrieben werden. In den meisten vernünftigen Systemen ändern asynchrone Interrupts nicht den Zustand des Codes, den sie unterbrechen (einschließlich CPSR). Wenn dies der Fall ist, wird es sehr schwierig, darüber nachzudenken, was der Code tun 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 so implementieren, dass alle potenziellen Probleme, auf die ich hingewiesen habe, behoben werden:

#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, dass Sie mit kompilieren, -mcpu=cortex-a9da zumindest einige GCC-Versionen (wie meine) standardmäßig eine ältere ARM-CPU verwenden, die und nicht cpsieunterstützt cpsid.

Ich habe andsstatt nur andin verwendet ARM_INT_LOCK, also ist es eine 16-Bit-Anweisung, wenn dies im Thumb-Code verwendet wird. Der "cc"Clobber ist sowieso notwendig, also ist es ausschließlich ein Vorteil für Leistung/Codegröße.

0und 1sind lokale Labels , als Referenz.

Diese sollten auf die gleiche Weise wie Ihre Versionen verwendet werden können. Der ARM_INT_LOCKist genauso schnell/klein wie Ihr Original. Leider konnte ich nicht mit ARM_INT_UNLOCKannähernd so wenigen Anweisungen einen sicheren Weg finden.

Wenn Ihr System Einschränkungen hat, wann IRQs und FIQs deaktiviert sind, könnte dies vereinfacht werden. Wenn sie beispielsweise immer zusammen deaktiviert sind, könnten Sie sie wie folgt zu einem cbz+ kombinieren cpsie if:

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

Alternativ, wenn Sie sich überhaupt nicht für FIQs interessieren, ist es ähnlich, sie einfach vollständig zu aktivieren/deaktivieren.

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

Du sagtest,

Ich portiere Legacy-Code von einem ARM926-Core auf CortexA9.

Der SP7021 ist eine interessante Lösung, die Sie nach Möglichkeit in Betracht ziehen sollten, da er 4 ARM7-Kerne enthält, plus einen ARM926-Kern und auch einen 8051-Kern. Diesen Code nicht portieren zu müssen, wäre ein großer Gewinn, und es gibt einen Grund, warum dieser Chip mit 4 Kernen zum Hosten von Linux entwickelt wurde, aber einem völlig separaten Kern für den Echtzeitaspekt – wenn Sie den Echtzeitkern herunterladen Wie können Sie mit einem Betriebssystem sicherstellen, dass Ihr Echtzeitcode nicht kompromittiert wird? Dieses Zeug ist so schwer richtig zu machen, dass ich denke, die wahre Lösung besteht darin, Wege zu finden, wie man es umgeht, wie die Verwendung separater Chips. Du hast auch gesagt,

Ich vermute einen subtilen Fehler.

Wie Jack Ganssle in seinem Buch "The Art of Designing Embedded Systems" , Kapitel 3, "Stop Writing Big Programs", Abschnitt "Partition with CPUs", pg. 41,

"Es ist normalerweise billiger, mehr CPUs hinzuzufügen, nur um die Software zu vereinfachen."

Denken Sie also bitte daran, dem Echtzeitaspekt einen Kern zu widmen, wie es der SP7021 gebaut hat. (Vollständige Offenlegung – ich habe weder eine Beziehung zu noch ein Interesse an dem Produkt oder dem Unternehmen).

Hier ist ein Gedanke: Nur weil Ihr Unternehmen es „Legacy“ nennt, lässt es die Verwendung dieses Wortes automatisch alt und verkrustet erscheinen, und dann müssen wir es natürlich unbedingt ersetzen – nicht unbedingt! Ausgetesteter Code ist Gold wert – so viel kostet es, ihn zu entwickeln, zu warten und zu aktualisieren. Und wenn ein neuer Mikrocontroller denselben "klassischen" Kern "nur für Echtzeit" eingebaut hat, warum sollte er dann nicht verwendet werden?

Was folgt, sind meine Meinungen darüber, wie ich diese Art von Code geschrieben sehen möchte, obwohl ich ein Senior Developer bin, der (noch) nicht wirklich viel von Echtzeit-Zeug gemacht hat.

Ich mag kooperatives Multitasking, weil es vorhersehbar ist (was bedeutet, dass es möglich ist, Fehler zu beheben).

Ich mag die Koordinationssprache namens Linda , in der zu erledigende Aufgaben gepostet werden und Worker-Prozesse ein Arbeitselement "auschecken", um daran zu arbeiten, und dann die Antwort posten.

Ich mag die Art und Weise, wie Erlang es macht (und ich mag ihre hohe Verfügbarkeit):

  • Alles ist ein Prozess.
  • Prozesse sind stark isoliert.
  • Die Erstellung und Zerstörung von Prozessen ist ein einfacher Vorgang.
  • Die Nachrichtenübermittlung ist die einzige Möglichkeit für Prozesse, miteinander zu interagieren.
  • Prozesse haben eindeutige Namen.
  • Wenn Sie den Namen eines Prozesses kennen, können Sie ihm eine Nachricht senden.
  • Prozesse teilen sich keine Ressourcen.
  • Die Fehlerbehandlung ist nicht lokal.
  • Prozesse tun, was sie tun sollen, oder versagen.

Anstatt zu versuchen, mit Sperren und Semaphoren und Race-Bedingungen und Deadlocks umzugehen, versuchen Sie, eine Lösung zu übernehmen, bei der Sie nicht so tun müssen, als könnten Sie diese Dinge tatsächlich tun – weil niemand das kann – nicht wirklich. Es gibt einen Grund, warum Linda und Erlang das so machen!

Ein 8051?? Sie sollten eine Trigger-Warnung auf einen Beitrag setzen, der die Verwendung eines 8051 in der heutigen Zeit vorschlägt. Gute Trauer.
@ElliotAlderson – Ich nehme an, als nächstes werden wir den gesamten Cobol-Code entfernen und das gesamte Bankensystem in die Knie zwingen. ;-) Sie haben Recht, aber er sagte, dass er Legacy-Code hat, und er könnte einige 8051 haben, die er auch neu schreiben muss.