Gibt es Plattformen, auf denen das Deaktivieren/Wiederherstellen von Interrupts von ISR anders erfolgen sollte als von Nicht-ISR-Kontexten?

Ich bin mit mehreren Echtzeitkerneln vertraut: AVIX , FreeRTOS , TNKernel , und in allen haben wir 2 Versionen von fast allen Funktionen: eine zum Aufrufen von Tasks und eine zum Aufrufen von ISR.

Natürlich ist es sinnvoll für Funktionen, die den Kontext wechseln und / oder schlafen könnten: Offensichtlich kann ISR nicht schlafen, und der Kontextwechsel sollte auf andere Weise erfolgen. Es gibt jedoch mehrere Funktionen, die weder den Kontext wechseln noch schlafen: Sagen wir, es kann die System-Tick-Zählung zurückgeben oder den Software-Timer einrichten usw.

Jetzt implementiere ich meinen eigenen Kernel: TNeoKernel , der wohlgeformten Code hat und sorgfältig getestet wurde, und ich denke darüber nach, manchmal "universelle" Funktionen zu erfinden: diejenigen, die entweder aus dem Task- oder dem ISR-Kontext aufgerufen werden können. Aber da alle drei oben genannten Kernel separate Funktionen verwenden, fürchte ich, dass ich etwas falsch mache.

Angenommen, im Task- und ISR-Kontext verwendet TNKernel verschiedene Routinen zum Deaktivieren/Wiederherstellen von Interrupts, aber soweit ich sehe, besteht der einzig mögliche Unterschied darin, dass ISR-Funktionen als Optimierung "kompiliert" werden können, wenn die Zielplattform dies nicht unterstützt verschachtelte Interrupts. Wenn die Zielplattform jedoch verschachtelte Interrupts unterstützt, sieht das Deaktivieren/Wiederherstellen von Interrupts für Task- und ISR-Kontext absolut gleich aus.

Meine Frage lautet also : Gibt es Plattformen, auf denen das Deaktivieren / Wiederherstellen von Interrupts von ISR anders erfolgen sollte als von Nicht-ISR-Kontexten?

Wenn es solche Plattformen nicht gibt, würde ich lieber mit "universellen" Funktionen arbeiten. Wenn Sie Kommentare zu diesem Ansatz haben, sind diese sehr willkommen.

UPD: Ich mag es nicht, zwei Funktionssätze zu haben, weil sie zu einer bemerkenswerten Duplizierung und Komplikation des Codes führen. Angenommen, ich muss eine Funktion bereitstellen, die den Software-Timer starten soll. So sieht es aus:

enum TN_RCode _tn_timer_start(struct TN_Timer *timer, TN_Timeout timeout)
{
   /* ... real job is done here ... */
}

/*
 * Function to be called from task
 */
enum TN_RCode tn_timer_start(struct TN_Timer *timer, TN_Timeout timeout)
{
   TN_INTSAVE_DATA;  //-- define the variable to store interrupt status,
                     //   it is used by TN_INT_DIS_SAVE()
                     //   and TN_INT_RESTORE()
   enum TN_RCode rc = TN_RC_OK;

   //-- check that function is called from right context
   if (!tn_is_task_context()){
      rc = TN_RC_WCONTEXT;
      goto out;
   }

   //-- disable interrupts
   TN_INT_DIS_SAVE();

   //-- perform real job, after all
   rc = _tn_timer_start(timer, timeout);

   //-- restore interrupts state
   TN_INT_RESTORE();

out:
   return rc;
}

/*
 * Function to be called from ISR
 */
enum TN_RCode tn_timer_istart(struct TN_Timer *timer, TN_Timeout timeout)
{
   TN_INTSAVE_DATA_INT;    //-- define the variable to store interrupt status, 
                           //   it is used by TN_INT_DIS_SAVE()                
                           //   and TN_INT_RESTORE()                           
   enum TN_RCode rc = TN_RC_OK;

   //-- check that function is called from right context
   if (!tn_is_isr_context()){
      rc = TN_RC_WCONTEXT;
      goto out;
   }

   //-- disable interrupts
   TN_INT_IDIS_SAVE();

   //-- perform real job, after all
   rc = _tn_timer_start(timer, timeout);

   //-- restore interrupts state
   TN_INT_IRESTORE();

out:
   return rc;
}

Wir brauchen also Wrapper wie die oben genannten für fast alle Systemfunktionen. Dies ist eine Art Unannehmlichkeit, sowohl für mich als Kernel-Entwickler als auch für Kernel-Benutzer.

Der einzige Unterschied besteht darin, dass verschiedene Makros verwendet werden: für Aufgaben sind dies TN_INTSAVE_DATA, TN_INT_DIS_SAVE(), TN_INT_RESTORE(); für Interrupts sind dies TN_INTSAVE_DATA_INT, TN_INT_IDIS_SAVE(), TN_INT_IRESTORE().

Für die Plattformen, die verschachtelte Interrupts (ARM, PIC32) unterstützen, sind diese Makros identisch. Für andere Plattformen, die keine verschachtelten Interrupts unterstützen, werden TN_INTSAVE_DATA_INT, TN_INT_IDIS_SAVE()und TN_INT_IRESTORE()auf nichts erweitert. Es ist also ein bisschen Leistungsoptimierung, aber die Kosten sind meiner Meinung nach zu hoch: Es ist schwieriger zu warten, nicht so bequem zu verwenden und die Codegröße nimmt zu.

Ich glaube, dass es ein Parameter ist, die Interrupt-Latenz so gering wie möglich zu halten. Die Aufteilung in zwei Routinen ist eine Möglichkeit, die eine Optimierung ermöglicht, in diesem Fall, um die Interrupt-Ausschaltzeit so gering wie möglich zu halten. Wir alle wissen, dass eine vorzeitige Optimierung eine schlechte Sache ist, also sollten Sie damit vielleicht auf später warten. Aber es ist eine sehr gute Idee, die Routinen in einen Satz für den Interrupt-Kontext und einen für den Task-Kontext zu unterteilen, selbst wenn es sich anfänglich um dieselbe Routine handelt (nur unterschiedliche Namen, die auf denselben Eintrag verweisen).
Der zweite Punkt ist, dass das Deaktivieren von Interrupts eine privilegierte Funktion ist. Wenn Sie jemals Ihr "System" in Benutzer- und privilegierten Modus aufteilen möchten, werden die Funktionen unterschiedlich sein - um Interrupts aus dem Benutzermodus zu deaktivieren, müssen Sie in den privilegierten Modus "fallen".
Es gibt tatsächlich noch einen dritten Grund, der manchmal als Optimierung in einem Betriebssystem verfügbar ist, das auf andere Architekturen portiert werden kann. Einige Prozessoren haben Primitive, mit denen Sie beispielsweise ein Element zu einer Warteschlange hinzufügen können, ohne Interrupts überhaupt zu deaktivieren. In einem tragbaren Betriebssystem hätten Sie dies als zwei separate Routinen, den Benutzermodus und den Interrupt-Modus. Auf einigen Prozessoren wären sie gleich, auf anderen müssten Sie aus dem Benutzermodus heraus abfangen. Das Abfangen aus dem Unterbrechungskontext, wenn es nicht benötigt wird, ist teuer.

Antworten (2)

Ich glaube, es gibt noch mehr treibende Kräfte dafür, zwei Gruppen von Funktionen zu haben, als Sie erwähnt haben.

  1. Diese Echtzeitbetriebssysteme sind so konzipiert, dass sie auf viele verschiedene Prozessor-Befehlssatzarchitekturen (ISAs) portierbar sind, und sie versuchen auch sicherzustellen, dass Entwickler Systeme schreiben können, die portabel sind. Daher müssen sie die gesamte Variabilität von Prozessor-ISAs auf eine Weise berücksichtigen, die die Unterschiede zwischen ISAs vor den RTOS-Benutzern verbirgt .
  2. Einige der unter einem RTOS verwendeten ISAs haben sowohl einen „Benutzer“-Modus als auch einen „privilegierten“ Modus. Der Benutzermodus hat keinen Zugriff auf das gesamte System und kann insbesondere keine Interrupts blockieren. Außerdem können einige der Funktionen möglicherweise nicht im 'Benutzer'-Modus ausgeführt werden und können zu einem Ausnahme-Handler im privilegierten Modus 'fallen', um die Arbeit tatsächlich zu erledigen. Daher könnte es für RTOS- und Anwendungsentwickler hilfreich sein, zwei „Namespaces“ zu haben, die auf die beiden CPU-Berechtigungszustände ausgerichtet sind.
  3. IMHO ist es viel einfacher , sich daran zu erinnern, eine Funktion aus einem "Namensraum" (z. B. "Benutzer"-Modus) zu verwenden, als genau herauszufinden, welche Funktion in einem bestimmten Kontext verwendet werden kann. Insbesondere ist es für alle Funktionen viel einfacher, in einen Namensraum für „Benutzeranwendung“ und einen Namensraum für „privilegierte Systemkomponenten“ zu kommen. Auch wenn der Code identisch ist, ist es für den Anwendungsentwickler einfacher, die Funktionen im Namensraum „Anwendung“ oder „Benutzer“ zu verwenden. Wenn dies ein Vorteil für den Anwendungsentwickler ist, dann ist es für den RTOS-Anbieter hilfreicher, den Code in eine Funktion im anderen „Namensraum“ zu „duplizieren“, als die duplizierte Funktion nicht zu haben.

Zusammenfassung: IMHO hat es zwei Sätze von Funktionen, die effektiv zwei verschiedene „Namensräume“ („Benutzeranwendung“ und „Systemkomponente“) bilden, und erleichtert es dem „Anwendungsentwickler“ und den Entwicklern, zusätzliche „privilegierte Systemkomponenten“ hinzuzufügen verwenden.

Es könnte auch einfacher sein, zwei verschiedene Funktionssätze zu haben, um die Verwaltung von Interrupts zu vereinfachen, die ausgelöst werden, während der Code in zwei verschiedenen Systemzuständen ausgeführt wird.

Es kann relativ einfach sein, die beiden Alternativen mit einigen sorgfältig durchdachten Präprozessor-Makros zu erstellen, um die Codeduplizierung zu minimieren.

Wenn Sie nicht beabsichtigen, dass jemand anderes Ihr Betriebssystem verwendet, oder Ihr Betriebssystem Ausnahmen behandelt, die im Benutzermodus und im Systemmodus auftreten, können Sie die Idee wahrscheinlich ignorieren.

Wenn Sie jedoch nicht wissen , wie das alles funktionieren könnte (zum Beispiel ist dies das erste Mal, dass Sie ein RTOS geschrieben haben) oder wenn Sie glauben, dass es sich ändern könnte, dann sollten Sie vielleicht sorgfältig darüber nachdenken, wie Sie es wieder einführen würden zwei 'Namensräume' auf halbem Weg durch die Entwicklung Ihres Betriebssystems.

Erstens kann ich Folgendes nicht verstehen: Sie haben gesagt: " IMHO ist es viel einfacher, sich daran zu erinnern, eine Funktion aus einem 'Namensraum' zu verwenden, als genau herauszufinden, welche Funktion in einem bestimmten Kontext verwendet werden kann. " , und ich nahm an, Sie meinen, dass es besser ist, nur einen Satz von Funktionen zu haben, anstatt zwei. Aber dann sagten Sie, dass zwei Funktionssätze besser sind.
Und zweitens: Sie haben gesagt, dass es für mich als RTOS-Entwickler sowie für RTOS-Benutzer "einfacher" ist, zwei Sets zu haben. Ich stimme dem nicht zu: Es ist wirklich nicht bequem für mich als Kernel-Entwickler, zwei Sätze von Funktionen zu pflegen (siehe meine aktualisierte Frage), und es ist für mich als Benutzer nicht bequem, darüber nachzudenken, ob ich Funktion für Aufgabe oder verwenden sollte die für ISR. Ist es nicht einfacher , sich nur eine Funktion zu merken statt zwei?
Zum Benutzermodus / Kernelmodus: Aus meiner Erfahrung (ich habe hauptsächlich mit PIC24 und PIC32 gearbeitet) werden Echtzeitkernel wie die oben genannten normalerweise in der eingebetteten Welt mit unzureichenden Ressourcen usw. verwendet, sodass der Code den Kernel tatsächlich nie verlässt Modus. Haben Sie andere Erfahrungen?
Ich wollte klarstellen. Ein wichtiger Punkt ist, dass einige Funktionen zwei Versionen haben müssen , eine für „Benutzer“ und eine für „Kernel“. Es muss also eine einfache Konvention (oder einen Namensraum) geben, damit der Benutzer den richtigen auswählen kann.
Danke. Nun, zurück zur eigentlichen Frage: Kennen Sie eine CPU, bei der das Deaktivieren/Wiederherstellen von Interrupts von einer ISR anders durchgeführt werden muss als von einem Nicht-ISR-Kontext?
IIRC, einige der ARM-CPUs "schützen" einen Teil des Statusregisters, sodass ein Programm im Benutzermodus keine Interrupts steuern kann. Ich denke, einige MIPS-CPUs tun das auch. Wenn der gesamte Code im privilegierten Modus ausgeführt wird, hat dies keine Auswirkungen. Ich würde dazu neigen, die MMU zu verwenden, um es fehlerhaften Programmen zu erschweren, das System zum Absturz zu bringen.
Ich verstehe. Alle Chips, mit denen ich gearbeitet habe, haben nicht einmal eine "normale" MMU (mit "normal" meine ich TLB-basiert), und ja, der gesamte Code läuft im privilegierten Modus.
Ich meine eine sehr einfache MMU, Basis- und Grenzregister. Das reicht aus, um fehlerhafte Prozesse zu stoppen, die ein System beschädigen. Ich denke, die meisten ARM und MIPS haben das. Wenn Ihr Betriebssystem nur für Ihre Verwendung oder die Verwendung innerhalb der Organisation bestimmt ist, kann der gesamte Code, der im privilegierten Modus ausgeführt wird, akzeptabel sein. Wenn es jedoch keine bemerkenswerten Vorteile hat, verwende ich es möglicherweise nicht. Die MMU wird einige meiner Fehler abfangen. Obwohl es sich nicht lohnt, meine Ansichten zu berücksichtigen, da ich nicht auf dem Markt für ein RTOS bin.

Einige vom Hersteller bereitgestellte E/A-Bibliotheken haben ihre eigenen Methoden zum Aktivieren und Deaktivieren von Interrupts. In einigen Fällen sind sie auf eine unglückliche Weise geschrieben, die anfällig für Fehlfunktionen ist, es sei denn, der gesamte Code, der Interrupts aktiviert oder deaktiviert, tut dies über solche Bibliotheken. Wenn die E/A-Routinen solche Routinen verwenden, dann kann es notwendig sein, sicherzustellen, dass der gesamte andere Code systemweit den Anforderungen solcher Bibliotheken entspricht, es sei denn, die Routinen können gepatcht werden, um sich auf vernünftige Weise zu verhalten.

Ich bin mir nicht sicher, warum Anbieterbibliotheken nicht einfach eine Funktion bereitstellen, die den Interrupt-Aktivierungszustand erfasst, Interrupts deaktiviert und den erfassten Zustandswert zurückgibt, zusammen mit einer Funktion, die einen erfassten Zustand wiederherstellt, und das alles ohne statische oder globale Verwendung Objekte. Module, die diesen allgemeinen Ansatz verwenden, sind natürlich mit anderen Modulen kompatibel, die dies ebenfalls tun, ohne dass sie sich hinsichtlich der globalen Objektverwendung koordinieren müssen. Nichtsdestotrotz gibt es schlecht gestaltete Bibliotheken, und es ist manchmal notwendig, Programme zu schreiben, die sie berücksichtigen.