Echtzeitsystemprogrammierung mit Ada — Priorität, Periodizität und Ausführungszeitsteuerung in der Praxis

· · Ada, Echtzeit, Ravenscar, Ceiling-Locking, Tasking, Scheduling, Prioritätsinversion, Programmiersprache, Echtzeitsysteme, Hohe Zuverlässigkeit

1. Einleitung — die enge Beziehung zwischen Ada und Echtzeit

Der vorherige Artikel „Sichere Nebenläufigkeit in Ada“ behandelte die Grundlagen sicherer Nebenläufigkeit mit Adas Tasks und geschützten Objekten. Dieses Mal gehen wir einen Schritt weiter, in ein noch stärker eingeschränktes Gebiet — Echtzeitsysteme.

In einem Echtzeitsystem bedeutet „Korrektheit“ nicht nur die logische Korrektheit des Berechnungsergebnisses, sondern schließt auch ein, dass dieses Ergebnis fristgerecht vorliegt. Eine um eine Millisekunde verspätete richtige Antwort ist ebenso gefährlich wie eine falsche.

Ada begegnet dieser Anforderung mit einer umfassenden, als Annex D (Real-Time Systems) der Sprachspezifikation standardisierten Sammlung von Echtzeitfunktionen. Das ist keine nachträglich hinzugefügte Bibliothek, sondern eine direkt in die Sprachlaufzeit selbst eingebaute Echtzeitgarantie.

Adas Echtzeitfunktionen (Annex D):
- Task-Prioritäten und Präemption (FIFO_Within_Priorities)
- Das Ceiling_Locking-Protokoll (Vermeidung von Prioritätsinversion)
- delay until für periodische Ausführung zu absoluten Zeitpunkten
- Das Ravenscar-Profil (sicherheitskritische Teilmenge)
- Timing-Ereignisse (zeitgesteuertes Aufwachen ohne Polling)
- Überwachung der Ausführungszeit (Ada.Execution_Time)
- Multi-periodisches Scheduling

In diesem Artikel werden diese Funktionen anhand von 8 praxisnahen Codebeispielen Schritt für Schritt erklärt. Jedes Snippet lässt sich als eigenständiges Beispiel behandeln, aber die Beispiele 04 und 05, die mehrere Kompilationseinheiten enthalten, müssen vor gnatmake erst mit gnatchop aufgeteilt werden.

Zielgruppe und Vorkenntnisse: Dieser Artikel setzt voraus, dass Sie die im vorherigen Artikel behandelten Grundlagen zu Tasks, Rendezvous und geschützten Objekten kennen. Er richtet sich an Entwicklerinnen und Entwickler, die sich für Steuerungssoftware in eingebetteten Geräten oder hochverlässlichen Systemen interessieren, und behandelt keine Einführung in die Ada-Syntax selbst.

Prüfumgebung: Die 8 Beispiele dieses Artikels wurden nachweislich mit GNAT 13.3.0 (Ubuntu 24.04, x86-64) gebaut. Auch die im Text abgedruckten Ausführungsergebnisse (Kapitel 3 und 5) stammen aus derselben Umgebung. Wie Prioritäten und Präemption tatsächlich wirken, hängt vom Betriebssystem und der GNAT-Laufzeit ab (Kapitel 12), sodass sich die Details der Ausgabe je nach Umgebung unterscheiden können.

Die in diesem Artikel vorkommenden Codeausschnitte sind als Referenzsammlung, nach Kapiteln in Dateien geordnet, auf GitHub veröffentlicht.

ada-real-time-systems - komurasoft-blog-samples (GitHub)

2. Was ist ein Echtzeitsystem?

Beginnen wir mit der Klärung der Begriffe.

Konzept Beschreibung
Hard-Real-Time Eine Fristüberschreitung bedeutet einen kritischen Systemausfall (Flugsteuerung, Airbag, Herzschrittmacher)
Soft-Real-Time Eine Fristüberschreitung ist unerwünscht, aber gelegentliche Überschreitungen sind tolerierbar (Videostreaming, Spiele)
Deadline Der absolute Zeitpunkt, bis zu dem eine Task abgeschlossen sein muss
Periode (period) Das Zeitintervall, in dem eine Task wiederholt gestartet wird
WCET (Worst-Case Execution Time) Die maximale Ausführungszeit einer Task
Jitter Die Schwankung der periodischen Ausführung
Schedulability Die Eigenschaft, ob „diese Menge von Tasks so ausgeführt werden kann, dass alle Fristen eingehalten werden“. Die Untersuchung dieser Frage am Schreibtisch heißt Schedulability-Analyse (etwa Antwortzeitanalyse)
Ravenscar-Profil Ein Regelwerk, das Adas Tasking-Funktionen auf eine leicht statisch analysierbare Teilmenge beschränkt. Der Name geht auf das englische Dorf Ravenscar zurück, in dem die Ausarbeitungstagung stattfand (Kapitel 6)

Beim Entwurf eines Echtzeitsystems ist es eine wichtige notwendige Bedingung, dass für jede Task „WCET <= Deadline“ gilt. Das allein garantiert jedoch noch nicht, dass das Gesamtsystem alle Fristen einhält. Dafür ist zusätzlich eine Antwortzeitanalyse nötig, die Blockierzeiten, die Prioritätszuweisung, Jitter, Interrupts sowie das Verhalten von Laufzeit und Betriebssystem einbezieht. In der Praxis strebt man WCET < Deadline an, um Spielraum zu lassen. Adas Echtzeitfunktionen stellen auf Sprachebene ein vorhersagbares Ausführungsmodell bereit, das diese Analyse erleichtert.

EchtzeitanforderungenFristüberschreitung = kritischer Ausfallseltene Überschreitung tolerierbarDeadlineabzuschließen bis ZeitpunktPeriodeWiederholungsintervallWCETWorst-Case-AusführungszeitJitterSchwankung der PeriodeHard-Real-TimeFlugsteuerungAirbagHerzschrittmacherSoft-Real-TimeVideostreamingSpieleUIAda Annex DMechanismen für VorhersagbarkeitFIFO_Within_PrioritiesCeiling_Lockingdelay untilSchedulability-AnalyseNotwendige Bedingung: WCET &lt;= DeadlineHinlänglichkeit durch Antwortzeitanalyse bestätigen

Eines der gefährlichsten Phänomene in Echtzeitsystemen ist die Prioritätsinversion. Dieses Problem trat 1997 beim Mars Pathfinder tatsächlich auf und führte dazu, dass die Sonde wiederholt zurückgesetzt wurde.

Gemeinsame RessourceMedium-Priority-TaskHigh-Priority-TaskLow-Priority-TaskSchedulerGemeinsame RessourceMedium-Priority-TaskHigh-Priority-TaskLow-Priority-TaskSchedulerKritischer Abschnitt läuftH wacht auf, Scheduler unterbricht LBlockiert! (L hält den Lock noch)H wartet auf den Lock, also läuft L weiterSetzt fort, um den Lock freizugeben...M wacht auf, Scheduler unterbricht LL kann den Lock nicht freigebenM läuft frei weiter (H und L stecken fest)【PRIORITÄTSINVERSION】Hohe Priorität unbegrenzt blockiertErwirbt LockVersucht, den Lock zu erwerben

Eine Task mit niedriger Priorität wird, während sie den Lock hält, von einer mittelpriorisierten Task präemptiert, wodurch die hochpriorisierte Task unbegrenzt blockiert bleibt. Die beim Mars Pathfinder tatsächlich ergriffene Gegenmaßnahme war die Aktivierung von Priority Inheritance in VxWorks; Ada stellt für dasselbe Problem mit Ceiling_Locking einen anderen Ansatz als Sprachfeature bereit.

3. Grundlagen der Task-Priorität — FIFO_Within_Priorities

FIFO_Within_Priorities ist eine in Ada Annex D festlegbare, standardisierte prioritätsbasierte Dispatching-Policy. Wird keine Policy explizit angegeben, ist das Standardverhalten implementierungsdefiniert; GNAT verwendet jedoch auf vielen Zielplattformen diese Familie von Policies. Innerhalb derselben Priorität wird nach FIFO (First In, First Out) ausgeführt, und eine Task mit höherer Priorität präemptiert (unterbricht) eine Task mit niedrigerer Priorität.

-- 01_task_priority.ada
-- Grundform von Task-Priorität und FIFO_Within_Priorities
-- Die Konfigurationspragma steht vor den Kontextklauseln

pragma Task_Dispatching_Policy (FIFO_Within_Priorities);

with Ada.Text_IO;               use Ada.Text_IO;
with System;                    use System;
with Ada.Real_Time;             use Ada.Real_Time;

procedure Task_Priority_Demo is

   task High_Priority_Task is
      pragma Priority (Priority'Last);
      pragma Storage_Size (4 * 1024);
   end High_Priority_Task;

   task Low_Priority_Task is
      pragma Priority (Priority'First);
      pragma Storage_Size (4 * 1024);
   end Low_Priority_Task;

   task body High_Priority_Task is
   begin
      Put_Line ("[T=0.0s] High priority task started");
      delay until Clock + Milliseconds (100);
      Put_Line ("[T=0.1s] High priority task completed");
   end High_Priority_Task;

   task body Low_Priority_Task is
   begin
      Put_Line ("[T=0.0s] Low priority task started");
      delay until Clock + Milliseconds (500);
      Put_Line ("[T=0.5s] Low priority task completed");
   end Low_Priority_Task;

begin
   Put_Line ("=== Task Priority Demo (FIFO_Within_Priorities) ===");
   Put_Line ("Main: waiting for tasks to complete...");
   delay until Clock + Milliseconds (800);
   Put_Line ("Main: done");
end Task_Priority_Demo;

Wichtige Punkte:

  • pragma Priority weist jeder Task eine statische Priorität zu. Priority'Last ist die höchste, Priority'First die niedrigste.
  • Der Task-Rumpf dieser Demo führt keine aufwendige Berechnung durch, sondern wartet mit delay until bis zum angegebenen Zeitpunkt. Worauf es hier ankommt: Sobald beide Tasks gleichzeitig lauffähig sind, erhält die höherpriorisierte Task zuerst die Gelegenheit zur Ausführung.
  • Dieses Diagramm zeigt von FIFO_Within_Priorities nur die Präemption zwischen unterschiedlichen Prioritäten. Um die FIFO-Reihenfolge innerhalb derselben Priorität zu zeigen, wäre ein separates Beispiel mit mehreren gleich priorisierten Tasks nötig.
  • In realen Systemen ist es üblich, relative Prioritäten anhand von System.Default_Priority zu entwerfen.
Niedrigpriorisierte Task(Priority=First)Hochpriorisierte Task(Priority=Last)Main-TaskSchedulerNiedrigpriorisierte Task(Priority=First)Hochpriorisierte Task(Priority=Last)Main-TaskSchedulerT=0ms: Beide Tasks sind runnableGibt Start-Log ausGibt Start-Log ausT=100ms: HP wacht aufGibt Abschluss-Log ausT=500ms: LP wacht aufGibt Abschluss-Log aus(T=800ms) Main beendetTask erzeugenTask erzeugenWählt HP mit höchster PrioritätBlockiert bei delay until T+100msFührt als Nächstes LP ausBlockiert bei delay until T+500msFührt HP ausFührt LP aus

Ausführungsbeispiel (GNAT 13.3.0 / Ubuntu 24.04, x86-64):

$ gnatchop -w 01_task_priority.ada .     # → task_priority_demo.adb
$ gnatmake task_priority_demo.adb
$ ./task_priority_demo
[T=0.0s] High priority task started
[T=0.0s] Low priority task started
=== Task Priority Demo (FIFO_Within_Priorities) ===
Main: waiting for tasks to complete...
[T=0.1s] High priority task completed
[T=0.5s] Low priority task completed
Main: done

Beachten Sie, dass die Start-Logs der beiden Tasks vor der Überschriftszeile von Main erscheinen. Im Deklarationsteil deklarierte Tasks werden aktiviert, bevor der Rumpf des umschließenden Unterprogramms betreten wird, weshalb sich diese Reihenfolge ergeben kann. Außerdem kann sich die Reihenfolge dieser beiden Start-Log-Zeilen bei jeder Ausführung vertauschen. Wie in Kapitel 12 beschrieben, hängt es vom Betriebssystem und der GNAT-Laufzeit ab, wie sich pragma Priority tatsächlich auf das Scheduling auswirkt; auf einem allgemeinen Linux-System ist eine prioritätsgetreue Startreihenfolge nicht garantiert. Was sich in diesem Beispiel stabil beobachten lässt, ist die mit delay until festgelegte Abschlussreihenfolge nach 0,1 s bzw. 0,5 s.

Wertebereich der Ada-Priorität (GNAT-Standard):
  Priority'First  = 0   (niedrigste)
  Priority'Last   = 30  (höchste, abhängig vom Betriebssystem)

4. Ceiling_Locking — die Sprache verhindert Prioritätsinversion

Eines der hartnäckigsten Probleme in Echtzeitsystemen ist die Prioritätsinversion (priority inversion): Eine hochpriorisierte Task wartet auf einen Lock, den eine niedrigpriorisierte Task hält, und diese niedrigpriorisierte Task wird von einer mittelpriorisierten Task präemptiert — dadurch bleibt die hochpriorisierte Task unbegrenzt blockiert.

Ada begegnet diesem Problem, indem es das Ceiling_Locking-Protokoll direkt in geschützte Objekte integriert.

-- 02_ceiling_locking.ada
-- Verhinderung von Prioritätsinversion durch das Ceiling_Locking-Protokoll
-- Die Konfigurationspragma steht vor den Kontextklauseln

pragma Locking_Policy (Ceiling_Locking);

with Ada.Text_IO;               use Ada.Text_IO;
with System;                    use System;
with Ada.Real_Time;             use Ada.Real_Time;

procedure Ceiling_Locking_Demo is

   Ceiling : constant System.Any_Priority := System.Any_Priority'Last;

   protected Shared_Data is
      pragma Priority (Ceiling);
      procedure Write (V : Integer);
      function Read return Integer;
   private
      Value : Integer := 0;
   end Shared_Data;

   protected body Shared_Data is
      procedure Write (V : Integer) is
      begin
         Value := V;
      end Write;

      function Read return Integer is
      begin
         return Value;
      end Read;
   end Shared_Data;

   task Producer is
      pragma Priority (Priority'Last);
      pragma Storage_Size (4 * 1024);
   end Producer;

   task Consumer is
      pragma Priority (Priority'First);
      pragma Storage_Size (4 * 1024);
   end Consumer;

   task body Producer is
   begin
      Put_Line ("[T=0.0s] Producer (high prio): about to write");
      Shared_Data.Write (42);
      Put_Line ("[T=0.0s] Producer (high prio): write done");
      delay until Clock + Milliseconds (100);
   end Producer;

   task body Consumer is
   begin
      delay until Clock + Milliseconds (10);
      Put_Line ("[T=0.01s] Consumer (low prio): about to read");
      declare
         V : Integer;
      begin
         V := Shared_Data.Read;
         Put_Line ("[T=0.01s] Consumer (low prio): read done, got" &
                     Integer'Image (V));
      end;
      delay until Clock + Milliseconds (100);
   end Consumer;

begin
   Put_Line ("=== Ceiling_Locking Demo ===");
   Put_Line ("Main: producer priority = Last, consumer priority = First");
   Put_Line ("Ceiling = Any_Priority'Last, locking = Ceiling_Locking");
   delay until Clock + Milliseconds (300);
   Put_Line ("Main: done");
end Ceiling_Locking_Demo;

Funktionsweise von Ceiling_Locking:

  1. Am geschützten Objekt wird mit pragma Priority (Ceiling) die Ceiling-Priorität festgelegt.
  2. Unabhängig davon, welche Task das geschützte Objekt betritt, wird sie beim Eintritt automatisch auf die Ceiling-Priorität angehoben.
  3. Dadurch kann eine mittelpriorisierte Task eine Task, die das geschützte Objekt gerade nutzt, nicht präemptieren.
  4. Beim Verlassen des geschützten Objekts kehrt die Task zu ihrer ursprünglichen Priorität zurück.

Das folgende Diagramm ist keine exakte zeitliche Spur des vorstehenden Beispielcodes, sondern ein Konzeptdiagramm, das zeigt, wie das in Abbildung 2 gezeigte Muster der Prioritätsinversion durch Ceiling_Locking unterdrückt wird.

Geschütztes Objekt(Ceiling=30)Hochpriorisierte Task(Priorität=30)Mittelpriorisierte Task(Priorität=20)Niedrigpriorisierte Task(Priorität=10)SchedulerGeschütztes Objekt(Ceiling=30)Hochpriorisierte Task(Priorität=30)Mittelpriorisierte Task(Priorität=20)Niedrigpriorisierte Task(Priorität=10)SchedulerEin Aufrufer mit aktiver Priorität > Ceiling löst Program_Error ausH(30) entspricht dem Ceiling(30) und darf daher eintretenAusführungspriorität steigt auf 30M wacht aufL läuft mit Ceiling-Priorität 30M(20) kann nicht präemptierenH wacht aufH(30) besteht die Ceiling-Prüfungwartet aber, da L das PO nutztPriorität kehrt auf 10 zurückNach Freigabe des PO läuft HH(30) = Ceiling(30), tritt nach Auflösung der Konkurrenz einBetritt geschützte OperationFührt Operation ausVerlässt geschützte OperationBetritt geschützte OperationVerlässt geschützte Operation

Design-Grundsatz: Die Ceiling-Priorität eines geschützten Objekts muss mindestens so hoch wie die höchste Priorität aller Tasks sein, die dieses geschützte Objekt nutzen. Verstößt man dagegen und ruft eine Task mit einer höheren aktiven Priorität als der Ceiling-Priorität eine geschützte Operation auf, kann Ada diesen Entwurfsfehler durch Auslösen von Program_Error erkennen.

Um dasselbe mit pthread-Mutexen in C zu erreichen, muss explizit das Attribut PTHREAD_PRIO_PROTECT gesetzt werden; in Ada ist dies hingegen ein Standard-Sprachfeature.

5. delay until — periodische Tasks ohne Drift ausführen

Das grundlegende Muster in Echtzeitsystemen ist die periodische Task. Bei einer Task, die in festen Abständen wiederholt ausgeführt wird, ist es äußerst wichtig, kumulative Zeitfehler (Drift) zu vermeiden.

Adas delay until löst dieses Problem auf elegante Weise.

-- 03_periodic_task.ada
-- Periodische Task mit delay until ── verhindert kumulative Drift

with Ada.Text_IO;               use Ada.Text_IO;
with System;                    use System;
with Ada.Real_Time;             use Ada.Real_Time;

procedure Periodic_Task_Demo is

   Period_MS : constant Time_Span := Milliseconds (200);
   Cycles    : constant Positive  := 5;

   task Sensor_Reader is
      pragma Priority (Priority'Last - 2);
      pragma Storage_Size (4 * 1024);
   end Sensor_Reader;

   task body Sensor_Reader is
      Start_Time  : constant Time := Clock;
      Next_Release : Time := Start_Time + Period_MS;
      Cycle_Count  : Natural := 0;
   begin
      Put_Line ("[Sensor] Periodic task starts, period=" &
                To_Duration (Period_MS)'Image & "s, cycles=" &
                Natural'Image (Cycles));

      for I in 1 .. Cycles loop
         delay until Next_Release;

         Cycle_Count := Cycle_Count + 1;
         Put_Line ("[Sensor] Cycle" & Natural'Image (Cycle_Count) &
                   " at" & Duration'Image (To_Duration (Clock - Start_Time)) & "s");

         Next_Release := Next_Release + Period_MS;
      end loop;

      Put_Line ("[Sensor] Periodic task finished. Actual elapsed:" &
                Duration'Image (To_Duration (Clock - Start_Time)) & "s");
   end Sensor_Reader;

begin
   Put_Line ("=== Periodic Task Demo (delay until) ===");
   Put_Line ("Main: waiting for" & Natural'Image (Cycles) & " cycles...");
   delay until Clock + Milliseconds (1500);
   Put_Line ("Main: done");
end Periodic_Task_Demo;

Warum delay until:

Methode Problem
delay Period; Die Verarbeitungszeit jeder Iteration addiert sich, wodurch die Periode allmählich abdriftet (kumulative Drift)
delay until Next_Release; Next_Release := Next_Release + Period; Da absolute Zeit zugrunde liegt, ist der nächste Startzeitpunkt selbst dann korrekt, wenn ein einzelner Durchlauf sich verzögert

Allerdings garantiert delay until nicht automatisch, dass die Verarbeitungszeit innerhalb der Periode bleibt. Überschreitet die Verarbeitung den nächsten Aufwachzeitpunkt, kehrt dieses delay until nahezu sofort zurück, und das System befindet sich in einem Zustand, den es als deadline miss behandeln sollte.

Bei delay:
  T=0ms → Verarbeitung(15ms) → delay 100ms → T=115ms → Verarbeitung(10ms) → ...
  Tatsächlicher Abstand: 115ms, 110ms, ... (Verarbeitungszeit summiert sich)

Bei delay until:
  Next_Release: 100ms, 200ms, 300ms, ... (absolute Zeitpunkte)
  T=0ms → Verarbeitung(15ms) → delay until 100ms → T=100ms → Verarbeitung(10ms) → delay until 200ms
  Tatsächlicher Abstand: 100ms, 100ms, ... (unabhängig von der Verarbeitungszeit)

Ausführungsbeispiel (GNAT 13.3.0 / Ubuntu 24.04, x86-64):

$ gnatchop -w 03_periodic_task.ada .     # → periodic_task_demo.adb
$ gnatmake periodic_task_demo.adb
$ ./periodic_task_demo
[Sensor] Periodic task starts, period= 0.200000000s, cycles= 5
=== Periodic Task Demo (delay until) ===
Main: waiting for 5 cycles...
[Sensor] Cycle 1 at 0.200326876s
[Sensor] Cycle 2 at 0.400159550s
[Sensor] Cycle 3 at 0.600183089s
[Sensor] Cycle 4 at 0.800327451s
[Sensor] Cycle 5 at 1.000260472s
[Sensor] Periodic task finished. Actual elapsed: 1.000297359s
Main: done

Bei jedem Zyklus tritt eine Aufwachverzögerung von etwa 0,2 bis 0,3 Millisekunden auf, doch man erkennt, dass sich diese Verzögerung nicht in die nächste Periode überträgt. Selbst beim fünften Zyklus liegt die Abweichung vom Referenzzeitpunkt unter einer Millisekunde. Wäre der Code mit delay Period; geschrieben, würde sich diese Verzögerung jedes Mal aufsummieren und nach fünf Zyklen zu einem sichtbaren Unterschied führen. Diese Werte sind ein Beispiel auf einem allgemeinen Linux-System und keine für Hard-Real-Time-Umgebungen garantierten Werte.

Dieses delay until-Muster kommt in allen folgenden periodischen Tasks zum Einsatz.

Periodenüberschreitung - deadline missBerechnung 130msNächster = T+100msdelay until T+100ms kehrt sofort zurückVerspätung erkennen und als Überlast behandelndelay until - absoluter ZeitbezugBerechnung 15msNächster = T+100msdelay until T+100ms → Aufwachen bei 100msBerechnung 10msNächster = T+200ms → Aufwachen bei 200msTatsächlicher Abstand: 100ms, 100ms...delay Period - kumulative Driftdelay 100ms → Aufwachen bei 115msT=0ms: Berechnung 15msBerechnung 10ms → 125msdelay 100ms → Aufwachen bei 225msTatsächlicher Abstand: 115ms, 110ms...Fehler kumuliert sich mit der ZeitVerhindert kumulative Drift

6. Das Ravenscar-Profil — eine verifizierbare Echtzeit-Teilmenge

Adas Tasking-Funktionen sind mächtig, doch in sicherheitskritischen Systemen wird genau diese Mächtigkeit zum Problem. Dynamische Task-Erzeugung, die select-Anweisung, die abort-Anweisung und Ähnliches erschweren die statische Analyse der schlechtesten Ausführungszeit.

Das Ravenscar-Profil ist Adas Antwort auf dieses Problem: Es beschränkt die Tasking-Funktionen auf eine statisch analysierbare, deterministische Teilmenge.

-- 04_ravenscar_profile.ada
-- Grundform des Ravenscar-Profils
-- Zur Kompilierzeit in gnat.adc pragma Profile (Ravenscar); angeben
-- Build: gnatchop -w 04_ravenscar_profile.ada .
--         → wird aufgeteilt in ravenscar_state.ads / ravenscar_state.adb / ravenscar_demo.adb
--         gnat.adc bereitstellen, dann gnatmake ravenscar_demo.adb

with Ada.Text_IO;               use Ada.Text_IO;
with System;                    use System;
with Ada.Real_Time;             use Ada.Real_Time;

package Ravenscar_State is

   protected Signal is
      pragma Priority (System.Default_Priority + 5);
      entry Wait_For_Release;
      procedure Release;
   private
      Released : Boolean := False;
   end Signal;

   task Periodic_Worker is
      pragma Priority (System.Default_Priority + 1);
      pragma Storage_Size (4 * 1024);
   end Periodic_Worker;

   task Monitor is
      pragma Priority (System.Default_Priority);
      pragma Storage_Size (4 * 1024);
   end Monitor;

end Ravenscar_State;

with Ada.Text_IO;               use Ada.Text_IO;
with System;                    use System;
with Ada.Real_Time;             use Ada.Real_Time;

package body Ravenscar_State is

   protected body Signal is
      entry Wait_For_Release when Released is
      begin
         Released := False;
      end Wait_For_Release;

      procedure Release is
      begin
         Released := True;
      end Release;
   end Signal;

   task body Periodic_Worker is
      Start_Time   : constant Time := Clock;
      Next_Release : Time := Start_Time + Milliseconds (100);
      Period       : constant Time_Span := Milliseconds (100);
      Cycle_Count  : Natural := 0;
   begin
      Put_Line ("[Worker] Ravenscar periodic task starts");

      for I in 1 .. 4 loop
         delay until Next_Release;

         Cycle_Count := Cycle_Count + 1;
         Put_Line ("[Worker] Cycle" & Natural'Image (Cycle_Count) &
                   " at" & Duration'Image (To_Duration (Clock - Start_Time)) & "s");
         Signal.Release;
         Next_Release := Next_Release + Period;
      end loop;

       Put_Line ("[Worker] Finished demo, waiting (Ravenscar: No_Task_Termination)");
      loop
         delay until Clock + Seconds (1);
      end loop;
   end Periodic_Worker;

   task body Monitor is
   begin
      Put_Line ("[Monitor] Waiting for signals...");

      for I in 1 .. 4 loop
         Signal.Wait_For_Release;
         Put_Line ("[Monitor] Received signal" & Natural'Image (I));
      end loop;

      Put_Line ("[Monitor] All signals received, waiting (Ravenscar: No_Task_Termination)");
      loop
         delay until Clock + Seconds (1);
      end loop;
   end Monitor;

end Ravenscar_State;

with Ravenscar_State; use Ravenscar_State;
with Ada.Text_IO;     use Ada.Text_IO;
with Ada.Real_Time;   use Ada.Real_Time;

procedure Ravenscar_Demo is
begin
   Put_Line ("=== Ravenscar Profile Demo ===");
   Put_Line ("(compile with: gnatmake -gnatec=gnat.adc ravenscar_demo)");
   Put_Line ("Main: waiting for Ravenscar tasks...");
   delay until Clock + Milliseconds (800);
   Put_Line ("Main: demo window elapsed; waiting forever (Ravenscar: No_Task_Termination)");
   loop
      delay until Clock + Seconds (1);
   end loop;
end Ravenscar_Demo;

Einschränkungen des Ravenscar-Profils:

Verbotenes Feature Begründung
Dynamische Task-Erzeugung (new oder Access-Typen) Speicherzuweisung zur Laufzeit ist nicht deterministisch
select-Anweisung Nicht nur mehrere Alternativen — die gesamte select-Anweisung erschwert die Kontrollflussanalyse
abort-Anweisung Asynchrone Unterbrechung macht den Zustand unvorhersagbar
Ada.Task_Attributes Dynamisches Verhalten zur Laufzeit
Dynamische Prioritätsänderung Die Annahmen der Scheduling-Analyse ändern sich zur Laufzeit
Relatives delay (delay) Neigt zu kumulativer Drift; stattdessen absolutes delay until verwenden
Mehrere Entries je geschütztem Objekt Erhöht die Blockierbedingungen und den Analyseaufwand
Beendigung von Tasks Ravenscar behandelt alle Tasks als nicht terminierend
requeue-Anweisung Erschwert die Kontrollflussverfolgung

Durch diese Einschränkungen wird ein Ravenscar-konformes Programm leichter für die statische Zeitanalyse zugänglich. Das ist eine Eigenschaft, die Sicherheitsnormen wie DO-178C (Luftfahrtsoftware) oder ISO 26262 (funktionale Sicherheit im Automobilbau) verlangen. Die obige Liste ist ein Auszug der wichtigsten Einschränkungen; das tatsächliche Profil enthält zusätzliche, Laufzeit und Analysierbarkeit betreffende Regeln wie No_Task_Hierarchy oder Detect_Blocking.

Vollständige Ada-Tasking-FunktionenRavenscar-ProfilEinschränkungenVerpflichtende PoliciesVerbot dynamischer Task-ErzeugungVerbot der select-AnweisungVerbot der abort-AnweisungVerbot von Task_AttributesBeschränkung auf 1 Entry je geschütztem ObjektVerbot der requeue-AnweisungVerbot von relativem delaydelay until verwendenVerbot dynamischer PrioritätsänderungVerbot der Task-Beendigungalle Tasks nicht terminierendFIFO_Within_PrioritiesCeiling_LockingWird dadurch einfacher:statische ZeitanalyseDO-178CLuftfahrtsoftwareISO 26262Funktionale Sicherheit im AutomobilbauIEC 62304Software für Medizinprodukte

Um das Ravenscar-Profil zu aktivieren, schreiben Sie Folgendes in eine gnat.adc-Datei:

pragma Profile (Ravenscar);

7. Timing-Ereignisse — zeitgesteuertes Aufwachen ohne Polling

In vielen Echtzeitsystemen tritt häufig die Anforderung auf, „eine hochpriorisierte Task zu einem festgelegten Zeitpunkt aufzuwecken“. Eine naive Implementierung würde einen Timer per Polling abfragen, doch Ada bietet einen ausgefeilteren Mechanismus — Timing-Ereignisse.

-- 05_timing_events.ada
-- Timing-Ereignisse (Ada.Real_Time.Timing_Events)
-- Mechanismus, um eine hochpriorisierte Task ohne Polling aufzuwecken
-- Build: gnatchop -w 05_timing_events.ada .
--         → wird aufgeteilt in signal_pkg.ads / signal_pkg.adb / timing_events_demo.adb
--         gnatmake timing_events_demo.adb

pragma Locking_Policy (Ceiling_Locking);

with Ada.Text_IO;               use Ada.Text_IO;
with System;                    use System;
with Ada.Real_Time;             use Ada.Real_Time;
with Ada.Real_Time.Timing_Events; use Ada.Real_Time.Timing_Events;

package Signal_Pkg is
   protected type Signal_Type is
      pragma Priority (System.Interrupt_Priority'Last);
      entry Wait_For_Event;
      procedure Fire (Event : in out Timing_Event);
   private
      Fired : Boolean := False;
   end Signal_Type;

   S : Signal_Type;
end Signal_Pkg;

with Ada.Text_IO;               use Ada.Text_IO;
with System;                    use System;
with Ada.Real_Time;             use Ada.Real_Time;
with Ada.Real_Time.Timing_Events; use Ada.Real_Time.Timing_Events;

package body Signal_Pkg is
   protected body Signal_Type is
      entry Wait_For_Event when Fired is
      begin
         Fired := False;
      end Wait_For_Event;

      procedure Fire (Event : in out Timing_Event) is
      begin
         Fired := True;
      end Fire;
   end Signal_Type;
end Signal_Pkg;

with Signal_Pkg; use Signal_Pkg;

with Ada.Text_IO;               use Ada.Text_IO;
with System;                    use System;
with Ada.Real_Time;             use Ada.Real_Time;
with Ada.Real_Time.Timing_Events; use Ada.Real_Time.Timing_Events;

procedure Timing_Events_Demo is

   pragma Priority (29);

   Timer_1 : Timing_Event;
   Timer_2 : Timing_Event;

   task Reactor is
      pragma Priority (System.Default_Priority + 5);
      pragma Storage_Size (4 * 1024);
   end Reactor;

   task body Reactor is
   begin
      Put_Line ("[Reactor] Waiting for timing events...");

      S.Wait_For_Event;
      Put_Line ("[Reactor] Got event #1");

      S.Wait_For_Event;
      Put_Line ("[Reactor] Got event #2");

      Put_Line ("[Reactor] Done");
   end Reactor;

begin
   Put_Line ("=== Timing Events Demo ===");
   Put_Line ("Scheduling two timers at +100ms and +250ms...");

   Set_Handler (Timer_1, Clock + Milliseconds (100), S.Fire'Access);
   Set_Handler (Timer_2, Clock + Milliseconds (250), S.Fire'Access);

   delay until Clock + Milliseconds (500);
   Put_Line ("Main: done");
end Timing_Events_Demo;

Ablauf eines Timing-Ereignisses:

1. Set_Handler(Timer_1, T+100ms, S.Fire'Access)  ── registriert einen Handler für einen absoluten Zeitpunkt
2. T+100ms verstrichen ── die Laufzeit ruft S.Fire **mit der Ceiling-Priorität** auf
3. Fire setzt das Fired-Flag auf True ── die Barriere öffnet sich
4. Die Reactor-Task wacht aus Wait_For_Event auf

Entscheidend ist hier, dass dieses Beispiel Ceiling_Locking ausdrücklich verwendet und der Fire-Handler eine Prozedur des geschützten Objekts ist, sodass er mit der Ceiling-Priorität ausgeführt wird. Eine geschützte Prozedur, die als Handler für ein Timing-Ereignis verwendet wird, gehört in ein geschütztes Objekt mit einer Ceiling-Priorität auf Interrupt-Ebene — hier System.Interrupt_Priority'Last. Dadurch entsteht bei der Verarbeitung von Timing-Ereignissen keine Prioritätsinversion.

8. Eine Echtzeit-Warteschlange mit geschützten Objekten

Ein häufiges Muster in Echtzeitsystemen ist Producer-Consumer: Ein Sensor erzeugt Daten, eine Steuerungstask konsumiert sie — dabei müssen der wechselseitige Ausschluss und das Blockieren des Puffers effizient umgesetzt werden.

Mit Adas geschützten Objekten und Entry-Barrieren lässt sich das als barrierebasierte Synchronisation implementieren. Da die Laufzeit intern den wechselseitigen Ausschluss verwaltet, muss der Anwendungscode keine Mutexe oder Bedingungsvariablen direkt schreiben.

-- 06_protected_queue.ada
-- Gemeinsame Echtzeitdaten über ein geschütztes Objekt
-- Pipeline: Producer -> Bounded_Buffer -> Consumer
-- Zur Kompilierzeit in gnat.adc pragma Locking_Policy (Ceiling_Locking); angeben

pragma Locking_Policy (Ceiling_Locking);

with Ada.Text_IO;               use Ada.Text_IO;
with System;                    use System;
with Ada.Real_Time;             use Ada.Real_Time;

procedure Protected_Queue_Demo is

   Buffer_Size : constant := 4;

   type Buf_Array is array (1 .. Buffer_Size) of Integer;

   protected Bounded_Buffer is
      pragma Priority (System.Any_Priority'Last);
      entry Put (Item : Integer);
      entry Get (Item : out Integer);
   private
      Buf    : Buf_Array;
      Count  : Natural := 0;
      Head   : Positive := 1;
      Tail   : Positive := 1;
   end Bounded_Buffer;

   protected body Bounded_Buffer is
      entry Put (Item : Integer) when Count < Buffer_Size is
      begin
         Buf (Tail) := Item;
         Tail := (Tail mod Buffer_Size) + 1;
         Count := Count + 1;
      end Put;

      entry Get (Item : out Integer) when Count > 0 is
      begin
         Item := Buf (Head);
         Head := (Head mod Buffer_Size) + 1;
         Count := Count - 1;
      end Get;
   end Bounded_Buffer;

   task Producer is
      pragma Priority (System.Default_Priority + 2);
      pragma Storage_Size (4 * 1024);
   end Producer;

   task Consumer is
      pragma Priority (System.Default_Priority + 1);
      pragma Storage_Size (4 * 1024);
   end Consumer;

   task body Producer is
      Next_Release : Time := Clock + Milliseconds (50);
      Period       : constant Time_Span := Milliseconds (50);
   begin
      for I in 1 .. 6 loop
         Bounded_Buffer.Put (I);
         Put_Line ("[Producer] Put" & Integer'Image (I));
         delay until Next_Release;
         Next_Release := Next_Release + Period;
      end loop;
      Put_Line ("[Producer] Done");
   end Producer;

   task body Consumer is
      Item         : Integer;
      Next_Release : Time := Clock + Milliseconds (80);
      Period       : constant Time_Span := Milliseconds (80);
   begin
      delay until Clock + Milliseconds (30);
      for I in 1 .. 6 loop
         Bounded_Buffer.Get (Item);
         Put_Line ("[Consumer] Got" & Integer'Image (Item));
         delay until Next_Release;
         Next_Release := Next_Release + Period;
      end loop;
      Put_Line ("[Consumer] Done");
   end Consumer;

begin
   Put_Line ("=== Protected Queue Demo (Ceiling_Locking) ===");
   Put_Line ("Buffer size = 4; Producer every 50ms, Consumer every 80ms");
   delay until Clock + Milliseconds (800);
   Put_Line ("Main: done");
end Protected_Queue_Demo;

Wichtige Designpunkte:

  • entry Put when Count < Buffer_Size ── Ist der Puffer voll, wird der Producer automatisch blockiert.
  • entry Get when Count > 0 ── Ist der Puffer leer, wird der Consumer automatisch blockiert.
  • pragma Priority (System.Any_Priority'Last) ── Durch Ceiling-Locking entsteht zwischen Producer und Consumer keine Prioritätsinversion.
  • Die Barrierebedingungen sind über den internen Zustand des geschützten Objekts (Count) definiert und werden bei jeder Lock-Freigabe automatisch neu ausgewertet.

In diesem Code kommen keine anwendungsseitigen Mutexe, Semaphoren oder Bedingungsvariablen vor. Die benötigte Synchronisation wird über die Entry-Barrieren des geschützten Objekts ausgedrückt.

AusgangszustandPut (1 Element hinzugefügt)Put / GetGet (letztes Element entnommen)Put (letzten freien Platz gefüllt)Get (Platz frei geworden)Get blockiert (Barriere Count=0)Put blockiert (Barriere Count=Buffer_Size)Leer / Count=0Teilweise gefüllt / Count=1..Buffer_Size-1Voll / Count=Buffer_Size

Bei erfolgreichem Put wird die Barriere wartender Get-Aufrufe neu ausgewertet, bei erfolgreichem Get die Barriere wartender Put-Aufrufe. Das geschieht unabhängig davon, in welchem Zustand des Diagramms man sich gerade befindet, jeweils beim Abschluss der geschützten Operation.

9. Messung der Ausführungszeit — der erste Schritt zur Laufzeitüberwachung

Um die Schedulability eines Echtzeitsystems zu bewerten, muss die Ausführungszeit (CPU-Zeit) jeder Task genau bekannt sein. Adas Paket Ada.Execution_Time stellt den CPU-Verbrauch je Task bereit.

-- 07_execution_time.ada
-- Steuerung der Ausführungszeit (Execution_Time)
-- Misst den CPU-Verbrauch je Task

with Ada.Text_IO;               use Ada.Text_IO;
with System;                    use System;
with Ada.Real_Time;             use Ada.Real_Time;
with Ada.Execution_Time;
use type Ada.Execution_Time.CPU_Time;

procedure Execution_Time_Demo is

   package ET renames Ada.Execution_Time;

   task Busy_Worker is
      pragma Priority (System.Default_Priority + 1);
      pragma Storage_Size (4 * 1024);
   end Busy_Worker;

   task body Busy_Worker is
      Wall_Start : Time;
      Cpu_Start  : ET.CPU_Time;
      Dummy      : Integer := 0;
      pragma Volatile (Dummy);
   begin
      Wall_Start := Clock;
      Cpu_Start := ET.Clock;

      Put_Line ("[Worker] Starting compute-bound work...");
      for I in 1 .. 20_000_000 loop
         Dummy := Dummy + 1;
      end loop;
      Put_Line ("[Worker] Dummy =" & Integer'Image (Dummy));

      declare
         Wall_Elapsed : constant Duration :=
            To_Duration (Clock - Wall_Start);
         Cpu_Span     : constant Time_Span :=
            ET.Clock - Cpu_Start;
      begin
         Put_Line ("[Worker] Done, wall time:" &
                   Duration'Image (Wall_Elapsed) & "s");
         Put_Line ("[Worker] CPU time consumed:" &
                   Duration'Image (To_Duration (Cpu_Span)) & "s");
      end;
   end Busy_Worker;

   Cpu_Start_Main : constant ET.CPU_Time := ET.Clock;

begin
   Put_Line ("=== Execution Time Demo ===");

   delay until Clock + Milliseconds (500);

   declare
      Cpu_Span : constant Time_Span := ET.Clock - Cpu_Start_Main;
   begin
      Put_Line ("Main: CPU time consumed after 500ms:" &
                Duration'Image (To_Duration (Cpu_Span)) & "s");
   end;

   Put_Line ("Main: done");
end Execution_Time_Demo;

Wanduhrzeit vs. CPU-Zeit:

Wanduhrzeit (Wall Clock): Ada.Real_Time.Clock
  → Tatsächlich verstrichene Zeit. Enthält auch Block- und Präemptionszeiten.

CPU-Zeit (Execution Time): Ada.Execution_Time.Clock
  → Nur die Zeit, in der diese Task tatsächlich auf der CPU ausgeführt wurde.
  → Block- und Präemptionszeiten werden nicht mitgezählt.

Diese Unterscheidung ist der Ausgangspunkt für die Überwachung der Ausführungszeit und die Validierung des WCET. Während Busy_Worker mit delay until wartet, steigt die CPU-Zeit nicht; sie erhöht sich nur während der tatsächlichen Berechnung. Auch während des delay until Clock + Milliseconds(500) der Haupttask sollte die CPU-Zeit nahezu null sein. Allerdings garantiert die reine Messung der CPU-Zeit nicht den wahren WCET. Für den WCET, der Cache, Pipeline und Speicherkonkurrenz einschließt, ist zusätzlich eine statische Analyse oder eine Verifikation in der Zielumgebung erforderlich.

CPU-ZeitEnthält: nur tatsächliche BerechnungCPU-Zeit gesamt: 120msWanduhrzeitEnthält: Berechnung + Warten + Blockiert + PräemptiertGesamte verstrichene Zeit: 500msDifferenz = Warte-, Block- und PräemptionszeitCPU-Zeit erfasst die tatsächlichen RechenkostenHilft bei WCET-Validierung und ÜberwachungSchließt Warte-, Block- und Präemptionszeit ausAchtungDie Messung garantiert nicht den wahren WCETStatische Analyse oder Zielvalidierung nötig

10. Integrationsdemo — ein Multi-periodisches Echtzeitsystem

Wir integrieren nun alle bisher behandelten Elemente — Priorität, Ceiling_Locking, delay until und geschützte Objekte — zu einem typischen Multi-periodischen Echtzeitsystem.

-- 08_multiperiodic.ada
-- Integrationsdemo eines Multi-periodischen Echtzeitsystems
-- Sensor-Lesetask mit schneller Periode (100ms)
-- Steuerungstask mit langsamer Periode (400ms)
-- Gemeinsame Daten über Ceiling_Locking

pragma Locking_Policy (Ceiling_Locking);

with Ada.Text_IO;               use Ada.Text_IO;
with System;                    use System;
with Ada.Real_Time;             use Ada.Real_Time;

procedure Multiperiodic_Demo is

   package Int_IO is new Ada.Text_IO.Integer_IO (Integer);

   protected Shared_Sensor is
      pragma Priority (System.Any_Priority'Last);
      procedure Write (V : Integer);
      function Read return Integer;
   private
      Value : Integer := 0;
   end Shared_Sensor;

   protected body Shared_Sensor is
      procedure Write (V : Integer) is
      begin
         Value := V;
      end Write;

      function Read return Integer is
      begin
         return Value;
      end Read;
   end Shared_Sensor;

   task Fast_Sensor is
      pragma Priority (System.Default_Priority + 3);
      pragma Storage_Size (4 * 1024);
   end Fast_Sensor;

   task body Fast_Sensor is
      Next_Release : Time := Clock + Milliseconds (100);
      Period       : constant Time_Span := Milliseconds (100);
      Cycle        : Natural := 0;
   begin
      Put_Line ("[Fast] Sensor reader starts (100ms period)");

      for I in 1 .. 12 loop
         delay until Next_Release;
         Cycle := Cycle + 1;
         Shared_Sensor.Write (Cycle * 10);
         Next_Release := Next_Release + Period;
      end loop;
      Put_Line ("[Fast] Done");
   end Fast_Sensor;

   task Slow_Controller is
      pragma Priority (System.Default_Priority + 2);
      pragma Storage_Size (4 * 1024);
   end Slow_Controller;

   task body Slow_Controller is
      Next_Release : Time := Clock + Milliseconds (150);
      Period       : constant Time_Span := Milliseconds (400);
      Cycle        : Natural := 0;
      Raw          : Integer;
   begin
      Put_Line ("[Slow] Controller starts (400ms period)");

      for I in 1 .. 3 loop
         delay until Next_Release;
         Cycle := Cycle + 1;
         Raw := Shared_Sensor.Read;
         Put_Line ("[Slow] Cycle" & Natural'Image (Cycle) &
                   " reads sensor =" & Integer'Image (Raw));
         Next_Release := Next_Release + Period;
      end loop;
      Put_Line ("[Slow] Done");
   end Slow_Controller;

begin
   Put_Line ("=== Multiperiodic Real-Time System Demo ===");
   Put_Line ("Fast sensor (100ms) x 12 + Slow controller (400ms) x 3");
   Put_Line ("Ceiling_Locking prevents priority inversion on shared data");
   delay until Clock + Milliseconds (2000);
   Put_Line ("Main: done");
end Multiperiodic_Demo;

Systemaufbau:

Das folgende Diagramm ist ein Beispielschedule, der auf den Release-Zeitpunkten des Beispielcodes basiert (schneller Sensor: 100ms-Periode; langsame Regelung: 400ms-Periode mit 150ms-Versatz) und für die Erklärung angenommene Ausführungszeiten unterstellt. Da der Code selbst keine 80ms lange Regelungsberechnung enthält, handelt es sich nicht um eine gemessene Darstellung. Hat der schnelle Sensor die höhere Priorität, wird die langsame Regelung unterbrochen, sobald während ihrer Ausführung ein Release des schnellen Sensors eintrifft. Zur besseren Lesbarkeit zeigt das Diagramm pro Zyklus der langsamen Regelung nur eine repräsentative Unterbrechung, tatsächlich wird der schnelle Sensor jedoch an jeder 100ms-Grenze released.

Langsame Regelung Zyklus 3 (release=950ms)Langsame Regelung Zyklus 2 (release=550ms)Langsame Regelung Zyklus 1 (release=150ms)1000-1010msSchneller Sensor #10unterbricht wegen P+3950-1000msLangsame Regelung #3 erste Hälfte1010-1040msLangsame Regelung #3 zweite Hälfte600-610msSchneller Sensor #6unterbricht wegen P+3550-600msLangsame Regelung #2 erste Hälfte610-640msLangsame Regelung #2 zweite Hälfte150-200msLangsame Regelung #1 erste Hälfte100-110msSchneller Sensor #1200-210msSchneller Sensor #2unterbricht wegen P+3210-240msLangsame Regelung #1 zweite HälfteIllustrative AnnahmeSchnelle Sensor: 10ms VerarbeitungLangsame Regelung: 80ms Verarbeitung

Dieses Muster — „schnelle Sensorerfassung + langsame Regelschleife“ — ist eine in industriellen Steuerungssystemen und in der Robotersteuerung häufig anzutreffende, typische Struktur.

11. Wo Adas Echtzeitfunktionen glänzen

Adas Echtzeitfunktionen entfalten ihren Wert besonders in den folgenden Bereichen.

Ada Annex DEchtzeitfunktionenLuft- und RaumfahrtDO-178CEisenbahnEN-50128-FamilieAutomobilbauISO 26262MedizingeräteIEC 62304IndustriesteuerungIEC-61508-FamilieVerteidigung und hochverlässliche SystemeFlugsteuerungbewährter EinsatzbereichSatelliten- und RaumfahrzeugsteuerungSignalsystemeAutomatische ZugsteuerungKandidat für sicherheitsrelevante SteuergeräteBegrenzte, selektive Anwendung imvon C / MISRA-C dominierten BereichHerzschrittmacherInfusionspumpenRobotersteuerungCNC-WerkzeugmaschinenMissionsrechnerLangzeitbetriebssysteme

Dass im Diagramm nur beim Automobilbau „begrenzte, selektive Anwendung“ steht, liegt weniger an der Eignung der Sprache selbst als vielmehr am Umfang des bereits bestehenden Ökosystems. Fahrzeugsoftware baut durchgängig auf C und MISRA-C auf — von branchenweiten API-Spezifikationen wie AUTOSAR über von Zulieferern gelieferten Code bis hin zu zertifizierten Compilern und Verifikationswerkzeugen sowie der Größe des verfügbaren Fachpersonals. Die Sprache zu wechseln bedeutet, selbst wenn es nur um eine einzelne Komponente geht, das gesamte umgebende Werkzeug sowie die Beschaffungs- und Verifikationsprozesse neu aufzubauen. Deshalb tendiert Ada dazu, nicht als Standard für das gesamte Fahrzeug, sondern selektiv für einzelne Komponenten mit besonders hohen Anforderungen oder bei Organisationen eingesetzt zu werden, die bereits über Ada-Bestände und eine entsprechende Entwicklungsorganisation verfügen. Umgekehrt existiert diese Hürde in Bereichen wie der Luft- und Raumfahrt oder der Eisenbahn, wo das gesamte Ökosystem bereits auf hohe Verlässlichkeit ausgerichtet ist, von vornherein gar nicht.

12. Vorbehalte und Grenzen

Adas Echtzeitfunktionen sind mächtig, aber kein Allheilmittel.

1. Plattformabhängigkeit:

  • Die tatsächliche Zuordnung von pragma Priority hängt von der Ausführungsumgebung (Betriebssystem + GNAT-Laufzeit) ab. Unter Linux wird sie auf SCHED_FIFO abgebildet, unter Windows ist eine vollständige Präemption jedoch nicht immer garantiert.

2. Einschränkungen durch Ravenscar:

  • Da dynamische Task-Erzeugung verboten ist, müssen beim Systemstart alle Tasks statisch deklariert werden. Das schränkt die Entwurfsfreiheit ein.

3. Grenzen der WCET-Messung:

  • Ada.Execution_Time liefert eine Messung, keine Garantie. Der wahre WCET, der Cache-Fehlzugriffe und Pipeline-Hazards einschließt, muss zusätzlich mit statischen Analysewerkzeugen verifiziert werden.

4. Overhead:

  • Die Auswertung der Barrieren eines geschützten Objekts erfolgt automatisch beim Abschluss oder Abbruch eines Entry-Aufrufs sowie beim Verlassen des geschützten Objekts. Bei häufig aufgerufenen geschützten Objekten muss dieser Overhead berücksichtigt werden.

5. Toolchain-Hürden:

  • Um Adas Echtzeitfunktionen voll auszunutzen, sind ein geeigneter Cross-Compiler und eine passende Laufzeitumgebung nötig. Besonders bei eingebetteten Zielplattformen ist man auf vom Anbieter bereitgestellte Laufzeiten angewiesen.

13. Zusammenfassung

Dieser Artikel hat die von Adas Annex D bereitgestellten Echtzeitfunktionen anhand von 8 Codebeispielen Schritt für Schritt betrachtet.

Funktion Gebotener Wert
Task-Prioritäten Präemptives, prioritätsbasiertes Scheduling
Ceiling_Locking In die Sprache eingebaute Verhinderung von Prioritätsinversion
delay until Periodische Ausführung ohne kumulative Drift
Ravenscar-Profil Tasking-Teilmenge, die leicht statisch analysierbar ist
Timing-Ereignisse Zeitgesteuertes Aufwachen ohne Polling
Geschützte Warteschlange Barrierebasierte Synchronisation mit geschützten Objekten
Messung der Ausführungszeit CPU-Zeitüberwachung je Task
Multi-periodische Integration Design für das sichere Zusammenleben von Tasks mit unterschiedlichen Perioden

Das Wesen von Adas Echtzeitfunktionen ist, dass sie „nicht nachträglich hinzugefügt“ wurden. Sperrregeln, die Prioritätsinversion unterdrücken, Zeitangaben für periodische Ausführung und die Überwachung der Ausführungszeit werden als Teil der Sprachspezifikation bereitgestellt. Das eigentliche Erreichen von Fristen wird selbstverständlich weiterhin durch Entwurf und Analyse bestätigt, aber dass die Sprachlaufzeit die dafür nötigen Voraussetzungen bereitstellt, ist eine große Stärke.

Als nächsten Schritt können Sie die Echtzeitsystementwicklung mit Ada selbst ausprobieren: Installieren Sie mit Alire die GNAT-Toolchain und bauen Sie den Beispielcode dieses Artikels mit gnatchop + gnatmake.

Die Grundlagen der Ada-Nebenläufigkeit (Tasks, Rendezvous, geschützte Objekte) finden Sie im vorherigen Artikel „Sichere Nebenläufigkeit in Ada“.

14. Quellen

Aktuelle Artikel mit denselben Schlagwörtern führen zu verwandten Themen weiter.

Diese Seiten ordnen den Artikel in einen größeren Leistungs- und Entscheidungskontext ein.

Häufige Fragen

Fragen, die in Beratungen zu diesem Artikelthema häufig gestellt werden.

Was ist Annex D in Ada?
Annex D ist eine als Teil der Ada-Sprachspezifikation standardisierte Sammlung von Funktionen für Echtzeitsysteme. Dazu gehören die prioritätsbasierte präemptive Ablaufplanung über FIFO_Within_Priorities, das Ceiling_Locking-Protokoll zur Vermeidung von Prioritätsinversion, die periodische Ausführung zu absoluten Zeitpunkten mit delay until, das Ravenscar-Profil, Timing-Ereignisse sowie die Messung der Ausführungszeit je Task mit Ada.Execution_Time. Charakteristisch ist, dass dies nicht nachträglich als Bibliothek hinzugefügt wurde, sondern direkt in die Sprachlaufzeit selbst integriert ist.
Was ist Prioritätsinversion? Wie verhindert Ada sie?
Prioritätsinversion ist das Phänomen, dass eine Task mit niedriger Priorität, die einen Lock hält, von einer Task mit mittlerer Priorität präemptiert wird, wodurch eine Task mit hoher Priorität, die auf denselben Lock wartet, unbegrenzt blockiert bleibt. Dies trat 1997 beim Mars Pathfinder tatsächlich auf und führte dazu, dass die Sonde wiederholt zurückgesetzt wurde. Ada stellt dafür das Ceiling_Locking-Protokoll als Sprachfeature bereit: Eine Task, die ein geschütztes Objekt betritt, wird automatisch auf dessen Ceiling-Priorität angehoben, wodurch eine Präemption durch eine mittelpriorisierte Task verhindert wird.
Was ist das Ravenscar-Profil?
Es ist ein Profil, das für sicherheitskritische Systeme Adas Tasking-Funktionen auf eine statisch analysierbare, deterministische Teilmenge beschränkt. Verboten sind unter anderem dynamische Task-Erzeugung, die select-Anweisung, die abort-Anweisung, relatives delay und die requeue-Anweisung. Diese Einschränkung erleichtert die statische Zeitanalyse und damit die Erfüllung von Eigenschaften, die Sicherheitsnormen wie DO-178C (Luftfahrtsoftware) oder ISO 26262 (funktionale Sicherheit im Automobilbau) verlangen. Bei GNAT wird es aktiviert, indem pragma Profile (Ravenscar) in eine gnat.adc-Datei geschrieben wird.
Warum delay until statt delay bei periodischen Tasks verwenden?
Weil sich bei einem relativ angegebenen delay die Verarbeitungszeit jeder Iteration aufsummiert und die Periode allmählich abdriftet (kumulative Drift). delay until legt den nächsten Startzeitpunkt anhand eines absoluten Zeitpunkts fest, sodass selbst bei einer Verzögerung eines einzelnen Durchlaufs die folgenden Startzeitpunkte korrekt bleiben. Überschreitet die Verarbeitung jedoch den nächsten Aufwachzeitpunkt, kehrt delay until nahezu sofort zurück; dafür ist gesondert ein Design nötig, das dies als deadline miss erkennt und als Überlast behandelt.

Autorenprofil

Profilseite des Artikelautors.

Go Komura

Geschäftsführer von KomuraSoft LLC

Spezialisiert auf Windows-Softwareentwicklung, technische Beratung und Fehleranalyse, insbesondere bei bestehenden Systemen und schwer reproduzierbaren Störungen.

Zurück zum Blog