Programmazione di sistemi real-time in Ada ── Priorità, periodi e controllo del tempo di esecuzione in pratica
· Aggiornato il: · Go Komura · 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.
flowchart LR
accTitle: Mappa della conoscenza della programmazione di sistemi real-time in Ada
accDescr: Diagramma 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 Pathfinder
ada_annex_d["Annex D (standard Ada per i sistemi real-time)"]
ceiling_locking["Protocollo Ceiling_Locking"]
fifo_within_priorities["FIFO_Within_Priorities"]
ada_delay_until["delay until (ritardo a tempo assoluto)"]
ravenscar_profile["Profilo Ravenscar"]
ada_timing_events["Eventi di temporizzazione (Ada.Real_Time.Timing_Events)"]
ada_execution_time["Ada.Execution_Time (misura del tempo di esecuzione)"]
ada_task["Task Ada (concorrenza)"]
protected_object["Oggetto protetto (protected object)"]
gnat["GNAT"]
priority_inversion["Inversione di priorità (priority inversion)"]
mars_pathfinder_priority_inversion["Incidente di inversione di priorità del Mars Pathfinder"]
ada_relative_delay["delay relativo (istruzione delay)"]
cumulative_drift["Drift cumulativo dell'esecuzione periodica"]
ada_annex_d -->|"usa"| fifo_within_priorities
ada_annex_d -->|"usa"| ceiling_locking
ada_annex_d -->|"usa"| ada_delay_until
ada_annex_d -->|"usa"| ravenscar_profile
ada_annex_d -->|"usa"| ada_timing_events
ada_annex_d -->|"usa"| ada_execution_time
ada_annex_d -->|"richiede"| ada_task
ada_annex_d -.->|"richiede"| protected_object
ada_annex_d -.->|"richiede"| gnat
ceiling_locking -->|"previene"| priority_inversion
priority_inversion -->|"può causare"| mars_pathfinder_priority_inversion
ravenscar_profile -->|"richiede"| ada_task
ravenscar_profile -->|"richiede"| fifo_within_priorities
ravenscar_profile -->|"richiede"| ceiling_locking
ravenscar_profile -.->|"configurato da"| gnat
ada_relative_delay -->|"può causare"| cumulative_drift
ada_delay_until -->|"previene"| cumulative_drift
ada_relative_delay -->|"sconsigliato per"| ravenscar_profile
ada_timing_events -->|"usa"| ceiling_locking
ada_timing_events -->|"usa"| protected_object
ada_execution_time -->|"richiede"| ada_task
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.
flowchart LR
accTitle: Requisiti dei sistemi real-time e analisi di schedulabilità
accDescr: Hard e soft real-time, deadline, periodo, WCET e jitter confluiscono nell'analisi di schedulabilità, sostenuta dai meccanismi di Ada Annex D.
HRT[Hard real-time] -->|Mancato rispetto = fallimento fatale| Examples[Controllo di volo<br/>Airbag<br/>Pacemaker]
SRT[Soft real-time] -->|Superamenti rari sono tollerati| Examples2[Streaming video<br/>Giochi<br/>UI]
Ada[Ada Annex D<br/>Meccanismi per la prevedibilità] --> Mechanism[FIFO_Within_Priorities<br/>Ceiling_Locking<br/>delay until]
subgraph Requirements[Requisiti real-time]
D[Deadline<br/>Istante assoluto di completamento]
P[Periodo<br/>Intervallo di ripetizione]
W[WCET<br/>Tempo di esecuzione worst-case]
J[Jitter<br/>Variabilità del periodo]
end
D --> Analysis[Analisi di schedulabilità]
P --> Analysis
W --> Analysis
J --> Analysis
HRT --> Analysis
SRT --> Analysis
Mechanism --> Analysis
Analysis --> Constraint[Condizione necessaria: WCET <= deadline<br/>La 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.
sequenceDiagram
accTitle: Come si verifica l'inversione di priorità
accDescr: 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.
participant S as Scheduler
participant L as Task a bassa priorità
participant H as Task ad alta priorità
participant M as Task a priorità media
participant R as Risorsa condivisa
L->>R: Acquisisce il lock
activate L
Note over L: In esecuzione nella critical section
Note over S,L: H si risveglia e lo scheduler sospende L
deactivate L
activate H
H->>R: Tenta di acquisire il lock
Note over H: Bloccato in attesa del lock! (L lo detiene)
deactivate H
Note over S,L: H attende il lock, quindi L riprende
activate L
Note over L: Continua verso il rilascio del lock...
Note over S,L: M si risveglia e lo scheduler sospende L
deactivate L
activate M
Note over L: L non può rilasciare il lock
Note over M: M continua a eseguire (H e L sono fermi)
Note over H: [INVERSIONE DI PRIORITÀ] Alta priorità bloccata indefinitamente
deactivate M
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 Prioritysi assegna a ciascun task una priorità statica.Priority'Lastè la più alta,Priority'Firstla più bassa. - Il corpo dei task in questa demo non fa calcoli pesanti: attende con
delay untilfino 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.
sequenceDiagram
accTitle: Dispatching FIFO_Within_Priorities tra priorità diverse
accDescr: Quando entrambi i task sono eseguibili, lo scheduler sceglie prima il task a priorità più alta e lo fa eseguire fino al delay until.
participant S as Scheduler
participant Main as Task main
participant HP as Task ad alta priorità<br/>(Priority=Last)
participant LP as Task a bassa priorità<br/>(Priority=First)
Main->>HP: Creazione del task
Main->>LP: Creazione del task
Note over HP,LP: T=0ms: entrambi i task sono runnable
S->>HP: Seleziona HP, la priorità più alta
activate HP
Note over HP: Stampa il log di avvio
HP->>S: Si blocca con delay until T+100ms
deactivate HP
S->>LP: Esegue poi LP
activate LP
Note over LP: Stampa il log di avvio
LP->>S: Si blocca con delay until T+500ms
deactivate LP
Note over S: T=100ms: HP si risveglia
S->>HP: Esegue HP
activate HP
Note over HP: Stampa il log di completamento
deactivate HP
Note over S: T=500ms: LP si risveglia
S->>LP: Esegue LP
activate LP
Note over LP: Stampa il log di completamento
deactivate LP
Note over Main: (T=800ms) Il main termina
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:
- Sull’oggetto protetto si imposta la priorità di ceiling con
pragma Priority (Ceiling). - Qualunque task entri nell’oggetto protetto viene automaticamente elevato alla priorità di ceiling all’ingresso.
- Così un task a priorità media non può preemptare il task che sta usando l’oggetto protetto.
- 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.
sequenceDiagram
accTitle: Come Ceiling_Locking previene l'inversione di priorità
accDescr: Un task che entra in un oggetto protetto viene elevato alla priorità di ceiling, così un task a priorità media non può preemptarlo.
participant S as Scheduler
participant L as Task a bassa priorità<br/>(priorità=10)
participant M as Task a priorità media<br/>(priorità=20)
participant H as Task ad alta priorità<br/>(priorità=30)
participant PO as Oggetto protetto<br/>(priorità di ceiling=30)
Note over PO: Un chiamante con priorità attiva > priorità di ceiling solleva Program_Error<br/>H(30) nel diagramma è uguale al ceiling (30), quindi può entrare
L->>PO: Entra nell'operazione protetta
activate L
Note over L,PO: La priorità di esecuzione sale a 30
Note over S: M si risveglia
Note over S,L: L sta eseguendo con priorità di ceiling 30<br/>M(20) non può preemptarlo
Note over S: H si risveglia
Note over S,H: H(30) supera il controllo del ceiling<br/>ma attende perché L sta usando il PO
L->>PO: Esegue l'operazione
L->>PO: Esce dall'operazione protetta
deactivate L
Note over L: La priorità torna a 10
Note over S,H: Dopo il rilascio del PO esegue H
activate H
H->>PO: Entra nell'operazione protetta
Note over H,PO: H(30) = ceiling (30), quindi può entrare una volta risolta la contesa
H->>PO: Esce dall'operazione protetta
deactivate H
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.
flowchart TB
accTitle: Confronto tra delay relativo e delay until
accDescr: delay 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.
subgraph Bad["delay Period - drift cumulativo"]
B1[T=0ms: calcolo 15ms] --> B2[delay 100ms → risveglio a 115ms]
B2 --> B3[calcolo 10ms → 125ms]
B3 --> B4[delay 100ms → risveglio a 225ms]
B4 --> B5[Intervallo effettivo: 115ms, 110ms...]
end
subgraph Good["delay until - basato su istanti assoluti"]
G1[Prossimo = T+100ms] --> G2[calcolo 15ms]
G2 --> G3[delay until T+100ms → risveglio a 100ms]
G3 --> G4[calcolo 10ms]
G4 --> G5[Prossimo = T+200ms → risveglio a 200ms]
G5 --> G6[Intervallo effettivo: 100ms, 100ms...]
end
subgraph Overrun["Sforamento del periodo - deadline miss"]
O1[Prossimo = T+100ms] --> O2[calcolo 130ms]
O2 --> O3[delay until T+100ms ritorna subito]
O3 --> O4[Si rileva il ritardo e si tratta come sovraccarico]
end
Bad --> Drift[L'errore si accumula nel tempo]
Good --> Stable[Previene il drift cumulativo]
Good --> Overrun
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.
flowchart TB
accTitle: Restrizioni e politiche obbligatorie del profilo Ravenscar
accDescr: Il 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.
Full[Tasking Ada completo] --> Profile[Profilo Ravenscar]
Profile --> Restrict[Restrizioni]
Profile --> Policy[Politiche obbligatorie]
Restrict --> R1[Divieto di creazione dinamica di task]
Restrict --> R2[Divieto dell'istruzione select]
Restrict --> R3[Divieto dell'istruzione abort]
Restrict --> R4[Divieto di Task_Attributes]
Restrict --> R5[Al massimo 1 entry per oggetto protetto]
Restrict --> R6[Divieto dell'istruzione requeue]
Restrict --> R7[Divieto del delay relativo<br/>si usa delay until]
Restrict --> R8[Divieto del cambio dinamico di priorità]
Restrict --> R9[Divieto di terminazione dei task<br/>tutti i task non terminano]
Policy --> P1[FIFO_Within_Priorities]
Policy --> P2[Ceiling_Locking]
Restrict --> Benefit[Cosa diventa più facile:<br/>analisi statica dei tempi]
Policy --> Benefit
Benefit --> DO178[DO-178C<br/>software avionico]
Benefit --> ISO26262[ISO 26262<br/>sicurezza funzionale automotive]
Benefit --> IEC62304[IEC 62304<br/>software 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.
stateDiagram-v2
accTitle: Stati del buffer limitato e barriere delle entry
accDescr: Put e Get transiscono tra vuoto, parziale e pieno; Get si blocca se Count è 0 e Put si blocca se Count è Buffer_Size.
Empty: Vuoto / Count=0
Partial: Parzialmente pieno / Count=1..Buffer_Size-1
Full: Pieno / Count=Buffer_Size
[*] --> Empty: Stato iniziale
Empty --> Partial: Put (aggiunge 1 elemento)
Partial --> Partial: Put / Get
Partial --> Empty: Get (estrae l'ultimo elemento)
Partial --> Full: Put (riempie l'ultimo posto libero)
Full --> Partial: Get (si libera un posto)
Empty --> Empty: Get si blocca (barriera Count=0)
Full --> Full: Put si blocca (barriera 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.
flowchart LR
accTitle: Differenza tra tempo di wall clock e tempo CPU
accDescr: Il 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.
subgraph Wall[Tempo di wall clock]
W1[Tempo totale trascorso: 500ms] --> W2[Dettaglio: calcolo + attesa + blocco + preemption]
end
subgraph CPU[Tempo CPU]
C1[Tempo CPU totale: 120ms] --> C2[Dettaglio: solo il calcolo effettivo]
end
Wall --> Diff[Differenza = tempo di attesa, blocco e preemption]
CPU --> Diff
Diff --> Insight[Il tempo CPU osserva il costo di calcolo reale<br/>Supporta verifica e monitoraggio del WCET<br/>Esclude attesa, blocco e preemption]
Insight --> Caveat[Attenzione<br/>La misura non garantisce il WCET vero<br/>Servono 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.
flowchart TB
accTitle: Esempio di schedule multi-periodico
accDescr: Un sensore veloce a 100 ms preempta il controllore lento a 400 ms quando il rilascio del sensore cade durante l'esecuzione del controllore.
Assumption["Ipotesi a scopo illustrativo<br/>Sensore veloce: elaborazione 10ms<br/>Controllo lento: elaborazione 80ms"]
subgraph Cycle1["Controllo lento, periodo 1 (release=150ms)"]
direction LR
C1F1["100-110ms<br/>Sensore veloce #1"] --> C1S1["150-200ms<br/>Controllo lento #1, prima metà"]
C1S1 --> C1F2["200-210ms<br/>Sensore veloce #2<br/>interruzione perché P+3"]
C1F2 --> C1S2["210-240ms<br/>Controllo lento #1, seconda metà"]
end
subgraph Cycle2["Controllo lento, periodo 2 (release=550ms)"]
direction LR
C2S1["550-600ms<br/>Controllo lento #2, prima metà"] --> C2F6["600-610ms<br/>Sensore veloce #6<br/>interruzione perché P+3"]
C2F6 --> C2S2["610-640ms<br/>Controllo lento #2, seconda metà"]
end
subgraph Cycle3["Controllo lento, periodo 3 (release=950ms)"]
direction LR
C3S1["950-1000ms<br/>Controllo lento #3, prima metà"] --> C3F10["1000-1010ms<br/>Sensore veloce #10<br/>interruzione perché P+3"]
C3F10 --> C3S2["1010-1040ms<br/>Controllo lento #3, seconda metà"]
end
Assumption --> C1F1
C1S2 --> C2S1
C2S2 --> C3S1
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.
flowchart TB
accTitle: Campi in cui brillano le funzionalità real-time di Ada
accDescr: Ada Annex D trova applicazione in aerospazio, ferrovie, automotive, dispositivi medicali, controllo industriale e sistemi ad alta affidabilità per la difesa.
Ada[Ada Annex D<br/>Funzionalità real-time] --> Aero[Aerospazio<br/>DO-178C]
Ada --> Rail[Ferrovie<br/>Famiglia EN 50128]
Ada --> Auto[Automotive<br/>ISO 26262]
Ada --> Medical[Dispositivi medicali<br/>IEC 62304]
Ada --> Industrial[Controllo industriale<br/>Famiglia IEC 61508]
Ada --> Defense[Difesa e sistemi ad alta affidabilità]
Aero --> A1[Controllo di volo<br/>Ambito di applicazione consolidato]
Aero --> A2[Controllo di satelliti e veicoli spaziali]
Rail --> R1[Sistemi di segnalamento]
Rail --> R2[Controllo automatico dei treni]
Auto --> Au1[Candidato per ECU safety-related]
Auto --> Au2[Applicazione limitata e selettiva<br/>in un dominio dominato da C / MISRA-C]
Medical --> M1[Pacemaker]
Medical --> M2[Pompe per infusione]
Industrial --> I1[Controllo robot]
Industrial --> I2[Macchine utensili NC]
Defense --> D1[Computer di missione]
Defense --> D2[Sistemi 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 Prioritydipende dall’ambiente di esecuzione (sistema operativo + runtime GNAT). Su Linux viene mappato suSCHED_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 correlati
Articoli recenti con gli stessi tag per approfondire argomenti vicini.
Concorrenza sicura in Ada ── Guida pratica a task e oggetti protetti
Introduzione alla concorrenza integrata nel linguaggio Ada: task e oggetti protetti. L'articolo organizza rendezvous (entry/accept), acce...
Programmazione generic in Ada ── Contratti nei tipi e riuso a costo zero
Una guida sistematica alla programmazione generic in Ada: sottoprogrammi e package generici, parametri formali, categorie di tipo e crite...
Introduzione alla verifica formale con SPARK ── Dai contratti Ada alla dimostrazione matematica
Un'introduzione pratica alla verifica formale con SPARK, il sottoinsieme di Ada. L'articolo copre il passaggio dai contratti (Pre/Post) a...
Impostazioni di scheduling del processore Windows - Background services e P/E core
Cosa cambia effettivamente con l'impostazione Windows 'Background services', spiegato attraverso quantum time, favoritismo del primo pian...
Le profondità della virtualizzazione Windows (parte 3) — Macchine virtuali che si avviano in pochi secondi: perché WSL2, Windows Sandbox e i container sono così leggeri
Perché WSL2 e Windows Sandbox partono in pochi secondi e sembrano così leggeri? Questo articolo spiega i meccanismi, dalle immagini di ba...
Argomenti correlati
Queste pagine collocano l’argomento in un contesto più ampio di servizi e decisioni.
Argomenti tecnici Windows
Portale su sviluppo Windows, analisi dei problemi e valorizzazione delle risorse esistenti.
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.