Programmation de systèmes temps réel avec Ada ── Priorités, exécution périodique et contrôle du temps d'exécution en pratique
· Go Komura · Ada, Temps Réel, Ravenscar, CeilingLocking, Tâches, Ordonnancement, Inversion de Priorité, Langage de Programmation
1. Introduction ── La relation profonde entre Ada et le temps réel
Notre article précédent, « Concurrence sécurisée en Ada », a présenté les bases de la concurrence sûre reposant sur les tâches et les objets protégés d’Ada. Cette fois, nous allons plus loin, dans un domaine encore plus contraint — les systèmes temps réel.
Dans un système temps réel, la « correction » ne se limite pas à l’exactitude logique du résultat du calcul : elle inclut aussi le fait que ce résultat soit obtenu dans les délais. Une réponse correcte obtenue avec une milliseconde de retard est aussi dangereuse qu’une réponse erronée.
Ada répond à cette exigence avec un ensemble complet de fonctionnalités temps réel normalisées dans l’Annexe D (Real-Time Systems) de la spécification du langage. Il ne s’agit pas d’une bibliothèque « ajoutée après coup », mais d’une garantie temps réel intégrée directement au runtime du langage.
Fonctionnalités temps réel d'Ada (Annexe D) :
- Priorités de tâches et préemption (FIFO_Within_Priorities)
- Protocole Ceiling_Locking (prévention de l'inversion de priorité)
- Exécution périodique à temps absolu avec delay until
- Profil Ravenscar (sous-ensemble critique pour la sûreté)
- Événements temporisés (réveil à heure fixe sans scrutation)
- Surveillance du temps d'exécution (Ada.Execution_Time)
- Ordonnancement multi-périodique
Cet article présente ces fonctionnalités de façon progressive à travers 8 exemples de code pratiques. Chaque extrait peut être considéré comme un exemple indépendant, mais les exemples 04 et 05, qui contiennent plusieurs unités de compilation, doivent d’abord être séparés avec gnatchop avant d’être compilés avec gnatmake.
Les extraits de code de cet article sont par ailleurs disponibles, organisés par chapitre, dans un dépôt de référence sur GitHub.
ada-real-time-systems - komurasoft-blog-samples (GitHub)
2. Qu’est-ce qu’un système temps réel ?
Commençons par clarifier le vocabulaire.
| Concept | Description |
|---|---|
| Temps réel dur (Hard real-time) | Le dépassement d’une échéance constitue un échec catastrophique du système (commandes de vol, airbags, stimulateurs cardiaques) |
| Temps réel souple (Soft real-time) | Le dépassement d’une échéance est indésirable mais des dépassements occasionnels sont tolérés (streaming vidéo, jeux) |
| Échéance (deadline) | L’instant absolu auquel une tâche doit être terminée |
| Période (period) | L’intervalle de temps auquel une tâche est déclenchée de façon répétée |
| WCET (Worst-Case Execution Time) | Le temps d’exécution le plus défavorable d’une tâche |
| Gigue (jitter) | La variation dans l’exécution périodique |
Dans la conception d’un système temps réel, il est essentiel que « WCET <= échéance » soit vérifié pour chaque tâche. Cela ne garantit toutefois pas, à soi seul, que l’ensemble du système respecte ses échéances : il faut également une analyse du temps de réponse prenant en compte le temps de blocage, l’attribution des priorités, la gigue, les interruptions, ainsi que le comportement du runtime et de l’OS. En pratique, on vise plutôt WCET < échéance afin de conserver une marge. Les fonctionnalités temps réel d’Ada fournissent, au niveau du langage, un modèle d’exécution prévisible qui facilite cette analyse.
flowchart LR
HRT[Temps réel dur] -->|Échéance non tenue = échec critique| Examples[Commandes de vol<br/>Airbag<br/>Stimulateur cardiaque]
SRT[Temps réel souple] -->|Dépassement occasionnel toléré| Examples2[Streaming vidéo<br/>Jeux<br/>UI]
Ada[Ada Annexe D<br/>Mécanismes au service de la prévisibilité] --> Mechanism[FIFO_Within_Priorities<br/>Ceiling_Locking<br/>delay until]
subgraph Requirements[Exigences temps réel]
D[Échéance<br/>Instant absolu à respecter]
P[Période<br/>Intervalle de répétition]
W[WCET<br/>Temps d'exécution pire cas]
J[Gigue<br/>Variation de la période]
end
D --> Analysis[Analyse d'ordonnançabilité]
P --> Analysis
W --> Analysis
J --> Analysis
HRT --> Analysis
SRT --> Analysis
Mechanism --> Analysis
Analysis --> Constraint[Condition nécessaire : WCET <= échéance<br/>La suffisance se vérifie par l'analyse du temps de réponse]
L’un des phénomènes les plus dangereux dans les systèmes temps réel est l’inversion de priorité. Ce problème s’est réellement produit sur la sonde Mars Pathfinder en 1997, provoquant des redémarrages répétés de l’engin.
sequenceDiagram
participant S as Ordonnanceur
participant L as Tâche basse priorité
participant H as Tâche haute priorité
participant M as Tâche priorité moyenne
participant R as Ressource partagée
L->>R: Acquiert le verrou
activate L
Note over L: Exécution de la section critique
Note over S,L: H se réveille, l'ordonnanceur suspend L
deactivate L
activate H
H->>R: Tente d'acquérir le verrou
Note over H: Bloquée ! (L le détient toujours)
deactivate H
Note over S,L: H attend le verrou, donc L reprend
activate L
Note over L: Continue vers la libération du verrou...
Note over S,L: M se réveille, l'ordonnanceur suspend L
deactivate L
activate M
Note over L: L ne peut pas libérer le verrou
Note over M: M s'exécute librement (H et L bloquées)
Note over H: 【INVERSION DE PRIORITÉ】Haute priorité bloquée indéfiniment
deactivate M
Une tâche de faible priorité qui détient un verrou se fait préempter par une tâche de priorité intermédiaire, et la tâche de haute priorité se retrouve bloquée indéfiniment. Sur Mars Pathfinder, la contre-mesure effectivement appliquée fut l’activation de l’héritage de priorité (priority inheritance) de VxWorks ; Ada, lui, répond à ce même type de problème avec un mécanisme différent fourni comme fonctionnalité du langage : Ceiling_Locking.
3. Les bases des priorités de tâches ── FIFO_Within_Priorities
FIFO_Within_Priorities est une politique d’ordonnancement standard basée sur les priorités, que l’Annexe D d’Ada permet de spécifier explicitement. Si aucune politique n’est précisée, le comportement par défaut dépend de l’implémentation ; sous GNAT, cette famille de politiques est utilisée sur de nombreuses cibles. Au sein d’un même niveau de priorité, les tâches s’exécutent en FIFO (premier arrivé, premier servi), et une tâche de priorité supérieure préempte (interrompt) toute tâche de priorité inférieure.
-- 01_task_priority.ada
-- Bases de la priorité de tâche et de FIFO_Within_Priorities
-- Le pragma de configuration doit précéder toutes les clauses de contexte
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;
Points clés :
pragma Priorityattribue une priorité statique à chaque tâche.Priority'Lastest la plus élevée,Priority'Firstla plus basse.- Le corps des tâches de cette démonstration n’effectue pas de calcul lourd ; il attend simplement jusqu’à l’instant indiqué avec
delay until. Ce que l’on souhaite vérifier ici, c’est que la tâche de priorité supérieure obtient la première opportunité d’exécution dès que les deux tâches deviennent exécutables simultanément. - Ce schéma illustre le volet préemption entre priorités différentes de
FIFO_Within_Priorities. Pour vérifier l’ordre FIFO au sein d’un même niveau de priorité, il faudrait un autre exemple avec plusieurs tâches de même priorité. - Dans un système réel, il est courant de concevoir les priorités de façon relative par rapport à
System.Default_Priority.
sequenceDiagram
participant S as Ordonnanceur
participant Main as Tâche principale
participant HP as Tâche haute priorité<br/>(Priority=Last)
participant LP as Tâche basse priorité<br/>(Priority=First)
Main->>HP: Création de la tâche
Main->>LP: Création de la tâche
Note over HP,LP: T=0ms : les deux tâches sont exécutables
S->>HP: Sélectionne HP, priorité la plus élevée
activate HP
Note over HP: Affiche le message de démarrage
HP->>S: Se bloque jusqu'à T+100ms
deactivate HP
S->>LP: Exécute LP ensuite
activate LP
Note over LP: Affiche le message de démarrage
LP->>S: Se bloque jusqu'à T+500ms
deactivate LP
Note over S: T=100ms : HP se réveille
S->>HP: Exécute HP
activate HP
Note over HP: Affiche le message de fin
deactivate HP
Note over S: T=500ms : LP se réveille
S->>LP: Exécute LP
activate LP
Note over LP: Affiche le message de fin
deactivate LP
Note over Main: (T=800ms) Fin de la tâche principale
Plage de priorités d'Ada (valeurs par défaut de GNAT) :
Priority'First = 0 (la plus basse)
Priority'Last = 30 (la plus élevée, dépend toutefois de l'OS)
4. Ceiling_Locking ── Quand le langage empêche l’inversion de priorité
L’un des problèmes les plus délicats des systèmes temps réel est l’inversion de priorité (priority inversion) : une tâche de haute priorité attend un verrou détenu par une tâche de faible priorité, et cette dernière se fait préempter par une tâche de priorité intermédiaire, ce qui bloque indéfiniment la tâche de haute priorité.
Ada répond à ce problème en intégrant directement le protocole Ceiling_Locking dans les objets protégés.
-- 02_ceiling_locking.ada
-- Le protocole Ceiling_Locking empêche l'inversion de priorité
-- Le pragma de configuration doit précéder toutes les clauses de contexte
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;
Fonctionnement de Ceiling_Locking :
- Une priorité plafond (ceiling priority) est fixée sur l’objet protégé avec
pragma Priority (Ceiling). - Quelle que soit la tâche qui entre dans l’objet protégé, elle est automatiquement élevée à la priorité plafond dès son entrée.
- Cela empêche toute tâche de priorité intermédiaire de préempter la tâche en train d’utiliser l’objet protégé.
- En sortant de l’objet protégé, la tâche retrouve sa priorité d’origine.
Le schéma ci-dessous n’est pas une trace temporelle exacte du code d’exemple précédent : c’est un schéma conceptuel montrant comment le motif d’inversion de priorité de la Figure 2 est contenu grâce à Ceiling_Locking.
sequenceDiagram
participant S as Ordonnanceur
participant L as Tâche basse priorité<br/>(priorité=10)
participant M as Tâche priorité moyenne<br/>(priorité=20)
participant H as Tâche haute priorité<br/>(priorité=30)
participant PO as Objet protégé<br/>(plafond=30)
Note over PO: Un appelant dont la priorité active > priorité plafond déclenche Program_Error<br/>H(30) est égal au plafond (30), donc elle peut entrer
L->>PO: Entre dans l'opération protégée
activate L
Note over L,PO: Priorité active élevée à 30
Note over S: M se réveille
Note over S,L: L s'exécute au plafond 30<br/>M(20) ne peut pas la préempter
Note over S: H se réveille
Note over S,H: H(30) passe le contrôle de plafond<br/>mais attend car L utilise PO
L->>PO: Exécute l'opération
L->>PO: Sort de l'opération protégée
deactivate L
Note over L: Priorité restaurée à 10
Note over S,H: H s'exécute après la libération de PO
activate H
H->>PO: Entre dans l'opération protégée
Note over H,PO: H(30) = plafond (30), donc elle peut entrer une fois la contention résolue
H->>PO: Sort de l'opération protégée
deactivate H
Règle de conception : la priorité plafond d’un objet protégé doit être fixée au moins aussi haut que la priorité la plus élevée parmi toutes les tâches qui utilisent cet objet. Si une tâche dont la priorité active dépasse le plafond appelle une opération protégée, Ada peut détecter cette erreur de conception en déclenchant
Program_Error.
Obtenir le même résultat avec les mutex pthread du langage C nécessite de définir explicitement l’attribut PTHREAD_PRIO_PROTECT, alors qu’en Ada, il s’agit d’une fonctionnalité standard du langage.
5. delay until ── Exécuter une tâche périodique sans dérive
Le motif fondamental des systèmes temps réel est la tâche périodique : une tâche exécutée de façon répétée à intervalle fixe. Il est absolument essentiel d’éviter l’erreur temporelle cumulative (la dérive).
Le delay until d’Ada résout ce problème avec élégance.
-- 03_periodic_task.ada
-- delay until pour une tâche périodique ── empêche la dérive cumulative
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;
Pourquoi delay until plutôt qu’un simple delay :
| Méthode | Problème |
|---|---|
delay Period; |
Le temps de traitement de chaque itération s’accumule ; la période dérive progressivement (dérive cumulative) |
delay until Next_Release; Next_Release := Next_Release + Period; |
Basé sur un temps absolu : même si un traitement prend du retard, la prochaine date de réveil reste correcte |
Cependant, delay until ne garantit pas automatiquement que le traitement tienne dans la période. Si le traitement dépasse la prochaine date de réveil, ce delay until revient presque immédiatement, et le système se retrouve dans un état qui doit être traité comme un dépassement d’échéance (deadline miss).
Avec delay :
T=0ms → traitement(15ms) → delay 100ms → T=115ms → traitement(10ms) → ...
Intervalles réels : 115ms, 110ms, ... (le temps de traitement s'accumule)
Avec delay until :
Next_Release : 100ms, 200ms, 300ms, ... (temps absolu)
T=0ms → traitement(15ms) → delay until 100ms → T=100ms → traitement(10ms) → delay until 200ms
Intervalles réels : 100ms, 100ms, ... (indépendants du temps de traitement)
Ce motif delay until est utilisé dans toutes les tâches périodiques présentées par la suite.
flowchart TB
subgraph Bad["delay Period - Dérive cumulative"]
B1[T=0ms : calcul 15ms] --> B2[delay 100ms → réveil à 115ms]
B2 --> B3[calcul 10ms → 125ms]
B3 --> B4[delay 100ms → réveil à 225ms]
B4 --> B5[Intervalles réels : 115ms, 110ms...]
end
subgraph Good["delay until - Basé sur le temps absolu"]
G1[Prochaine = T+100ms] --> G2[calcul 15ms]
G2 --> G3[delay until T+100ms → réveil à 100ms]
G3 --> G4[calcul 10ms]
G4 --> G5[Prochaine = T+200ms → réveil à 200ms]
G5 --> G6[Intervalles réels : 100ms, 100ms...]
end
subgraph Overrun["Dépassement de période - deadline miss"]
O1[Prochaine = T+100ms] --> O2[calcul 130ms]
O2 --> O3[delay until T+100ms revient immédiatement]
O3 --> O4[Le retard est détecté et traité comme une surcharge]
end
Bad --> Drift[L'erreur s'accumule avec le temps]
Good --> Stable[Empêche la dérive cumulative]
Good --> Overrun
6. Le profil Ravenscar ── Un sous-ensemble temps réel vérifiable
Les fonctionnalités de tâches d’Ada sont puissantes, mais dans les systèmes où la sûreté est absolument critique, cette puissance devient elle-même un problème. La création dynamique de tâches, les instructions select, les instructions abort, etc., rendent difficile l’analyse statique du temps d’exécution pire cas.
Le profil Ravenscar est la réponse qu’apporte Ada à ce problème : il restreint les fonctionnalités de tâches à un sous-ensemble statiquement analysable et déterministe.
-- 04_ravenscar_profile.ada
-- Bases du profil Ravenscar
-- Activer avec : pragma Profile (Ravenscar); dans un fichier gnat.adc
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;
Restrictions du profil Ravenscar :
| Fonctionnalité interdite | Raison |
|---|---|
Création dynamique de tâches (new ou types accès) |
L’allocation mémoire à l’exécution est non déterministe |
Instructions select |
Pas seulement les alternatives multiples : l’instruction select dans son ensemble complique l’analyse du flux de contrôle |
Instructions abort |
L’interruption asynchrone rend l’état imprévisible |
Ada.Task_Attributes |
Comportement dynamique à l’exécution |
| Changement dynamique de priorité | Les hypothèses de l’analyse d’ordonnancement changent à l’exécution |
delay relatif |
Tend à produire une dérive cumulative ; utiliser le delay until à temps absolu |
| Plusieurs entrées par objet protégé | Augmente les conditions de blocage et le périmètre de l’analyse |
| Terminaison de tâche | Ravenscar traite toutes les tâches comme non terminantes |
Instruction requeue |
Complique le suivi du flux de contrôle |
Ces restrictions rendent un programme conforme à Ravenscar beaucoup plus propice à l’analyse temporelle statique — une propriété exigée par des normes de sûreté telles que DO-178C (logiciel aéronautique) ou ISO 26262 (sécurité fonctionnelle automobile). La liste ci-dessus n’est qu’un extrait des principales restrictions ; le profil réel comprend également des règles supplémentaires liées au runtime et à l’analysabilité, telles que No_Task_Hierarchy ou Detect_Blocking.
flowchart TB
Full[Fonctionnalités complètes de tâches d'Ada] --> Profile[Profil Ravenscar]
Profile --> Restrict[Restrictions]
Profile --> Policy[Politiques obligatoires]
Restrict --> R1[Interdiction de la création dynamique de tâches]
Restrict --> R2[Interdiction de l'instruction select]
Restrict --> R3[Interdiction de l'instruction abort]
Restrict --> R4[Interdiction de Task_Attributes]
Restrict --> R5[Limité à 1 entrée par objet protégé]
Restrict --> R6[Interdiction de l'instruction requeue]
Restrict --> R7[Interdiction du delay relatif<br/>utiliser delay until]
Restrict --> R8[Interdiction du changement dynamique de priorité]
Restrict --> R9[Interdiction de la terminaison de tâche<br/>toutes les tâches non terminantes]
Policy --> P1[FIFO_Within_Priorities]
Policy --> P2[Ceiling_Locking]
Restrict --> Benefit[Facilite :<br/>l'analyse temporelle statique]
Policy --> Benefit
Benefit --> DO178[DO-178C<br/>Logiciel aéronautique]
Benefit --> ISO26262[ISO 26262<br/>Sécurité fonctionnelle automobile]
Benefit --> IEC62304[IEC 62304<br/>Logiciel de dispositif médical]
Pour activer le profil Ravenscar, il suffit d’inscrire ceci dans un fichier gnat.adc :
pragma Profile (Ravenscar);
7. Événements temporisés ── Un réveil piloté par le temps, sans scrutation
De nombreux systèmes temps réel ont besoin de « réveiller une tâche de haute priorité à un instant donné ». Une implémentation naïve reposerait sur la scrutation (polling) d’un minuteur, mais Ada propose un mécanisme plus élégant : les événements temporisés (timing events).
-- 05_timing_events.ada
-- Événements temporisés (Ada.Real_Time.Timing_Events)
-- Réveille une tâche de haute priorité sans scrutation
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;
Fonctionnement des événements temporisés :
1. Set_Handler(Timer_1, T+100ms, S.Fire'Access) ── enregistre un gestionnaire à un instant absolu
2. T+100ms écoulé ── le runtime appelle S.Fire à **la priorité plafond**
3. Fire positionne le drapeau Fired à True ── la barrière s'ouvre
4. La tâche Reactor se réveille depuis Wait_For_Event
Ce qui compte ici, c’est que cet exemple spécifie explicitement Ceiling_Locking, et que le gestionnaire Fire étant une procédure protégée, il s’exécute à la priorité plafond de l’objet protégé. Une procédure protégée utilisée comme gestionnaire d’événement temporisé doit se trouver dans un objet protégé dont la priorité plafond est de niveau interruption, ici System.Interrupt_Priority'Last. Cela garantit qu’aucune inversion de priorité ne se produit lors du traitement de l’événement temporisé.
8. Une file d’attente temps réel avec les objets protégés
Un motif fréquent dans les systèmes temps réel est le schéma producteur-consommateur : un capteur génère des données, et une tâche de contrôle les consomme. L’exclusion mutuelle et le blocage du tampon doivent alors être gérés efficacement.
Les objets protégés et les barrières d’entrée d’Ada permettent d’implémenter cela comme une synchronisation basée sur des barrières. Le runtime gère l’exclusion mutuelle en interne, sans que le code applicatif n’ait à écrire directement de mutex ni de variable de condition.
-- 06_protected_queue.ada
-- Partage de données temps réel via un objet protégé
-- Pipeline : Producer -> Bounded_Buffer -> Consumer
-- Activer avec : pragma Locking_Policy (Ceiling_Locking); dans un fichier gnat.adc
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;
Points clés de la conception :
entry Put when Count < Buffer_Size── si le tampon est plein, le Producer se bloque automatiquement.entry Get when Count > 0── si le tampon est vide, le Consumer se bloque automatiquement.pragma Priority (System.Any_Priority'Last)── grâce au ceiling locking, aucune inversion de priorité ne se produit entre le Producer et le Consumer.- Les conditions de barrière sont définies à partir de l’état interne de l’objet protégé (
Count), et sont réévaluées automatiquement à chaque libération du verrou.
Ce code ne comporte aucun mutex, sémaphore ou variable de condition côté application. Les mises en attente nécessaires sont exprimées uniquement à travers les barrières d’entrée de l’objet protégé.
stateDiagram-v2
Empty: Vide / Count=0
Partial: Partiel / Count=1..Buffer_Size-1
Full: Plein / Count=Buffer_Size
[*] --> Empty: État initial
Empty --> Partial: Put (ajoute 1 élément)
Partial --> Partial: Put / Get
Partial --> Empty: Get (retire le dernier élément)
Partial --> Full: Put (comble la dernière place)
Full --> Partial: Get (libère une place)
Empty --> Empty: Get se bloque (barrière Count=0)
Full --> Full: Put se bloque (barrière Count=Buffer_Size)
Un Put réussi réévalue les Get en attente, et un Get réussi réévalue les Put en attente. Cette réévaluation de barrière a lieu à la fin de l’opération protégée, indépendamment de l’état représenté dans le schéma.
9. Mesurer le temps d’exécution ── Le premier pas vers la surveillance du temps d’exécution
Pour évaluer l’ordonnançabilité d’un système temps réel, il faut connaître précisément le temps d’exécution (temps CPU) de chaque tâche. Le paquetage Ada.Execution_Time d’Ada fournit le décompte du temps CPU consommé, par tâche.
-- 07_execution_time.ada
-- Contrôle du temps d'exécution (Execution_Time)
-- Mesure la consommation CPU par tâche
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;
Temps horloge murale vs temps CPU :
Temps horloge murale (Wall Clock) : Ada.Real_Time.Clock
→ Temps réellement écoulé. Inclut les périodes de blocage et de préemption.
Temps CPU (Execution Time) : Ada.Execution_Time.Clock
→ Seulement le temps où la tâche a réellement été exécutée sur le CPU.
→ Les périodes de blocage et de préemption ne sont pas comptées.
Cette distinction constitue le point de départ de la surveillance du temps d’exécution et de la validation du WCET. Pendant que Busy_Worker attend dans un delay until, son temps CPU n’augmente pas ; il n’augmente que pendant le calcul effectif. De même, pendant le delay until Clock + Milliseconds(500) de la tâche principale, le temps CPU devrait rester quasiment nul. Cependant, la mesure du temps CPU ne garantit pas le véritable WCET : le cache, le pipeline, la contention mémoire, etc., nécessitent une analyse statique ou une validation séparée sur l’environnement cible.
flowchart LR
subgraph Wall[Temps horloge murale]
W1[Total écoulé : 500ms] --> W2[Comprend : calcul + attente + blocage + préemption]
end
subgraph CPU[Temps CPU]
C1[Total CPU : 120ms] --> C2[Uniquement : le calcul réel]
end
Wall --> Diff[Écart = temps d'attente, de blocage et de préemption]
CPU --> Diff
Diff --> Insight[Le temps CPU observe le coût de calcul réel<br/>Aide à la validation et à la surveillance du WCET<br/>Exclut l'attente, le blocage et la préemption]
Insight --> Caveat[Attention<br/>La mesure ne garantit pas le véritable WCET<br/>Une analyse statique ou une validation cible reste nécessaire]
10. Démonstration intégrée ── Un système temps réel multi-périodique
Intégrons maintenant tous les éléments vus jusqu’ici — priorités, Ceiling_Locking, delay until, objets protégés — pour construire un système temps réel multi-périodique typique.
-- 08_multiperiodic.ada
-- Démonstration intégrée d'un système temps réel multi-périodique
-- Tâche de lecture de capteur à cycle rapide (100ms)
-- Tâche de contrôle à cycle lent (400ms)
-- Partage de données via 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;
Architecture du système :
Le schéma ci-dessous s’appuie sur les instants de release du code d’exemple (capteur rapide à cycle de 100ms, contrôle lent à cycle de 400ms avec un décalage de 150ms), en supposant des temps d’exécution donnés à titre illustratif. Le code lui-même ne contient pas de calcul de contrôle de 80ms ; ce n’est donc pas un relevé de mesure réel. Si le capteur rapide a la priorité la plus élevée, une release survenant pendant l’exécution du contrôle lent interrompt momentanément ce dernier. Pour rester lisible, le schéma ne représente qu’une interruption représentative par cycle de contrôle lent, alors qu’en réalité le capteur rapide est relâché à chaque frontière de 100ms.
flowchart TB
Assumption["Hypothèse illustrative<br/>Capteur rapide : traitement 10ms<br/>Contrôle lent : traitement 80ms"]
subgraph Cycle1["Contrôle lent cycle 1 (release=150ms)"]
direction LR
C1F1["100-110ms<br/>Capteur rapide #1"] --> C1S1["150-200ms<br/>Contrôle lent #1 partie A"]
C1S1 --> C1F2["200-210ms<br/>Capteur rapide #2<br/>P+3, donc interruption"]
C1F2 --> C1S2["210-240ms<br/>Contrôle lent #1 partie B"]
end
subgraph Cycle2["Contrôle lent cycle 2 (release=550ms)"]
direction LR
C2S1["550-600ms<br/>Contrôle lent #2 partie A"] --> C2F6["600-610ms<br/>Capteur rapide #6<br/>P+3, donc interruption"]
C2F6 --> C2S2["610-640ms<br/>Contrôle lent #2 partie B"]
end
subgraph Cycle3["Contrôle lent cycle 3 (release=950ms)"]
direction LR
C3S1["950-1000ms<br/>Contrôle lent #3 partie A"] --> C3F10["1000-1010ms<br/>Capteur rapide #10<br/>P+3, donc interruption"]
C3F10 --> C3S2["1010-1040ms<br/>Contrôle lent #3 partie B"]
end
Assumption --> C1F1
C1S2 --> C2S1
C2S2 --> C3S1
Ce motif — « acquisition rapide par capteur + boucle de contrôle lente » — est un schéma classique fréquemment rencontré dans les systèmes de contrôle industriel et la robotique.
11. Les domaines où les fonctionnalités temps réel d’Ada excellent
Les fonctionnalités temps réel d’Ada apportent une valeur particulière dans les domaines suivants :
flowchart TB
Ada[Ada Annexe D<br/>Fonctionnalités temps réel] --> Aero[Aérospatial<br/>DO-178C]
Ada --> Rail[Ferroviaire<br/>Famille EN 50128]
Ada --> Auto[Automobile<br/>ISO 26262]
Ada --> Medical[Dispositifs médicaux<br/>IEC 62304]
Ada --> Industrial[Contrôle industriel<br/>Famille IEC 61508]
Ada --> Defense[Défense et systèmes à haute fiabilité]
Aero --> A1[Commandes de vol<br/>domaine d'application historique]
Aero --> A2[Contrôle de satellites et d'engins spatiaux]
Rail --> R1[Systèmes de signalisation]
Rail --> R2[Contrôle automatique des trains]
Auto --> Au1[Candidat pour les ECU liés à la sécurité]
Auto --> Au2[Usage limité et sélectif dans un domaine<br/>dominé par le C / MISRA-C]
Medical --> M1[Stimulateurs cardiaques]
Medical --> M2[Pompes à perfusion]
Industrial --> I1[Contrôle de robots]
Industrial --> I2[Machines-outils à commande numérique]
Defense --> D1[Calculateurs de mission]
Defense --> D2[Systèmes à exploitation de longue durée]
12. Points d’attention et limites
Les fonctionnalités temps réel d’Ada sont puissantes, mais elles ne sont pas une solution miracle.
1. Dépendance à la plateforme :
- Le mappage réel de
pragma Prioritydépend de l’environnement d’exécution (OS + runtime GNAT). Sous Linux, il est mappé surSCHED_FIFO, mais sous Windows, une préemption complète n’est pas toujours garantie.
2. Contraintes de Ravenscar :
- La création dynamique de tâches étant interdite, toutes les tâches doivent être déclarées statiquement au démarrage du système. Cela limite la liberté de conception.
3. Limites de la mesure du WCET :
Ada.Execution_Timefournit une mesure, pas une garantie. Le véritable WCET, incluant les défauts de cache et les aléas de pipeline, doit être vérifié séparément avec des outils d’analyse statique.
4. Surcoût :
- L’évaluation des barrières d’un objet protégé s’exécute automatiquement lors de l’achèvement ou de l’annulation d’une entrée, ainsi qu’à la sortie de l’objet protégé. Pour des objets protégés appelés très fréquemment, ce surcoût doit être pris en compte.
5. Barrière de la chaîne d’outils :
- Exploiter pleinement les fonctionnalités temps réel d’Ada nécessite un compilateur croisé et un runtime adaptés. Sur les cibles embarquées en particulier, on dépendra du runtime fourni par le fournisseur.
13. Conclusion
Cet article a présenté, à travers 8 exemples de code progressifs, les fonctionnalités temps réel offertes par l’Annexe D d’Ada.
| Fonctionnalité | Valeur apportée |
|---|---|
| Priorités de tâches | Ordonnancement préemptif basé sur les priorités |
| Ceiling_Locking | Prévention de l’inversion de priorité intégrée au langage |
delay until |
Exécution périodique sans dérive cumulative |
| Profil Ravenscar | Sous-ensemble de tâches propice à l’analyse statique |
| Événements temporisés | Réveil piloté par le temps, sans scrutation |
| File protégée | Synchronisation basée sur les barrières d’un objet protégé |
| Mesure du temps d’exécution | Surveillance du temps CPU par tâche |
| Intégration multi-périodique | Conception permettant à des tâches de périodes différentes de cohabiter en toute sécurité |
L’essence des fonctionnalités temps réel d’Ada tient au fait qu’elles ne sont pas ajoutées après coup. Les règles de verrouillage qui limitent l’inversion de priorité, la spécification temporelle pour l’exécution périodique, la surveillance du temps d’exécution — tout cela est fourni comme partie intégrante de la spécification du langage. Bien entendu, le respect effectif des échéances doit être vérifié par la conception et l’analyse, mais le fait que le runtime du langage en pose les fondations constitue un atout majeur.
mindmap
root((Ada Annexe D<br/>Systèmes temps réel))
Ordonnancement
FIFO_Within_Priorities
Priorités de tâches
Préemption
Prévention de l'inversion de priorité
Protocole Ceiling_Locking
Élévation automatique de priorité
Règle de la priorité plafond
Exécution périodique
delay until
Empêche la dérive cumulative
Basé sur le temps absolu
Profil Ravenscar
Ensemble de tâches statique
Analyse déterministe
Interdiction du delay relatif
DO-178C / ISO 26262
Événements temporisés
Réveil sans scrutation
Enregistrement d'un gestionnaire protégé
Gestionnaire exécuté à la priorité plafond
Objets protégés / files
Barrières d'entrée
Gestion des états vide/partiel/plein
Synchronisation basée sur les barrières
Surveillance du temps d'exécution
Temps CPU par tâche
Horloge murale vs temps CPU
Aide à la validation et à la surveillance du WCET
Conception intégrée
Conception multi-périodique
Protection par barrières
Sûreté au niveau du langage
Pour aller plus loin et essayer concrètement le développement de systèmes temps réel en Ada, installez la chaîne d’outils GNAT via Alire, puis compilez les exemples de code de cet article avec gnatchop + gnatmake.
Pour les bases de la concurrence en Ada (tâches, rendez-vous, objets protégés), reportez-vous à notre article précédent, « Concurrence sécurisée en Ada ».
14. Références
- 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
- Recueil d’exemples de code Ada (GitHub)
Articles associés
Articles récents partageant les mêmes étiquettes, pour approfondir des sujets proches.
Concurrence sécurisée avec Ada ── Guide pratique des tâches et objets protégés
Un article d'introduction aux tâches et objets protégés, la concurrence intégrée au langage Ada. Il couvre le rendez-vous (entry/accept),...
La programmation générique en Ada ── Écrire des contrats par les types, réaliser une réutilisation à coût nul
Une présentation systématique de la programmation générique en Ada : sous-programmes génériques, paquets génériques, paramètres formels d...
Introduction à la vérification formelle avec SPARK ── Des contrats Ada à la preuve mathématique
Une introduction pratique à la vérification formelle avec SPARK, le sous-ensemble d'Ada. L'article couvre le passage des contrats (Pre/Po...
L'attrait du langage Ada ── Exprimer la conception par les types, faire fonctionner des logiciels pendant des décennies
Une introduction à l'attrait du langage Ada : le typage fort, les contraintes de plage, la séparation de la spécification et de l'impléme...
Gestion des erreurs et stratégie de nouvelle tentative dans Power Automate — éviter qu'un flux qui fonctionnait ne s'arrête sans que personne ne le remarque
Un ensemble de modèles de conception pour éviter qu'un flux Power Automate ne s'arrête sans que personne ne s'en aperçoive. Nous détaillo...
Sujets associés
Ces pages replacent le sujet dans un contexte plus large de services et de décisions.
Thèmes techniques Windows
Portail des sujets sur le développement Windows, l'analyse des incidents et la valorisation des actifs existants.
Questions fréquentes
Questions souvent posées lors d’une consultation sur le sujet de cet article.
- Qu'est-ce que l'Annexe D d'Ada ?
- C'est un ensemble de fonctionnalités pour les systèmes temps réel, normalisé comme partie intégrante de la spécification du langage Ada. Il comprend l'ordonnancement préemptif basé sur les priorités avec FIFO_Within_Priorities, le protocole Ceiling_Locking qui prévient l'inversion de priorité, l'exécution périodique à temps absolu avec delay until, le profil Ravenscar, les événements temporisés (timing events), ainsi que la mesure du temps d'exécution par tâche avec Ada.Execution_Time. Sa particularité est de ne pas être une bibliothèque ajoutée après coup, mais d'être intégré directement dans le runtime du langage lui-même.
- Qu'est-ce que l'inversion de priorité, et comment Ada l'empêche-t-elle ?
- C'est un phénomène dans lequel une tâche de faible priorité, tout en détenant un verrou, se fait préempter par une tâche de priorité intermédiaire, ce qui bloque indéfiniment une tâche de haute priorité en attente de ce même verrou. Ce problème s'est réellement produit sur la sonde Mars Pathfinder en 1997, provoquant des redémarrages répétés de l'engin. En Ada, le protocole Ceiling_Locking est fourni comme fonctionnalité du langage : toute tâche qui entre dans un objet protégé est automatiquement élevée à la priorité plafond (ceiling), ce qui empêche toute préemption par une tâche de priorité intermédiaire.
- Qu'est-ce que le profil Ravenscar ?
- C'est un profil qui restreint le modèle de tâches d'Ada à un sous-ensemble statiquement analysable et déterministe, destiné aux systèmes où la sûreté est absolument critique. Il interdit notamment la création dynamique de tâches, les instructions select, les instructions abort, les delay relatifs et les instructions requeue. Ces restrictions facilitent l'analyse temporelle statique et permettent de satisfaire plus facilement les propriétés exigées par des normes de sûreté telles que DO-178C (logiciel aéronautique) ou ISO 26262 (sécurité fonctionnelle automobile). Sous GNAT, on l'active en plaçant pragma Profile (Ravenscar) dans un fichier gnat.adc.
- Pourquoi utiliser delay until plutôt que delay pour une tâche périodique ?
- Avec delay, qui spécifie une durée relative, le temps de traitement de chaque itération s'accumule et le cycle dérive progressivement (dérive cumulative). delay until fixe la prochaine date de réveil sur une base de temps absolu : même si un traitement prend du retard, les dates de réveil suivantes restent correctes. Cependant, si le traitement dépasse la prochaine date de réveil, delay until revient presque immédiatement ; il faut alors concevoir séparément une détection de ce dépassement (deadline miss) et le traiter comme une situation de surcharge.
Profil de l’auteur
Page de présentation de l’auteur de l’article.
Go Komura
Représentant de KomuraSoft LLC
Spécialisé dans le développement de logiciels Windows, le conseil technique et l’analyse de pannes, notamment pour les systèmes existants et les incidents difficiles à reproduire.
Liens publics