Programmazione di sistemi real-time in Ada ── Priorità, periodi e controllo del tempo di esecuzione in pratica

· Aggiornato il: · · Ada, RealTime, Ravenscar, CeilingLocking, Tasking, Scheduling, PriorityInversion, ProgrammingLanguage, Real-time, Alta affidabilità

1. Introduzione ── Il legame profondo tra Ada e il real-time

Nell’articolo precedente, «Concorrenza sicura in Ada», abbiamo illustrato le basi della concorrenza sicura con i task e gli oggetti protetti di Ada. Questa volta proseguiamo su quella linea e entriamo in un dominio ancora più vincolato: i sistemi real-time.

In un sistema real-time, «correttezza» non significa solo che il risultato del calcolo sia logicamente giusto, ma anche che quel risultato arrivi entro il termine. Una risposta corretta arrivata con un millisecondo di ritardo è tanto pericolosa quanto una risposta sbagliata.

Di fronte a questo requisito, Ada offre un insieme completo di funzionalità real-time standardizzato come Annex D (Real-Time Systems) della specifica del linguaggio. Non è un’aggiunta a posteriori tramite libreria: è una garanzia real-time incorporata nel runtime del linguaggio stesso.

Funzionalità real-time di Ada (Annex D):
- Priorità dei task e preemption (FIFO_Within_Priorities)
- Protocollo Ceiling_Locking (prevenzione dell'inversione di priorità)
- Esecuzione periodica a istanti assoluti con delay until
- Profilo Ravenscar (sottoinsieme per sistemi safety-critical)
- Timing event (risveglio a un istante senza polling)
- Monitoraggio del tempo di esecuzione (Ada.Execution_Time)
- Scheduling multi-periodico

In questo articolo le esaminiamo passo dopo passo attraverso otto esempi di codice pratici. Ogni snippet si può trattare come esempio indipendente, ma gli esempi 04 e 05 contengono più unità di compilazione: vanno spezzati con gnatchop e poi compilati con gnatmake.

Lettori previsti e prerequisiti: diamo per nota la base di task, rendezvous e oggetti protetti trattata nell’articolo precedente. L’articolo è rivolto a chi sviluppa software di controllo per dispositivi embedded o sistemi ad alta affidabilità; non è un’introduzione alla sintassi di Ada.

Ambiente di verifica: abbiamo verificato che gli otto esempi di questo articolo si compilano con GNAT 13.3.0 (Ubuntu 24.04, x86-64). Anche gli output riportati nel testo (capitoli 3 e 5) sono stati raccolti nello stesso ambiente. Come priorità e preemption si comportino in pratica dipende dal sistema operativo e dal runtime GNAT (capitolo 12), quindi i dettagli dell’output possono cambiare da un ambiente all’altro.

I frammenti di codice che compaiono in questo articolo sono pubblicati su GitHub come raccolta di riferimento, organizzata in un file per capitolo.

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

Mappa della conoscenza di questo articolo

L’Annex D della specifica del linguaggio Ada incorpora nel runtime del linguaggio stesso FIFO_Within_Priorities basato sulla priorità dei task, il protocollo Ceiling_Locking che assegna automaticamente la priorità di ceiling agli oggetti protetti, l’esecuzione periodica con delay until che previene il drift cumulativo, il profilo Ravenscar più adatto all’analisi statica, i timing event che non richiedono polling, fino alla misura del tempo di esecuzione per task. Ceiling_Locking previene l’inversione di priorità che nel 1997 si è verificata realmente sul Mars Pathfinder e ha fatto sì che il lander si resetasse ripetutamente, elevando automaticamente alla priorità di ceiling il task che entra in un oggetto protetto. Il profilo Ravenscar vieta, tra le altre cose, il delay relativo e l’istruzione select, restringe i task Ada a un sottoinsieme più facile da sottoporre ad analisi statica dei tempi, e in GNAT lo si abilita scrivendo un pragma nel file gnat.adc. Tuttavia il mapping effettivo delle priorità dipende dal sistema operativo e dal runtime GNAT.

Mappa della conoscenza della programmazione di sistemi real-time in AdaDiagramma che mostra come l'Annex D di Ada incorpori, sopra i task e gli oggetti protetti, il dispatching basato su priorità, Ceiling_Locking, delay until, il profilo Ravenscar, i timing event e la misura del tempo di esecuzione, e come Ceiling_Locking prevenga un'inversione di priorità come quella verificatasi sul Mars Pathfinderusausausausausausarichiederichiederichiedeprevienepuò causarerichiederichiederichiedeconfigurato dapuò causareprevienesconsigliato perusausarichiedeAnnex D (standard Ada per i sistemi real-time)Protocollo Ceiling_LockingFIFO_Within_Prioritiesdelay until (ritardo a tempo assoluto)Profilo RavenscarEventi di temporizzazione (Ada.Real_Time.Timing_Events)Ada.Execution_Time (misura del tempo di esecuzione)Task Ada (concorrenza)Oggetto protetto (protected object)GNATInversione di priorità (priority inversion)Incidente di inversione di priorità del Mars Pathfinderdelay relativo (istruzione delay)Drift cumulativo dell'esecuzione periodica

Nel diagramma, una linea continua indica una relazione che vale sempre e una linea tratteggiata indica una relazione condizionale (le condizioni sono nella spiegazione di ciascuna relazione nella pagina di dettaglio). L’elenco completo delle relazioni (in totale 21, con evidenza e livello di certezza) e le definizioni dei concetti principali sono raccolti nella pagina di dettaglio della mappa della conoscenza (in giapponese). Dati: JSON-LD / Turtle

2. Che cos’è un sistema real-time

Partiamo dal chiarire i termini.

Concetto Descrizione
Hard real-time Superare la deadline significa un fallimento fatale del sistema (controllo di volo, airbag, pacemaker)
Soft real-time Superare la deadline è indesiderabile, ma superamenti rari sono tollerati (streaming video, giochi)
Deadline L’istante assoluto entro cui il task deve completare
Periodo (period) L’intervallo di tempo a cui il task viene attivato ripetutamente
WCET (Worst-Case Execution Time) Il tempo di esecuzione worst-case del task
Jitter La variabilità dell’esecuzione periodica
Schedulabilità (schedulability) La proprietà «quell’insieme di task può essere eseguito rispettando tutte le scadenze». Il lavoro di verificarla a tavolino è l’analisi di schedulabilità (analisi dei tempi di risposta e simili)
Profilo Ravenscar Convenzione che restringe le funzionalità di tasking di Ada a un sottoinsieme più facile da analizzare staticamente. Il nome deriva dal villaggio britannico di Ravenscar, dove si tenne la conferenza di definizione (capitolo 6)

Nella progettazione di un sistema real-time, una condizione necessaria importante per ciascun task è che valga «WCET <= deadline». Da sola, però, non garantisce che l’intero sistema rispetti le scadenze. Serve a parte un’analisi dei tempi di risposta che includa tempo di blocking, assegnazione delle priorità, jitter, interrupt e il comportamento del runtime e del sistema operativo. In pratica si punta a WCET < deadline per lasciare un margine. Le funzionalità real-time di Ada forniscono a livello di linguaggio un modello di esecuzione prevedibile che rende più facile svolgere quell’analisi.

Requisiti dei sistemi real-time e analisi di schedulabilitàHard e soft real-time, deadline, periodo, WCET e jitter confluiscono nell'analisi di schedulabilità, sostenuta dai meccanismi di Ada Annex D.Requisiti real-timeMancato rispetto = fallimento fataleSuperamenti rari sono tolleratiDeadlineIstante assoluto di completamentoPeriodoIntervallo di ripetizioneWCETTempo di esecuzione worst-caseJitterVariabilità del periodoHard real-timeControllo di voloAirbagPacemakerSoft real-timeStreaming videoGiochiUIAda Annex DMeccanismi per la prevedibilitàFIFO_Within_PrioritiesCeiling_Lockingdelay untilAnalisi di schedulabilitàCondizione necessaria: WCET &lt;= deadlineLa sufficienza si verifica con l'analisi dei tempi di risposta

Uno dei fenomeni più pericolosi nei sistemi real-time è l’inversione di priorità. Il problema si è verificato realmente sul Mars Pathfinder nel 1997 e ha fatto sì che il lander si resetasse ripetutamente.

Come si verifica l'inversione di prioritàUn task a bassa priorità che detiene un lock viene preemptato da un task a priorità media, e il task ad alta priorità resta bloccato indefinitamente.Risorsa condivisaTask a priorità mediaTask ad alta prioritàTask a bassa prioritàSchedulerRisorsa condivisaTask a priorità mediaTask ad alta prioritàTask a bassa prioritàSchedulerIn esecuzione nella critical sectionH si risveglia e lo scheduler sospende LBloccato in attesa del lock! (L lo detiene)H attende il lock, quindi L riprendeContinua verso il rilascio del lock...M si risveglia e lo scheduler sospende LL non può rilasciare il lockM continua a eseguire (H e L sono fermi)[INVERSIONE DI PRIORITÀ] Alta priorità bloccata indefinitamenteAcquisisce il lockTenta di acquisire il lock

Un task a bassa priorità che detiene un lock viene preemptato da un task a priorità media, e il task ad alta priorità resta bloccato indefinitamente. La mitigazione effettivamente applicata sul Mars Pathfinder è stata l’abilitazione del priority inheritance di VxWorks; Ada, per la stessa classe di problemi, offre come funzionalità del linguaggio un meccanismo diverso, Ceiling_Locking.

3. Basi delle priorità dei task ── FIFO_Within_Priorities

FIFO_Within_Priorities è una politica di dispatching standard basata su priorità che Ada Annex D consente di richiedere esplicitamente. Se non si specifica una politica, il comportamento predefinito è implementation-defined; GNAT, su molti target, usa una politica di questa famiglia. All’interno dello stesso livello di priorità i task eseguono in FIFO (first-in, first-out), e un task a priorità più alta preempta (interrompe) un task a priorità più bassa.

-- 01_task_priority.ada
-- Forma di base delle priorità dei task e di FIFO_Within_Priorities
-- I pragma di configurazione vanno prima delle context clause

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;

Punti chiave:

  • Con pragma Priority si assegna a ciascun task una priorità statica. Priority'Last è la più alta, Priority'First la più bassa.
  • Il corpo dei task in questa demo non fa calcoli pesanti: attende con delay until fino all’istante indicato. Quello che vogliamo osservare è che, quando entrambi diventano eseguibili nello stesso momento, il task ad alta priorità ottiene per primo l’opportunità di eseguire.
  • Questa figura mostra, di FIFO_Within_Priorities, il lato della preemption tra priorità diverse. Per verificare l’ordine FIFO all’interno della stessa priorità serve un altro esempio, con più task alla stessa priorità.
  • Nei sistemi reali è comune progettare le priorità in modo relativo, prendendo come riferimento System.Default_Priority.
Dispatching FIFO_Within_Priorities tra priorità diverseQuando entrambi i task sono eseguibili, lo scheduler sceglie prima il task a priorità più alta e lo fa eseguire fino al delay until.Task a bassa priorità(Priority=First)Task ad alta priorità(Priority=Last)Task mainSchedulerTask a bassa priorità(Priority=First)Task ad alta priorità(Priority=Last)Task mainSchedulerT=0ms: entrambi i task sono runnableStampa il log di avvioStampa il log di avvioT=100ms: HP si risvegliaStampa il log di completamentoT=500ms: LP si risvegliaStampa il log di completamento(T=800ms) Il main terminaCreazione del taskCreazione del taskSeleziona HP, la priorità più altaSi blocca con delay until T+100msEsegue poi LPSi blocca con delay until T+500msEsegue HPEsegue LP

Esempio di esecuzione (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

Notate che i log di avvio dei due task escono prima della riga di intestazione del main. I task dichiarati nella parte dichiarativa vengono attivati prima di entrare nel corpo del sottoprogramma che li racchiude, e può succedere un ordinamento di questo tipo. Inoltre, l’ordine di queste due righe di log di avvio può scambiarsi da un’esecuzione all’altra. Come spiegato nel capitolo 12, il modo in cui pragma Priority si riflette sullo scheduling effettivo dipende dal sistema operativo e dal runtime GNAT, e su un Linux generico l’ordine di avvio secondo la priorità non è garantito. Quello che in questo esempio si osserva in modo stabile è piuttosto l’ordine di completamento a 0,1 s / 0,5 s specificato con delay until.

Intervallo di priorità di Ada (default di GNAT):
  Priority'First  = 0   (minima)
  Priority'Last   = 30  (massima, dipende però dal sistema operativo)

4. Ceiling_Locking ── Il linguaggio previene l’inversione di priorità

Uno dei problemi più ostici nei sistemi real-time è l’inversione di priorità (priority inversion). Un task ad alta priorità attende un lock detenuto da un task a bassa priorità, e quel task a bassa priorità viene preemptato da un task a priorità media: il task ad alta priorità resta così bloccato indefinitamente.

Ada affronta il problema incorporando il protocollo Ceiling_Locking direttamente negli oggetti protetti.

-- 02_ceiling_locking.ada
-- Prevenzione dell'inversione di priorità con il protocollo Ceiling_Locking
-- I pragma di configurazione vanno prima delle context clause

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;

Come funziona Ceiling_Locking:

  1. Sull’oggetto protetto si imposta la priorità di ceiling con pragma Priority (Ceiling).
  2. Qualunque task entri nell’oggetto protetto viene automaticamente elevato alla priorità di ceiling all’ingresso.
  3. Così un task a priorità media non può preemptare il task che sta usando l’oggetto protetto.
  4. All’uscita dall’oggetto protetto la priorità torna a quella originale.

La figura sotto non è una traccia temporale stretta del codice di esempio appena visto: è uno schema concettuale di come Ceiling_Locking impedisce il pattern di inversione di priorità della figura 2.

Come Ceiling_Locking previene l'inversione di prioritàUn task che entra in un oggetto protetto viene elevato alla priorità di ceiling, così un task a priorità media non può preemptarlo.Oggetto protetto(priorità di ceiling=30)Task ad alta priorità(priorità=30)Task a priorità media(priorità=20)Task a bassa priorità(priorità=10)SchedulerOggetto protetto(priorità di ceiling=30)Task ad alta priorità(priorità=30)Task a priorità media(priorità=20)Task a bassa priorità(priorità=10)SchedulerUn chiamante con priorità attiva > priorità di ceiling solleva Program_ErrorH(30) nel diagramma è uguale al ceiling (30), quindi può entrareLa priorità di esecuzione sale a 30M si risvegliaL sta eseguendo con priorità di ceiling 30M(20) non può preemptarloH si risvegliaH(30) supera il controllo del ceilingma attende perché L sta usando il POLa priorità torna a 10Dopo il rilascio del PO esegue HH(30) = ceiling (30), quindi può entrare una volta risolta la contesaEntra nell'operazione protettaEsegue l'operazioneEsce dall'operazione protettaEntra nell'operazione protettaEsce dall'operazione protetta

Linea guida di progetto: la priorità di ceiling di un oggetto protetto va impostata almeno alla priorità più alta di tutti i task che usano quell’oggetto. Se si viola la regola e un task con priorità attiva superiore al ceiling invoca un’operazione protetta, Ada può rilevare l’errore di progetto sollevando Program_Error.

Per ottenere lo stesso effetto con i mutex pthread del C bisogna impostare esplicitamente l’attributo PTHREAD_PRIO_PROTECT; in Ada è una funzionalità standard del linguaggio.

5. delay until ── Eseguire i task periodici senza drift

Il pattern di base dei sistemi real-time è il task periodico. In un task che si ripete a intervallo fisso è essenziale impedire l’errore di temporizzazione cumulativo (drift).

Il delay until di Ada risolve il problema in modo elegante.

-- 03_periodic_task.ada
-- Task periodico con delay until ── previene il drift cumulativo

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;

Perché delay until:

Metodo Problema
delay Period; Il tempo di elaborazione di ogni iterazione si somma e il periodo deriva gradualmente (drift cumulativo)
delay until Next_Release; Next_Release := Next_Release + Period; Essendo basato su istanti assoluti, il prossimo istante di rilascio resta corretto anche se un’elaborazione ritarda

delay until, però, non garantisce da solo che il tempo di elaborazione stia dentro il periodo. Se l’elaborazione supera il prossimo istante di risveglio, quel delay until ritorna quasi subito e il sistema si trova in uno stato che andrebbe trattato come deadline miss.

Con delay:
  T=0ms → elaborazione (15ms) → delay 100ms → T=115ms → elaborazione (10ms) → ...
  Intervallo effettivo: 115ms, 110ms, ... (il tempo di elaborazione si accumula)

Con delay until:
  Next_Release: 100ms, 200ms, 300ms, ... (istanti assoluti)
  T=0ms → elaborazione (15ms) → delay until 100ms → T=100ms → elaborazione (10ms) → delay until 200ms
  Intervallo effettivo: 100ms, 100ms, ... (non dipende dal tempo di elaborazione)

Esempio di esecuzione (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

Su ciascun ciclo c’è un ritardo di risveglio dell’ordine di 0,2–0,3 millisecondi, ma si vede che quel ritardo non viene portato al ciclo successivo. Anche al quinto ciclo lo scostamento dall’istante di riferimento è inferiore a 1 millisecondo. Se si fosse scritto delay Period;, questo ritardo si sarebbe sommato a ogni ciclo e dopo cinque cicli la differenza sarebbe visibile. Questi numeri, peraltro, sono un esempio su un Linux generico, non valori garantiti in un ambiente hard real-time.

Useremo questo pattern con delay until in tutti i task periodici che seguono.

Confronto tra delay relativo e delay untildelay Period accumula drift; delay until mantiene gli istanti di rilascio assoluti; se il calcolo sfora il periodo, delay until ritorna subito e va trattato come deadline miss.Sforamento del periodo - deadline misscalcolo 130msProssimo = T+100msdelay until T+100ms ritorna subitoSi rileva il ritardo e si tratta come sovraccaricodelay until - basato su istanti assoluticalcolo 15msProssimo = T+100msdelay until T+100ms → risveglio a 100mscalcolo 10msProssimo = T+200ms → risveglio a 200msIntervallo effettivo: 100ms, 100ms...delay Period - drift cumulativodelay 100ms → risveglio a 115msT=0ms: calcolo 15mscalcolo 10ms → 125msdelay 100ms → risveglio a 225msIntervallo effettivo: 115ms, 110ms...L'errore si accumula nel tempoPreviene il drift cumulativo

6. Il profilo Ravenscar ── Un sottoinsieme real-time verificabile

Le funzionalità di tasking di Ada sono potenti, ma nei sistemi in cui la sicurezza è estremamente critica «troppo potenti» diventa un problema. La creazione dinamica di task, l’istruzione select, l’istruzione abort e simili rendono difficile l’analisi statica del tempo di esecuzione worst-case.

Il profilo Ravenscar è la risposta che Ada dà a questo problema. Restringe le funzionalità di tasking a un sottoinsieme staticamente analizzabile e deterministico.

-- 04_ravenscar_profile.ada
-- Forma di base del profilo Ravenscar
-- In compilazione si specifica pragma Profile (Ravenscar); in gnat.adc
-- Build: gnatchop -w 04_ravenscar_profile.ada .
--         → viene suddiviso in ravenscar_state.ads / ravenscar_state.adb / ravenscar_demo.adb
--         preparare gnat.adc e poi 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;

Restrizioni del profilo Ravenscar:

Funzionalità vietata Motivo
Creazione dinamica di task (new o access type) L’allocazione di memoria a runtime è non deterministica
Istruzione select Non solo le alternative multiple: l’istruzione select nel suo insieme rende più difficile l’analisi del flusso di controllo
Istruzione abort L’interruzione asincrona rende lo stato imprevedibile
Ada.Task_Attributes Comportamento dinamico a runtime
Cambio dinamico di priorità Le premesse dell’analisi di scheduling cambiano a runtime
Delay relativo (delay) Tende a produrre drift cumulativo, quindi si usa delay until a istante assoluto
Più entry per oggetto protetto Aumentano le condizioni di blocking e gli oggetti dell’analisi
Terminazione dei task In Ravenscar tutti i task si trattano come non terminanti
Istruzione requeue Il tracciamento del flusso di controllo si complica

Grazie a queste restrizioni, un programma conforme a Ravenscar assume una forma più adatta all’analisi statica dei tempi. È una proprietà richiesta da standard di sicurezza come DO-178C (software avionico) e ISO 26262 (sicurezza funzionale automotive). L’elenco sotto è un estratto delle restrizioni principali: il profilo effettivo include anche regole aggiuntive sul runtime e sull’analizzabilità, come No_Task_Hierarchy e Detect_Blocking.

Restrizioni e politiche obbligatorie del profilo RavenscarIl profilo Ravenscar limita il tasking Ada a un sottoinsieme deterministico e impone FIFO_Within_Priorities e Ceiling_Locking per facilitare l'analisi statica dei tempi.Tasking Ada completoProfilo RavenscarRestrizioniPolitiche obbligatorieDivieto di creazione dinamica di taskDivieto dell'istruzione selectDivieto dell'istruzione abortDivieto di Task_AttributesAl massimo 1 entry per oggetto protettoDivieto dell'istruzione requeueDivieto del delay relativosi usa delay untilDivieto del cambio dinamico di prioritàDivieto di terminazione dei tasktutti i task non terminanoFIFO_Within_PrioritiesCeiling_LockingCosa diventa più facile:analisi statica dei tempiDO-178Csoftware avionicoISO 26262sicurezza funzionale automotiveIEC 62304software per dispositivi medicali

Per abilitare il profilo Ravenscar si scrive quanto segue nel file gnat.adc:

pragma Profile (Ravenscar);

7. Timing event ── Risveglio a un istante senza polling

In molti sistemi real-time ricorre il requisito «quando scatta l’istante indicato, svegliare un task ad alta priorità». Un’implementazione ingenua farebbe polling su un timer; Ada offre un meccanismo più raffinato: i timing event.

-- 05_timing_events.ada
-- Timing event (Ada.Real_Time.Timing_Events)
-- Meccanismo per svegliare un task ad alta priorità senza polling
-- Build: gnatchop -w 05_timing_events.ada .
--         → viene suddiviso 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;

Come operano i timing event:

1. Set_Handler(Timer_1, T+100ms, S.Fire'Access)  ── registra l'handler a un istante assoluto
2. Trascorsi T+100ms ── il runtime invoca S.Fire **alla priorità di ceiling**
3. Fire imposta il flag Fired a True ── la barriera si apre
4. Il task Reactor si risveglia da Wait_For_Event

Il punto importante è che in questo esempio Ceiling_Locking è esplicito e l’handler Fire è una procedura di un oggetto protetto, quindi viene eseguito alla priorità di ceiling. La procedura protetta usata come handler di un timing event va messa in un oggetto protetto con priorità di ceiling a livello di interrupt, qui System.Interrupt_Priority'Last. Così durante il trattamento del timing event non si verifica inversione di priorità.

8. Code real-time con oggetti protetti

Un pattern ricorrente nei sistemi real-time è producer–consumer. Un sensore produce dati e un task di controllo li consuma: in quel momento servono mutua esclusione sul buffer e blocking efficienti.

Con gli oggetti protetti di Ada e le barriere delle entry si può implementare come sincronizzazione basata su barriera. Internamente il runtime gestisce la mutua esclusione, quindi nel codice applicativo non occorre scrivere direttamente mutex o variabili di condizione.

-- 06_protected_queue.ada
-- Condivisione di dati real-time tramite oggetti protetti
-- Pipeline: Producer -> Bounded_Buffer -> Consumer
-- In compilazione si specifica pragma Locking_Policy (Ceiling_Locking); in 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;

Punti di progetto:

  • entry Put when Count < Buffer_Size ── se il buffer è pieno, il Producer viene bloccato automaticamente.
  • entry Get when Count > 0 ── se il buffer è vuoto, il Consumer viene bloccato automaticamente.
  • pragma Priority (System.Any_Priority'Last) ── grazie al ceiling locking, non si verifica inversione di priorità tra Producer e Consumer.
  • La condizione di barriera è definita sullo stato interno dell’oggetto protetto (Count) e viene rivalutata automaticamente al rilascio del lock.

In questo codice non compaiono mutex, semafori o variabili di condizione lato applicazione. L’attesa necessaria si esprime con le barriere delle entry dell’oggetto protetto.

Stati del buffer limitato e barriere delle entryPut e Get transiscono tra vuoto, parziale e pieno; Get si blocca se Count è 0 e Put si blocca se Count è Buffer_Size.Stato inizialePut (aggiunge 1 elemento)Put / GetGet (estrae l'ultimo elemento)Put (riempie l'ultimo posto libero)Get (si libera un posto)Get si blocca (barriera Count=0)Put si blocca (barriera Count=Buffer_Size)Vuoto / Count=0Parzialmente pieno / Count=1..Buffer_Size-1Pieno / Count=Buffer_Size

Quando Put ha successo si rivaluta la barriera di chi attende Get; quando Get ha successo si rivaluta quella di chi attende Put. Questo avviene al completamento dell’operazione protetta, indipendentemente dallo stato in cui ci si trova nel diagramma.

9. Misurare il tempo di esecuzione ── Il primo passo del monitoraggio

Per valutare la schedulabilità di un sistema real-time occorre conoscere con precisione il tempo di esecuzione (tempo CPU) di ciascun task. Il package Ada.Execution_Time di Ada fornisce il tempo CPU consumato per task.

-- 07_execution_time.ada
-- Controllo del tempo di esecuzione (Execution_Time)
-- Misura il tempo CPU consumato da ciascun 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;

Tempo di wall clock vs tempo CPU:

Tempo di wall clock (Wall Clock): Ada.Real_Time.Clock
  → Tempo effettivamente trascorso. Include anche i periodi in cui si è bloccati o preemptati.

Tempo CPU (Execution Time): Ada.Execution_Time.Clock
  → Solo il tempo in cui quel task ha effettivamente eseguito sulla CPU.
  → I periodi di blocco e di preemption non vengono contati.

Questa distinzione è il punto di partenza del monitoraggio del tempo di esecuzione e della verifica del WCET. Mentre Busy_Worker attende con delay until il tempo CPU non cresce: aumenta solo durante il calcolo vero. Anche durante il delay until Clock + Milliseconds(500) del task main, il tempo CPU dovrebbe essere quasi zero. La misura del tempo CPU, però, non garantisce il WCET vero. Per un WCET che includa cache, pipeline e contesa di memoria servono a parte un’analisi statica e una verifica sull’ambiente target.

Differenza tra tempo di wall clock e tempo CPUIl tempo di wall clock include attesa, blocco e preemption; il tempo CPU conta solo il calcolo effettivo e da solo non garantisce il WCET vero.Tempo CPUDettaglio: solo il calcolo effettivoTempo CPU totale: 120msTempo di wall clockDettaglio: calcolo + attesa + blocco + preemptionTempo totale trascorso: 500msDifferenza = tempo di attesa, blocco e preemptionIl tempo CPU osserva il costo di calcolo realeSupporta verifica e monitoraggio del WCETEsclude attesa, blocco e preemptionAttenzioneLa misura non garantisce il WCET veroServono analisi statica e verifica sul target

10. Demo integrata ── Un sistema real-time multi-periodico

Integriamo tutti gli elementi visti finora — priorità, Ceiling_Locking, delay until, oggetti protetti — e costruiamo un tipico sistema real-time multi-periodico.

-- 08_multiperiodic.ada
-- Demo integrata di un sistema real-time multi-periodico
-- Task di lettura sensore a periodo veloce (100ms)
-- Task di controllo a periodo lento (400ms)
-- Condivisione dei dati con 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;

Architettura del sistema:

La figura sotto è un esempio di schedule che, sugli istanti di release del codice di esempio (sensore veloce a periodo 100 ms, controllo lento a periodo 400 ms con offset di 150 ms), assume tempi di esecuzione a scopo illustrativo. Nel codice non c’è un calcolo di controllo da 80 ms, quindi non è un diagramma di misura. Se la priorità del sensore veloce è più alta, quando arriva un release del sensore veloce mentre il controllo lento sta eseguendo, il controllo lento viene sospeso temporaneamente. Per chiarezza il diagramma disegna una sola interruzione rappresentativa per ciascun periodo del controllo lento, ma in realtà il sensore veloce va in release a ogni confine di 100 ms.

Esempio di schedule multi-periodicoUn sensore veloce a 100 ms preempta il controllore lento a 400 ms quando il rilascio del sensore cade durante l'esecuzione del controllore.Controllo lento, periodo 3 (release=950ms)Controllo lento, periodo 2 (release=550ms)Controllo lento, periodo 1 (release=150ms)1000-1010msSensore veloce #10interruzione perché P+3950-1000msControllo lento #3, prima metà1010-1040msControllo lento #3, seconda metà600-610msSensore veloce #6interruzione perché P+3550-600msControllo lento #2, prima metà610-640msControllo lento #2, seconda metà150-200msControllo lento #1, prima metà100-110msSensore veloce #1200-210msSensore veloce #2interruzione perché P+3210-240msControllo lento #1, seconda metàIpotesi a scopo illustrativoSensore veloce: elaborazione 10msControllo lento: elaborazione 80ms

Questo pattern è la struttura tipica «acquisizione sensore veloce + loop di controllo lento» che si vede spesso nei sistemi di controllo industriale e nel controllo dei robot.

11. Dove le funzionalità real-time di Ada danno il meglio

Le funzionalità real-time di Ada danno il massimo del loro valore soprattutto in campi come i seguenti.

Campi in cui brillano le funzionalità real-time di AdaAda Annex D trova applicazione in aerospazio, ferrovie, automotive, dispositivi medicali, controllo industriale e sistemi ad alta affidabilità per la difesa.Ada Annex DFunzionalità real-timeAerospazioDO-178CFerrovieFamiglia EN 50128AutomotiveISO 26262Dispositivi medicaliIEC 62304Controllo industrialeFamiglia IEC 61508Difesa e sistemi ad alta affidabilitàControllo di voloAmbito di applicazione consolidatoControllo di satelliti e veicoli spazialiSistemi di segnalamentoControllo automatico dei treniCandidato per ECU safety-relatedApplicazione limitata e selettivain un dominio dominato da C / MISRA-CPacemakerPompe per infusioneControllo robotMacchine utensili NCComputer di missioneSistemi a lungo ciclo di vita

Nel diagramma solo l’automotive reca la dicitura «applicazione limitata e selettiva»: il motivo è più la dimensione dell’ecosistema esistente che l’adeguatezza o meno del linguaggio. Il software di bordo si è accumulato intorno a C e MISRA-C: specifiche API di settore come AUTOSAR, codice fornito dai supplier, compilatori e strumenti di verifica già certificati, e la popolazione stessa degli ingegneri. Cambiare linguaggio, anche per un solo componente, significa rimettere in piedi insieme gli strumenti intorno e i processi di approvvigionamento e verifica. Per questo Ada tende a collocarsi non come standard dell’intero veicolo, ma come scelta selettiva per alcuni componenti che richiedono garanzie particolarmente alte, o in organizzazioni che hanno già asset e un’organizzazione di sviluppo in Ada. Al contrario, in campi già inclinati nel complesso verso l’alta affidabilità — aerospazio e ferrovie — questa barriera non esiste proprio.

12. Punti di attenzione e limiti

Le funzionalità real-time di Ada sono potenti, ma non sono una panacea.

1. Dipendenza dalla piattaforma:

  • Il mapping effettivo di pragma Priority dipende dall’ambiente di esecuzione (sistema operativo + runtime GNAT). Su Linux viene mappato su SCHED_FIFO, ma su Windows la preemption completa può non essere garantita.

2. Vincoli di Ravenscar:

  • La creazione dinamica di task è vietata, quindi all’avvio del sistema tutti i task vanno dichiarati in modo statico. Questo limita la libertà di progetto.

3. Limiti della misura del WCET:

  • Ada.Execution_Time è misura, non garanzia. Il WCET vero, che include cache miss e hazard di pipeline, va verificato a parte con strumenti di analisi statica.

4. Overhead:

  • La valutazione delle barriere di un oggetto protetto viene eseguita automaticamente al completamento o alla cancellazione di un’entry, e all’uscita dall’oggetto protetto. Su oggetti protetti invocati ad alta frequenza questo overhead va tenuto in conto.

5. La barriera della toolchain:

  • Per sfruttare appieno le funzionalità real-time di Ada servono un cross-compiler e un runtime adeguati. In particolare sui target embedded si finisce per dipendere dal runtime fornito dal vendor.

13. Conclusioni

In questo articolo abbiamo visto passo dopo passo, attraverso otto esempi di codice, le funzionalità real-time che Ada Annex D mette a disposizione.

Funzionalità Valore che offre
Priorità dei task Scheduling preemptive basato su priorità
Ceiling_Locking Prevenzione dell’inversione di priorità incorporata nel linguaggio
delay until Esecuzione periodica che previene il drift cumulativo
Profilo Ravenscar Sottoinsieme di tasking più adatto all’analisi statica
Timing event Risveglio guidato dal tempo, senza polling
Coda protetta Sincronizzazione basata sulle barriere degli oggetti protetti
Misura del tempo di esecuzione Monitoraggio del tempo CPU per task
Integrazione multi-periodica Un progetto in cui task a periodi diversi coesistono in modo sicuro

L’essenza delle funzionalità real-time di Ada è che non sono un’aggiunta a posteriori. Le regole di locking che contengono l’inversione di priorità, la specificazione degli istanti per l’esecuzione periodica, il monitoraggio del tempo di esecuzione e così via sono forniti come parte della specifica del linguaggio. Naturalmente il rispetto delle deadline si verifica con progetto e analisi; il punto di forza è che il runtime del linguaggio allestisce i presupposti per farlo.

Come passo successivo, per provare davvero lo sviluppo di sistemi real-time in Ada installate la toolchain GNAT con Alire e compilate i campioni di questo articolo con gnatchop + gnatmake.

Per le basi della concorrenza in Ada (task, rendezvous, oggetti protetti), si veda l’articolo precedente «Concorrenza sicura in Ada».

14. Riferimenti

Articoli recenti con gli stessi tag per approfondire argomenti vicini.

Queste pagine collocano l’argomento in un contesto più ampio di servizi e decisioni.

Domande frequenti

Domande che ricorrono nelle consulenze sull’argomento dell’articolo.

Che cos'è Annex D di Ada?
È l'insieme di funzionalità per i sistemi real-time standardizzato come parte della specifica del linguaggio Ada. Comprende lo scheduling preemptive basato su priorità con FIFO_Within_Priorities, il protocollo Ceiling_Locking che previene l'inversione di priorità, l'esecuzione periodica a istanti assoluti con delay until, il profilo Ravenscar, i timing event e la misura del tempo di esecuzione per task con Ada.Execution_Time. La caratteristica distintiva è che non è un'aggiunta a posteriori come libreria, ma è incorporato nel runtime del linguaggio stesso.
Che cos'è l'inversione di priorità? Come la previene Ada?
È il fenomeno in cui un task a bassa priorità detiene un lock e viene preemptato da un task a priorità media, mentre il task ad alta priorità che attende quel lock resta bloccato indefinitamente. Si è verificato realmente sul Mars Pathfinder nel 1997 e ha fatto sì che il lander si resetasse ripetutamente. In Ada il protocollo Ceiling_Locking è fornito come funzionalità del linguaggio: un task che entra in un oggetto protetto viene automaticamente elevato alla priorità di ceiling, quindi non può essere preemptato da un task a priorità media.
Che cos'è il profilo Ravenscar?
È un profilo che, per i sistemi in cui la sicurezza è estremamente critica, restringe le funzionalità di tasking di Ada a un sottoinsieme staticamente analizzabile e deterministico. Vieta la creazione dinamica di task, l'istruzione select, l'istruzione abort, il delay relativo, l'istruzione requeue e altro. Queste restrizioni rendono più facile l'analisi statica dei tempi e aiutano a soddisfare le proprietà richieste da standard di sicurezza come DO-178C (software avionico) e ISO 26262 (sicurezza funzionale automotive). In GNAT lo si abilita scrivendo pragma Profile (Ravenscar) nel file gnat.adc.
Perché nei task periodici si usa delay until e non delay?
Perché con un delay a tempo relativo il tempo di elaborazione di ogni iterazione si somma e il periodo deriva gradualmente (drift cumulativo). delay until determina il prossimo istante di rilascio rispetto a un orario assoluto, quindi anche se un'elaborazione ritarda, gli istanti di rilascio successivi restano corretti. Tuttavia, se l'elaborazione supera il prossimo istante di risveglio, delay until ritorna quasi subito, quindi serve un progetto separato che rilevi il fatto come deadline miss e lo tratti come sovraccarico.

Profilo dell’autore

Pagina di presentazione dell’autore dell’articolo.

Go Komura

Rappresentante di KomuraSoft LLC

Specializzato nello sviluppo di software Windows, nella consulenza tecnica e nell’analisi dei malfunzionamenti, soprattutto nei progetti con sistemi esistenti e guasti difficili da riprodurre.

Torna al blog