Die ganze Wahrheit über RTOS. Artikel 10. Scheduler: Erweiterte Funktionen und Kontexterhaltung



In einem früheren Artikel haben wir uns mit den verschiedenen vom RTOS unterstützten Planungsarten und den damit verbundenen Funktionen in Nucleus SE befasst. In diesem Artikel werden zusätzliche Planungsoptionen in Nucleus SE und der Prozess zum Speichern und Wiederherstellen des Kontexts erläutert.

Frühere Artikel in der Reihe:
Artikel 9. Scheduler: Implementierung
Artikel 8. Nucleus SE: Internes Design und Bereitstellung
Artikel 7. Nucleus SE: Einführung
Artikel 6. Andere RTOS-Dienste
Artikel 5. Aufgabeninteraktion und Synchronisation
Artikel 4. Aufgaben, Kontextwechsel und Interrupts
Artikel 3. Aufgaben und Planung
Artikel 2. RTOS: Struktur und Echtzeitmodus
Artikel 1. RTOS: Einführung.

Optionale Funktionen


Während der Entwicklung von Nucleus SE habe ich die maximale Anzahl von Funktionen optional gemacht, was Speicher und / oder Zeit spart.

Aufgaben aussetzen


Wie bereits im Artikel "Scheduler: Implementierung" erwähnt , unterstützt Nucleus SE verschiedene Optionen zum Anhalten von Aufgaben. Diese Funktion ist jedoch optional und wird durch das Symbol NUSE_SUSPEND_ENABLE in nuse_config.h enthalten . Bei TRUE wird die Datenstruktur als NUSE_Task_Status [] definiert . Diese Art der Aussetzung gilt für alle Aufgaben. Das Array ist vom Typ U8 , wobei 2 Halbbytes separat verwendet werden. Die unteren 4 Bits enthalten den Status der Aufgabe:
NUSE_READY, NUSE_PURE_SUSPEND , NUSE_SLEEP_SUSPEND , NUSE_MAILBOX_SUSPEND usw. Wenn eine Aufgabe durch einen API-Aufruf angehalten wird (z. B. NUSE_MAILBOX_SUSPEND ), enthalten die hohen 4 Bits den Index des Objekts, für das die Aufgabe angehalten wird. Diese Informationen werden verwendet, wenn die Ressource verfügbar wird. Um die API aufzurufen, müssen Sie herausfinden, welche der angehaltenen Aufgaben fortgesetzt werden müssen.

Um die Task-Suspendierung durchzuführen, werden zwei Scheduler-Funktionen verwendet: NUSE_Suspend_Task () und NUSE_Wake_Task () .

Der NUSE_Suspend_Task () - Code lautet wie folgt:



Die Funktion speichert den neuen Status der Aufgabe (alle 8 Bits), der als Parameter suspend_code erhalten wird. Wenn Sie das Sperren aktivieren (siehe "API-Aufrufe sperren" weiter unten), wird der NUSE_SUCCESS- Rückkehrcode gespeichert . Als nächstes wird NUSE_Reschedule () aufgerufen, um die Steuerung auf die nächste Aufgabe zu übertragen.

Der Code NUSE_Wake_Task () ist recht einfach:



Der Status der Aufgabe wird auf NUSE_READY gesetzt . Wenn der Prioritätsplaner nicht verwendet wird, belegt die aktuelle Aufgabe weiterhin den Prozessor, bis die Zeit zum Freigeben der Ressource gekommen ist. Wenn der Prioritätsplaner verwendet wird, wird NUSE_Reschedule () mit dem Aufgabenindex als Hinweis auf den Abschluss aufgerufen, da die Aufgabe möglicherweise eine höhere Priorität hat und sofort ausgeführt werden muss.

API-Aufrufe sperren


Nucleus RTOS unterstützt eine Reihe von API-Aufrufen, mit denen ein Entwickler eine Aufgabe anhalten (blockieren) kann, wenn keine Ressourcen verfügbar sind. Die Aufgabe wird fortgesetzt, wenn wieder Ressourcen verfügbar sind. Dieser Mechanismus ist auch in Nucleus SE implementiert und gilt für eine Reihe von Kernelobjekten: Eine Aufgabe kann in einem Speicherabschnitt, in einer Ereignisgruppe, einem Postfach, einer Warteschlange, einem Kanal oder einem Semaphor gesperrt werden. Wie die meisten Tools in Nucleus SE ist es jedoch optional und wird durch das Symbol NUSE_BLOCKING_ENABLE in nuse_config.h definiert . Wenn TRUE festgelegt ist , wird das Array NUSE_Task_Blocking_Return [] definiert, das den Rückkehrcode für jede Task enthält. Dies kann NUSE_SUCCESS oder der Code NUSE_MAILBOX_WAS_RESET sein , der angibt, dass das Objekt zurückgesetzt wurde, als die Aufgabe gesperrt wurde. Wenn die Sperre aktiviert ist, wird der entsprechende Code mithilfe der bedingten Kompilierung in die API-Funktionen aufgenommen.

Scheduler-Zähler


Nucleus RTOS berechnet, wie oft eine Aufgabe seit ihrer Erstellung und ihrem letzten Zurücksetzen geplant wurde. Diese Funktion ist auch in Nucleus SE implementiert, ist jedoch optional und wird durch das Symbol NUSE_SCHEDULE_COUNT_SUPPORT in nuse_config.h definiert . Bei TRUE wird ein Array von NUSE_Task_Schedule_Count [] vom Typ U16 erstellt , in dem der Zähler jeder Aufgabe in der Anwendung gespeichert wird .

Ausgangszustand der Aufgabe


Wenn eine Aufgabe in Nucleus RTOS erstellt wird, können Sie ihren Status auswählen: Bereit oder angehalten. In Nucleus SE sind standardmäßig alle Aufgaben beim Start bereit. Mit der Option, die mit dem Symbol NUSE_INITIAL_TASK_STATE_SUPPORT in nuse_config.h ausgewählt wurde, können Sie den Startstatus auswählen. Das Array NUSE_Task_Initial_State [] ist in nuse_config.c definiert und erfordert die Initialisierung von NUSE_READY oder NUSE_PURE_SUSPEND für jede Aufgabe in der Anwendung.

Kontext speichern


Die Idee, den Kontext einer Aufgabe mit einem beliebigen Schedulertyp außer RTC (Run to Completion) beizubehalten, wurde in Artikel 3 „Aufgaben und Zeitplanung“ vorgestellt. Wie bereits erwähnt, gibt es verschiedene Möglichkeiten, den Kontext aufrechtzuerhalten. Da Nucleus SE nicht für 32-Bit-Prozessoren ausgelegt ist, habe ich mich für die Verwendung von Tabellen und nicht von Stapeln entschieden, um den Kontext aufrechtzuerhalten.

Ein zweidimensionales Array vom ADDR- Typ NUSE_Task_Context [] [] wird verwendet, um den Kontext für alle Aufgaben in der Anwendung zu speichern. Die Zeilen sind NUSE_TASK_NUMBER (die Anzahl der Aufgaben in der Anwendung), die Spalten sind NUSE_REGISTERS (die Anzahl der Register, die gespeichert werden müssen; hängt vom Prozessor ab und ist in nuse_types.h festgelegt) .

Das Beibehalten des Kontexts und das Wiederherstellen von Code hängt natürlich vom Prozessor ab. Dies ist der einzige Nucleus SE-Code, der an ein bestimmtes Gerät (und eine bestimmte Entwicklungsumgebung) gebunden ist. Ich werde ein Beispiel für den Speicher- / Wiederherstellungscode für den ColdFire-Prozessor geben. Obwohl diese Auswahl aufgrund eines veralteten Prozessors seltsam erscheint, ist sein Assembler leichter zu lesen als die Assembler der meisten modernen Prozessoren. Der Code ist einfach genug, um als Grundlage für die Erstellung eines Kontextwechsels für andere Prozessoren verwendet zu werden:



Wenn eine Kontextumschaltung erforderlich ist, wird dieser Code in NUSE_Context_Swap aufgerufen. Es werden zwei Variablen verwendet: NUSE_Task_Active , der Index der aktuellen Aufgabe, dessen Kontext beibehalten werden muss; NUSE_Task_Next , der Index der Aufgabe, deren Kontext Sie laden möchten (siehe Abschnitt Globale Daten).

Der Kontexterhaltungsprozess funktioniert wie folgt:

  • Die Register A0 und D0 werden vorübergehend auf dem Stapel gespeichert.
  • A0 ist so konfiguriert, dass es auf ein Array von Kontextblöcken verweist. NUSE_Task_Context [] [] ;
  • D0 wird mit NUSE_Task_Active geladen und mit 72 multipliziert (ColdFire verfügt über 18 Register, für deren Speicherung 72 Byte erforderlich sind).
  • dann wird D0 zu A0 hinzugefügt, das nun auf einen Kontextblock für die aktuelle Aufgabe zeigt;
  • dann werden die Register im Kontextblock gespeichert; zuerst A0 und D0 (vom Stapel), dann D1-D7 und A1-A6 , dann SR und PC (vom Stapel sehen wir uns eine schnell eingeleitete Kontextumschaltung an), und am Ende wird der Stapelzeiger gespeichert.

Der Kontextladevorgang ist dieselbe Abfolge von Aktionen in umgekehrter Reihenfolge:

  • A0 ist so konfiguriert, dass es auf ein Array von Kontextblöcken verweist. NUSE_Task_Context [] [] ;
  • D0 wird mit NUSE_Task_Active geladen, inkrementiert und mit 72 multipliziert.
  • dann wird D0 zu A0 hinzugefügt, das nun auf den Kontextblock für die neue Aufgabe zeigt (da das Laden des Kontexts im umgekehrten Prozess des Speicherns der Sequenz erfolgen sollte, wird zuerst der Stapelzeiger benötigt);
  • dann werden die Register aus dem Kontextblock wiederhergestellt; Zuerst werden der Stapelzeiger, dann PC und SR auf den Stapel geschoben , dann werden D1-D7 und A1-A6 geladen und am Ende von D0 und A0 .

Die Schwierigkeit bei der Implementierung der Kontextumschaltung besteht darin, dass der Zugriff auf das Statusregister für viele Prozessoren schwierig ist (für ColdFire ist dies SR ). Eine übliche Lösung ist eine Unterbrechung, d. H. Eine Programmunterbrechung oder eine bedingte Verzweigungsunterbrechung, die bewirkt, dass der SR zusammen mit dem PC auf den Stapel geladen wird. So funktioniert Nucleus SE unter ColdFire. Das Makro NUSE_CONTEXT_SWAP () wird in nuse_types.h festgelegt , das sich auf Folgendes erstreckt:
asm ("trap # 0");

Das Folgende ist der Initialisierungscode ( NUSE_Init_Task () in nuse_init.c ) für Kontextblöcke:



Auf diese Weise erfolgt die Initialisierung des Stapelzeigers, des PCs und des SR . Die ersten beiden haben vom Benutzer in nuse_config.c festgelegte Werte. Der Wert von SR ist als das Zeichen NUSE_STATUS_REGISTER in nuse_types.h definiert . Für ColdFire ist dieser Wert 0x40002000 .

Globale Daten


Der Nucleus SE-Scheduler benötigt nur sehr wenig Speicher zum Speichern von Daten, verwendet jedoch natürlich Datenstrukturen, die mit Aufgaben verknüpft sind. Dies wird in den folgenden Artikeln ausführlich erläutert.

RAM-Daten


Der Scheduler verwendet die im ROM befindlichen Daten nicht und 2 bis 5 globale Variablen werden im RAM abgelegt (alle sind in nuse_globals.c festgelegt ), je nachdem, welcher Scheduler verwendet wird:

  • NUSE_Task_Active - eine Variable vom Typ U8, die den Index der aktuellen Aufgabe enthält;
  • NUSE_Task_State - eine Variable vom Typ U8, die einen Wert enthält, der den Status des aktuell ausgeführten Codes angibt. Dies kann eine Task, ein Interrupt-Handler oder ein Startcode sein. mögliche Werte sind: NUSE_TASK_CONTEXT , NUSE_STARTUP_CONTEXT , NUSE_NISR_CONTEXT und NUSE_MISR_CONTEXT ;
  • NUSE_Task_Saved_State - eine Variable vom Typ U8, die zum Schutz des Werts von NUSE_Task_State in einem verwalteten Interrupt verwendet wird.
  • NUSE_Task_Next - eine Variable vom Typ U8, die den Index der nächsten Task enthält, die für alle Scheduler außer RTC geplant werden sollte;
  • NUSE_Time_Slice_Ticks - eine Variable vom Typ U16, die einen Zähler für Zeitscheiben enthält ; Wird nur mit dem TS-Scheduler verwendet.

Scheduler-Daten-Footprint


Der Nucleus SE-Scheduler verwendet keine ROM-Daten. Die genaue Menge der RAM-Daten hängt vom verwendeten Scheduler ab:

  • für RTC - 2 Bytes ( NUSE_Task_Active und NUSE_Task_State );
  • für RR und Priorität - 4 Bytes ( NUSE_Task_Active , NUSE_Task_State , NUSE_Task_Saved_State und NUSE_Task_Next );
  • für TS - 6 Bytes ( NUSE_Task_Active , NUSE_Task_State , NUSE_Task_Saved_State , NUSE_Task_Next und NUSE_Time_Slice_Ticks ).

Implementierung anderer Planungsmechanismen


Trotz der Tatsache, dass Nucleus SE eine Auswahl von 4 Schedulern bietet, die die meisten Fälle abdecken, können Sie mit der offenen Architektur Möglichkeiten für andere Fälle implementieren.

Zeitscheiben mit Hintergrundaufgabe


Wie bereits in Artikel 3, „Aufgaben und Zeitplanung“ beschrieben, weist der einfache Zeitscheiben-Zeitplaner Einschränkungen auf, da er die maximale Zeit begrenzt, die ein Prozessor für eine Aufgabe verwenden kann. Eine komplexere Option wäre das Hinzufügen von Unterstützung für die Hintergrundaufgabe. Eine solche Aufgabe kann für jeden für angehaltene Aufgaben zugewiesenen Steckplatz geplant und ausgeführt werden, wenn der Steckplatz teilweise freigegeben ist. Mit diesem Ansatz können Sie Aufgaben in regelmäßigen Abständen und mit einem vorhergesagten Prozentsatz der zu erledigenden Prozessorkernzeit planen.

Priorität und Round Robin (RR)


In den meisten Echtzeitkernen unterstützt der Prioritätsplaner im Gegensatz zu Nucleus SE, bei dem jede Aufgabe eine eindeutige Ebene hat, mehrere Aufgaben auf jeder Prioritätsstufe. Ich habe die letztere Option bevorzugt, da sie die Datenstrukturen und damit den Scheduler-Code erheblich vereinfacht. Zur Unterstützung komplexerer Architekturen wären zahlreiche ROM- und RAM-Tabellen erforderlich.

Über den Autor: Colin Walls ist seit über dreißig Jahren in der Elektronikindustrie tätig und widmet sich die meiste Zeit der Firmware. Heute ist er Firmware-Ingenieur bei Mentor Embedded (einer Abteilung von Mentor Graphics). Colin Walls spricht häufig auf Konferenzen und Seminaren, Autor zahlreicher technischer Artikel und zweier Bücher über Firmware. Lebt in Großbritannien. Colins professioneller Blog , E-Mail: colin_walls@mentor.com.

Über die Übersetzung: Diese Artikelserie schien insofern interessant zu sein, als der Autor trotz der an einigen Stellen veralteten beschriebenen Ansätze den schlecht ausgebildeten Leser in die Funktionen des Echtzeit-Betriebssystems einführt. Ich selbst gehöre zu dem Team von Entwicklern des russischen RTOS , das wir kostenlos machen wollen , und ich hoffe, dass der Zyklus für unerfahrene Entwickler nützlich sein wird.

Source: https://habr.com/ru/post/de423967/


All Articles