Echtzeitsystemprogrammierung mit Ada — Priorität, Periodizität und Ausführungszeitsteuerung in der Praxis
· Go Komura · 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.
flowchart LR
HRT[Hard-Real-Time] -->|Fristüberschreitung = kritischer Ausfall| Examples[Flugsteuerung<br/>Airbag<br/>Herzschrittmacher]
SRT[Soft-Real-Time] -->|seltene Überschreitung tolerierbar| Examples2[Videostreaming<br/>Spiele<br/>UI]
Ada[Ada Annex D<br/>Mechanismen für Vorhersagbarkeit] --> Mechanism[FIFO_Within_Priorities<br/>Ceiling_Locking<br/>delay until]
subgraph Requirements[Echtzeitanforderungen]
D[Deadline<br/>abzuschließen bis Zeitpunkt]
P[Periode<br/>Wiederholungsintervall]
W[WCET<br/>Worst-Case-Ausführungszeit]
J[Jitter<br/>Schwankung der Periode]
end
D --> Analysis[Schedulability-Analyse]
P --> Analysis
W --> Analysis
J --> Analysis
HRT --> Analysis
SRT --> Analysis
Mechanism --> Analysis
Analysis --> Constraint[Notwendige Bedingung: WCET <= Deadline<br/>Hinlä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.
sequenceDiagram
participant S as Scheduler
participant L as Low-Priority-Task
participant H as High-Priority-Task
participant M as Medium-Priority-Task
participant R as Gemeinsame Ressource
L->>R: Erwirbt Lock
activate L
Note over L: Kritischer Abschnitt läuft
Note over S,L: H wacht auf, Scheduler unterbricht L
deactivate L
activate H
H->>R: Versucht, den Lock zu erwerben
Note over H: Blockiert! (L hält den Lock noch)
deactivate H
Note over S,L: H wartet auf den Lock, also läuft L weiter
activate L
Note over L: Setzt fort, um den Lock freizugeben...
Note over S,L: M wacht auf, Scheduler unterbricht L
deactivate L
activate M
Note over L: L kann den Lock nicht freigeben
Note over M: M läuft frei weiter (H und L stecken fest)
Note over H: 【PRIORITÄTSINVERSION】Hohe Priorität unbegrenzt blockiert
deactivate M
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 Priorityweist jeder Task eine statische Priorität zu.Priority'Lastist die höchste,Priority'Firstdie niedrigste.- Der Task-Rumpf dieser Demo führt keine aufwendige Berechnung durch, sondern wartet mit
delay untilbis 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_Prioritiesnur 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_Priorityzu entwerfen.
sequenceDiagram
participant S as Scheduler
participant Main as Main-Task
participant HP as Hochpriorisierte Task<br/>(Priority=Last)
participant LP as Niedrigpriorisierte Task<br/>(Priority=First)
Main->>HP: Task erzeugen
Main->>LP: Task erzeugen
Note over HP,LP: T=0ms: Beide Tasks sind runnable
S->>HP: Wählt HP mit höchster Priorität
activate HP
Note over HP: Gibt Start-Log aus
HP->>S: Blockiert bei delay until T+100ms
deactivate HP
S->>LP: Führt als Nächstes LP aus
activate LP
Note over LP: Gibt Start-Log aus
LP->>S: Blockiert bei delay until T+500ms
deactivate LP
Note over S: T=100ms: HP wacht auf
S->>HP: Führt HP aus
activate HP
Note over HP: Gibt Abschluss-Log aus
deactivate HP
Note over S: T=500ms: LP wacht auf
S->>LP: Führt LP aus
activate LP
Note over LP: Gibt Abschluss-Log aus
deactivate LP
Note over Main: (T=800ms) Main beendet
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:
- Am geschützten Objekt wird mit
pragma Priority (Ceiling)die Ceiling-Priorität festgelegt. - Unabhängig davon, welche Task das geschützte Objekt betritt, wird sie beim Eintritt automatisch auf die Ceiling-Priorität angehoben.
- Dadurch kann eine mittelpriorisierte Task eine Task, die das geschützte Objekt gerade nutzt, nicht präemptieren.
- 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.
sequenceDiagram
participant S as Scheduler
participant L as Niedrigpriorisierte Task<br/>(Priorität=10)
participant M as Mittelpriorisierte Task<br/>(Priorität=20)
participant H as Hochpriorisierte Task<br/>(Priorität=30)
participant PO as Geschütztes Objekt<br/>(Ceiling=30)
Note over PO: Ein Aufrufer mit aktiver Priorität > Ceiling löst Program_Error aus<br/>H(30) entspricht dem Ceiling(30) und darf daher eintreten
L->>PO: Betritt geschützte Operation
activate L
Note over L,PO: Ausführungspriorität steigt auf 30
Note over S: M wacht auf
Note over S,L: L läuft mit Ceiling-Priorität 30<br/>M(20) kann nicht präemptieren
Note over S: H wacht auf
Note over S,H: H(30) besteht die Ceiling-Prüfung<br/>wartet aber, da L das PO nutzt
L->>PO: Führt Operation aus
L->>PO: Verlässt geschützte Operation
deactivate L
Note over L: Priorität kehrt auf 10 zurück
Note over S,H: Nach Freigabe des PO läuft H
activate H
H->>PO: Betritt geschützte Operation
Note over H,PO: H(30) = Ceiling(30), tritt nach Auflösung der Konkurrenz ein
H->>PO: Verlässt geschützte Operation
deactivate H
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_Errorerkennen.
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.
flowchart TB
subgraph Bad["delay Period - kumulative Drift"]
B1[T=0ms: Berechnung 15ms] --> B2[delay 100ms → Aufwachen bei 115ms]
B2 --> B3[Berechnung 10ms → 125ms]
B3 --> B4[delay 100ms → Aufwachen bei 225ms]
B4 --> B5[Tatsächlicher Abstand: 115ms, 110ms...]
end
subgraph Good["delay until - absoluter Zeitbezug"]
G1[Nächster = T+100ms] --> G2[Berechnung 15ms]
G2 --> G3[delay until T+100ms → Aufwachen bei 100ms]
G3 --> G4[Berechnung 10ms]
G4 --> G5[Nächster = T+200ms → Aufwachen bei 200ms]
G5 --> G6[Tatsächlicher Abstand: 100ms, 100ms...]
end
subgraph Overrun["Periodenüberschreitung - deadline miss"]
O1[Nächster = T+100ms] --> O2[Berechnung 130ms]
O2 --> O3[delay until T+100ms kehrt sofort zurück]
O3 --> O4[Verspätung erkennen und als Überlast behandeln]
end
Bad --> Drift[Fehler kumuliert sich mit der Zeit]
Good --> Stable[Verhindert kumulative Drift]
Good --> Overrun
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.
flowchart TB
Full[Vollständige Ada-Tasking-Funktionen] --> Profile[Ravenscar-Profil]
Profile --> Restrict[Einschränkungen]
Profile --> Policy[Verpflichtende Policies]
Restrict --> R1[Verbot dynamischer Task-Erzeugung]
Restrict --> R2[Verbot der select-Anweisung]
Restrict --> R3[Verbot der abort-Anweisung]
Restrict --> R4[Verbot von Task_Attributes]
Restrict --> R5[Beschränkung auf 1 Entry je geschütztem Objekt]
Restrict --> R6[Verbot der requeue-Anweisung]
Restrict --> R7[Verbot von relativem delay<br/>delay until verwenden]
Restrict --> R8[Verbot dynamischer Prioritätsänderung]
Restrict --> R9[Verbot der Task-Beendigung<br/>alle Tasks nicht terminierend]
Policy --> P1[FIFO_Within_Priorities]
Policy --> P2[Ceiling_Locking]
Restrict --> Benefit[Wird dadurch einfacher:<br/>statische Zeitanalyse]
Policy --> Benefit
Benefit --> DO178[DO-178C<br/>Luftfahrtsoftware]
Benefit --> ISO26262[ISO 26262<br/>Funktionale Sicherheit im Automobilbau]
Benefit --> IEC62304[IEC 62304<br/>Software 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.
stateDiagram-v2
Empty: Leer / Count=0
Partial: Teilweise gefüllt / Count=1..Buffer_Size-1
Full: Voll / Count=Buffer_Size
[*] --> Empty: Ausgangszustand
Empty --> Partial: Put (1 Element hinzugefügt)
Partial --> Partial: Put / Get
Partial --> Empty: Get (letztes Element entnommen)
Partial --> Full: Put (letzten freien Platz gefüllt)
Full --> Partial: Get (Platz frei geworden)
Empty --> Empty: Get blockiert (Barriere Count=0)
Full --> Full: Put blockiert (Barriere 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.
flowchart LR
subgraph Wall[Wanduhrzeit]
W1[Gesamte verstrichene Zeit: 500ms] --> W2[Enthält: Berechnung + Warten + Blockiert + Präemptiert]
end
subgraph CPU[CPU-Zeit]
C1[CPU-Zeit gesamt: 120ms] --> C2[Enthält: nur tatsächliche Berechnung]
end
Wall --> Diff[Differenz = Warte-, Block- und Präemptionszeit]
CPU --> Diff
Diff --> Insight[CPU-Zeit erfasst die tatsächlichen Rechenkosten<br/>Hilft bei WCET-Validierung und Überwachung<br/>Schließt Warte-, Block- und Präemptionszeit aus]
Insight --> Caveat[Achtung<br/>Die Messung garantiert nicht den wahren WCET<br/>Statische 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.
flowchart TB
Assumption["Illustrative Annahme<br/>Schnelle Sensor: 10ms Verarbeitung<br/>Langsame Regelung: 80ms Verarbeitung"]
subgraph Cycle1["Langsame Regelung Zyklus 1 (release=150ms)"]
direction LR
C1F1["100-110ms<br/>Schneller Sensor #1"] --> C1S1["150-200ms<br/>Langsame Regelung #1 erste Hälfte"]
C1S1 --> C1F2["200-210ms<br/>Schneller Sensor #2<br/>unterbricht wegen P+3"]
C1F2 --> C1S2["210-240ms<br/>Langsame Regelung #1 zweite Hälfte"]
end
subgraph Cycle2["Langsame Regelung Zyklus 2 (release=550ms)"]
direction LR
C2S1["550-600ms<br/>Langsame Regelung #2 erste Hälfte"] --> C2F6["600-610ms<br/>Schneller Sensor #6<br/>unterbricht wegen P+3"]
C2F6 --> C2S2["610-640ms<br/>Langsame Regelung #2 zweite Hälfte"]
end
subgraph Cycle3["Langsame Regelung Zyklus 3 (release=950ms)"]
direction LR
C3S1["950-1000ms<br/>Langsame Regelung #3 erste Hälfte"] --> C3F10["1000-1010ms<br/>Schneller Sensor #10<br/>unterbricht wegen P+3"]
C3F10 --> C3S2["1010-1040ms<br/>Langsame Regelung #3 zweite Hälfte"]
end
Assumption --> C1F1
C1S2 --> C2S1
C2S2 --> C3S1
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.
flowchart TB
Ada[Ada Annex D<br/>Echtzeitfunktionen] --> Aero[Luft- und Raumfahrt<br/>DO-178C]
Ada --> Rail[Eisenbahn<br/>EN-50128-Familie]
Ada --> Auto[Automobilbau<br/>ISO 26262]
Ada --> Medical[Medizingeräte<br/>IEC 62304]
Ada --> Industrial[Industriesteuerung<br/>IEC-61508-Familie]
Ada --> Defense[Verteidigung und hochverlässliche Systeme]
Aero --> A1[Flugsteuerung<br/>bewährter Einsatzbereich]
Aero --> A2[Satelliten- und Raumfahrzeugsteuerung]
Rail --> R1[Signalsysteme]
Rail --> R2[Automatische Zugsteuerung]
Auto --> Au1[Kandidat für sicherheitsrelevante Steuergeräte]
Auto --> Au2[Begrenzte, selektive Anwendung im<br/>von C / MISRA-C dominierten Bereich]
Medical --> M1[Herzschrittmacher]
Medical --> M2[Infusionspumpen]
Industrial --> I1[Robotersteuerung]
Industrial --> I2[CNC-Werkzeugmaschinen]
Defense --> D1[Missionsrechner]
Defense --> D2[Langzeitbetriebssysteme]
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 Priorityhängt von der Ausführungsumgebung (Betriebssystem + GNAT-Laufzeit) ab. Unter Linux wird sie aufSCHED_FIFOabgebildet, 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_Timeliefert 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
- Ada Reference Manual - Annex D: Real-Time Systems
- Ravenscar Profile Definition (ISO/IEC TR 24718:2005)
- GNAT Real-Time Topics (AdaCore)
- The Ravenscar Profile for High-Integrity Systems (AdaCore)
- Rate Monotonic Analysis (Liu & Layland, 1973)
- Alire - Ada Package Manager
- Ada-Beispielcode-Sammlung (GitHub)
Verwandte Artikel
Aktuelle Artikel mit denselben Schlagwörtern führen zu verwandten Themen weiter.
Sichere Nebenläufigkeit mit Ada — Ein praktischer Leitfaden zu Tasks und geschützten Objekten
Ein Einführungsartikel zu Adas sprachintegrierter Nebenläufigkeit mit Tasks und geschützten Objekten. Er behandelt Rendezvous (entry/acce...
Generische Programmierung in Ada ── Verträge in Typen schreiben und Wiederverwendung ohne Laufzeitkosten
Eine systematische Einführung in die generische Programmierung in Ada - von generischen Unterprogrammen und Paketen über formale Unterpro...
Einführung in die formale Verifikation mit SPARK ── Von Ada-Verträgen zum mathematischen Beweis
Eine praxisorientierte Einführung in die formale Verifikation mit SPARK, der Ada-Teilsprache. Der Artikel zeigt den Weg von Verträgen (Pr...
Der Reiz der Sprache Ada — Design in Typen ausdrücken und Software, die jahrzehntelang läuft
Eine Einführung in den Reiz der Sprache Ada: starke Typisierung, Bereichsbeschränkungen, die Trennung von Spezifikation und Implementieru...
Windows-Prozessorzeitplanung – Hintergrunddienste und P-/E-Kerne
Was sich bei der Windows-Einstellung „Hintergrunddienste“ wirklich ändert – erklärt anhand von Quantum-Zeitscheiben, Foreground-Bevorzugu...
Verwandte Themen
Diese Seiten ordnen den Artikel in einen größeren Leistungs- und Entscheidungskontext ein.
Technische Windows-Themen
Portal zu Windows-Entwicklung, Fehleranalyse und der Nutzung bestehender Assets.
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.