Programación de sistemas de tiempo real con Ada — Control práctico de prioridad, periodo y tiempo de ejecución

· Actualizado el: · · Ada, RealTime, Ravenscar, CeilingLocking, Tasking, Scheduling, PriorityInversion, ProgrammingLanguage, Tiempo real, Alta fiabilidad

1. Introducción — la profunda relación entre Ada y el tiempo real

En el artículo anterior, «Concurrencia segura en Ada», explicamos los fundamentos de la concurrencia segura mediante tareas y objetos protegidos de Ada. En este artículo avanzamos hacia una extensión de ese tema, un ámbito con restricciones aún más estrictas: los sistemas de tiempo real.

En un sistema de tiempo real, la «corrección» no solo significa que el resultado del cálculo sea lógicamente correcto, sino también que ese resultado se obtenga dentro del plazo establecido. Una respuesta correcta que llega un milisegundo tarde es tan peligrosa como una respuesta incorrecta.

Ante este requisito, Ada ofrece un conjunto integral de funciones de tiempo real estandarizadas como el Annex D (Real-Time Systems) de la especificación del lenguaje. No se trata de «una biblioteca añadida a posteriori», sino de garantías de tiempo real integradas en el propio runtime del lenguaje.

Funciones de tiempo real de Ada (Annex D):
- Prioridad de tareas y apropiación (FIFO_Within_Priorities)
- Protocolo Ceiling_Locking (prevención de la inversión de prioridad)
- Ejecución periódica en tiempo absoluto con delay until
- Perfil Ravenscar (subconjunto para seguridad crítica)
- Eventos de temporización (activación por tiempo sin sondeo)
- Monitorización del tiempo de ejecución (Ada.Execution_Time)
- Planificación multiperiódica

En este artículo explicamos estas funciones paso a paso mediante 8 ejemplos prácticos de código. Cada fragmento puede tratarse como un ejemplo independiente, pero los ejemplos 04/05, que contienen varias unidades de compilación, se dividen con gnatchop antes de compilarlos con gnatmake.

Público destinatario y conocimientos previos: se asume que el lector conoce los fundamentos de tareas, rendez-vous y objetos protegidos tratados en el artículo anterior. Está orientado a desarrolladores interesados en el software de control de dispositivos embebidos y sistemas de alta fiabilidad; no se trata de una introducción a la sintaxis de Ada en sí.

Entorno de verificación: se ha confirmado que los 8 ejemplos de este artículo compilan con GNAT 13.3.0 (Ubuntu 24.04, x86-64). Los resultados de ejecución mostrados en el texto (capítulos 3 y 5) se obtuvieron en el mismo entorno. Cómo se aplican realmente la prioridad y la apropiación depende del sistema operativo y del runtime de GNAT (capítulo 12), por lo que los detalles de la salida pueden variar según el entorno.

Los fragmentos de código que aparecen en este artículo están organizados por capítulos en un repositorio de referencia publicado en GitHub.

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

2. Qué es un sistema de tiempo real

Empecemos ordenando la terminología.

Concepto Descripción
Tiempo real estricto (hard real-time) Superar el plazo supone un fallo fatal del sistema (control de vuelo, airbags, marcapasos)
Tiempo real flexible (soft real-time) Superar el plazo no es deseable, pero se toleran excesos ocasionales (streaming de vídeo, videojuegos)
Deadline (plazo) Instante absoluto en el que una tarea debe haberse completado
Periodo (period) Intervalo de tiempo con el que se activa repetidamente una tarea
WCET (Worst-Case Execution Time) Tiempo de ejecución en el peor caso de una tarea
Jitter Variabilidad de la ejecución periódica
Planificabilidad (schedulability) Propiedad de que «ese conjunto de tareas pueda ejecutarse cumpliendo todos los plazos». Verificar esto sobre el papel es el análisis de planificabilidad (análisis de tiempo de respuesta, etc.)
Perfil Ravenscar Convención que restringe las funciones de tareas de Ada a un subconjunto fácil de analizar estáticamente. El nombre proviene de Ravenscar, el pueblo inglés donde se celebró la reunión en la que se definió (capítulo 6)

En el diseño de sistemas de tiempo real, es un requisito necesario importante que se cumpla, para cada tarea, «WCET <= deadline». Sin embargo, eso por sí solo no garantiza que el sistema completo cumpla los plazos en su conjunto. También hace falta un análisis de tiempo de respuesta que tenga en cuenta el tiempo de bloqueo, la asignación de prioridades, el jitter, las interrupciones y el comportamiento del runtime y del sistema operativo. En la práctica, se busca WCET < deadline para dejar margen. Las funciones de tiempo real de Ada proporcionan, a nivel de lenguaje, un modelo de ejecución predecible que facilita ese análisis.

Requisitos de tiempo realincumplir el plazo = fallo fatalse toleran excesos ocasionalesDeadlineinstante absoluto de finalizaciónPeriodointervalo de repeticiónWCETtiempo de ejecución en el peor casoJittervariabilidad del periodoTiempo real estrictoControl de vueloAirbagsMarcapasosTiempo real flexibleStreaming de vídeoVideojuegosUIAda Annex Dmecanismos que sostienen la predictibilidadFIFO_Within_PrioritiesCeiling_Lockingdelay untilAnálisis de planificabilidadRequisito necesario: WCET &lt;= deadlinela suficiencia se confirma con análisis de tiempo de respuesta

Figura 1: Requisitos de tiempo real y el papel de los mecanismos de Ada Annex D en el análisis de planificabilidad.

Uno de los fenómenos más peligrosos en los sistemas de tiempo real es la inversión de prioridad. Este problema ocurrió realmente en 1997 en la sonda Mars Pathfinder y provocó reinicios repetidos de la nave.

Recurso compartidoTarea de prioridad mediaTarea de alta prioridadTarea de baja prioridadPlanificadorRecurso compartidoTarea de prioridad mediaTarea de alta prioridadTarea de baja prioridadPlanificadorEjecutando la sección críticaEl despertar de H hace que el planificador suspenda a LBloqueada esperando el bloqueo (L lo mantiene)Como H espera el bloqueo, se reanuda LContinúa hacia la liberación del bloqueo...El despertar de M hace que el planificador suspenda a LL no puede liberar el bloqueoM sigue ejecutándose (ni H ni L pueden avanzar)[Inversión de prioridad] la de alta prioridad queda bloqueada indefinidamenteAdquiere el bloqueoIntenta adquirir el bloqueo

Figura 2: Secuencia de una inversión de prioridad clásica: la tarea de baja prioridad es apropiada por una de prioridad media mientras mantiene el bloqueo que necesita la de alta prioridad.

Una tarea de baja prioridad, mientras mantiene un bloqueo, es apropiada por una tarea de prioridad media, y la tarea de alta prioridad queda bloqueada indefinidamente. La medida realmente aplicada en Mars Pathfinder fue activar la herencia de prioridad (priority inheritance) de VxWorks, pero Ada ofrece, para el mismo tipo de problema, un enfoque distinto: Ceiling_Locking, integrado como característica del lenguaje.

3. Fundamentos de la prioridad de tareas — FIFO_Within_Priorities

FIFO_Within_Priorities es la política estándar de despacho basada en prioridad que puede especificarse en el Annex D de Ada. Cuando no se indica ninguna política de forma explícita, el comportamiento por defecto queda definido por la implementación, pero en GNAT muchas plataformas usan una política de este tipo. Dentro de la misma prioridad se ejecuta en orden FIFO (primero en entrar, primero en salir), y una tarea de mayor prioridad apropia (interrumpe) a las tareas de menor prioridad.

-- 01_task_priority.ada
-- Prioridad de tareas y forma básica de FIFO_Within_Priorities
-- Los pragmas de configuración se colocan antes de las cláusulas de contexto

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;

Puntos clave:

  • pragma Priority asigna una prioridad estática a cada tarea. Priority'Last es la más alta y Priority'First la más baja.
  • El cuerpo de las tareas de esta demo no realiza cálculos pesados; simplemente espera hasta un instante indicado con delay until. Lo que se quiere confirmar aquí es que, cuando ambas quedan listas al mismo tiempo, la tarea de mayor prioridad obtiene primero la oportunidad de ejecutarse.
  • Este diagrama muestra el aspecto de la apropiación entre distintas prioridades dentro de FIFO_Within_Priorities. Para confirmar el orden FIFO dentro de una misma prioridad haría falta otro ejemplo con varias tareas de igual prioridad.
  • En sistemas reales es habitual diseñar las prioridades relativas tomando System.Default_Priority como referencia.
Tarea baja prioridad(Priority=First)Tarea alta prioridad(Priority=Last)Tarea principalPlanificadorTarea baja prioridad(Priority=First)Tarea alta prioridad(Priority=Last)Tarea principalPlanificadorT=0ms: ambas tareas están runnableEscribe el registro de inicioEscribe el registro de inicioT=100ms: despierta HPEscribe el registro de finalizaciónT=500ms: despierta LPEscribe el registro de finalización(T=800ms) finaliza la tarea principalCrea la tareaCrea la tareaSelecciona HP, de mayor prioridadSe bloquea con delay until T+100msA continuación ejecuta LPSe bloquea con delay until T+500msEjecuta HPEjecuta LP

Figura 3: Traza de planificación de la demo de prioridad de tareas: la tarea de alta prioridad se ejecuta primero en cada instante de despertar compartido.

Ejemplo de ejecución (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

Observe que los dos registros de inicio de las tareas aparecen antes que la línea de cabecera de la tarea principal. Las tareas declaradas se activan antes de entrar en el cuerpo del subprograma que las contiene, por lo que este orden puede darse. Además, el orden de estas dos líneas de inicio puede intercambiarse en cada ejecución. Como se indica en el capítulo 12, la forma en que pragma Priority se traduce realmente en la planificación depende del sistema operativo y del runtime de GNAT, y en un Linux de propósito general no está garantizado el orden de arranque según la prioridad. Lo que sí se observa de forma estable en este ejemplo es el orden de finalización a los 0,1 y 0,5 segundos indicados con delay until.

Rango de prioridades de Ada (valores por defecto de GNAT):
  Priority'First  = 0   (la más baja)
  Priority'Last   = 30  (la más alta, aunque depende del sistema operativo)

4. Ceiling_Locking — el lenguaje evita la inversión de prioridad

Uno de los problemas más complicados en los sistemas de tiempo real es la inversión de prioridad (priority inversion). Es el fenómeno en el que una tarea de alta prioridad espera un bloqueo que mantiene una tarea de baja prioridad, y esta última, al ser apropiada por una tarea de prioridad media, provoca que la tarea de alta prioridad quede bloqueada indefinidamente.

Ante este problema, Ada integra directamente en los objetos protegidos el protocolo Ceiling_Locking.

-- 02_ceiling_locking.ada
-- Prevención de la inversión de prioridad mediante el protocolo Ceiling_Locking
-- Los pragmas de configuración se colocan antes de las cláusulas de contexto

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;

Cómo funciona Ceiling_Locking:

  1. Al objeto protegido se le asigna una prioridad techo con pragma Priority (Ceiling).
  2. Cualquier tarea que entre en el objeto protegido asciende automáticamente a la prioridad techo en el momento de entrar.
  3. Gracias a esto, mientras una tarea usa el objeto protegido, ninguna tarea de prioridad media puede apropiarla.
  4. Al salir del objeto protegido, la tarea vuelve a su prioridad original.

El siguiente diagrama no es una traza temporal exacta del código de ejemplo anterior, sino un esquema conceptual que muestra cómo Ceiling_Locking contiene el patrón de inversión de prioridad de la figura 2.

Objeto protegido(prioridad techo=30)Tarea alta prioridad(prioridad=30)Tarea prioridad media(prioridad=20)Tarea baja prioridad(prioridad=10)PlanificadorObjeto protegido(prioridad techo=30)Tarea alta prioridad(prioridad=30)Tarea prioridad media(prioridad=20)Tarea baja prioridad(prioridad=10)PlanificadorSi la prioridad activa del llamante > prioridad techo, se produce Program_Erroren el diagrama, H(30) puede entrar porque iguala el techo (30)La prioridad de ejecución asciende a 30Despierta ML se ejecuta con la prioridad techo 30M(20) no puede apropiarlaDespierta HH(30) supera la comprobación del techopero espera porque L usa el POLa prioridad vuelve a 10Tras liberarse el PO, se ejecuta HH(30) = techo(30), por lo que puede entrar tras resolverse la contenciónEntra en la operación protegidaEjecuta la operaciónSale de la operación protegidaEntra en la operación protegidaSale de la operación protegida

Figura 4: Cómo Ceiling_Locking evita la inversión de prioridad ascendiendo automáticamente a la prioridad techo a la tarea que entra en el objeto protegido.

Pauta de diseño: la prioridad techo del objeto protegido debe fijarse en un valor igual o superior a la mayor prioridad de todas las tareas que usan ese objeto protegido. Si se incumple esto y una tarea con una prioridad activa superior a la prioridad techo invoca una operación protegida, Ada permite detectar el error de diseño mediante Program_Error.

Para lograr algo equivalente con mutexes de pthread en C hace falta configurar explícitamente el atributo PTHREAD_PRIO_PROTECT, mientras que en Ada esto es una característica estándar del lenguaje.

5. delay until — ejecución periódica de tareas sin deriva

El patrón básico de un sistema de tiempo real es la tarea periódica. En una tarea que se ejecuta repetidamente a intervalos fijos, es fundamental evitar el error de temporización acumulado (deriva o drift).

El delay until de Ada resuelve este problema con elegancia.

-- 03_periodic_task.ada
-- Tarea periódica con delay until — evita la deriva acumulada

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;

Por qué usar delay until:

Método Problema
delay Period; El tiempo de procesamiento de cada iteración se acumula y el periodo se desplaza progresivamente (deriva acumulada)
delay until Next_Release; Next_Release := Next_Release + Period; Al tomar como referencia un tiempo absoluto, el siguiente instante de activación es correcto aunque un procesamiento se retrase

Sin embargo, delay until no garantiza automáticamente que el tiempo de procesamiento quepa dentro del periodo. Si un procesamiento supera el siguiente instante de despertar, ese delay until retorna casi de inmediato, y el sistema entra en un estado que debería tratarse como deadline miss.

Con delay:
  T=0ms → proceso(15ms) → delay 100ms → T=115ms → proceso(10ms) → ...
  Intervalo real: 115ms, 110ms, ... (el tiempo de procesamiento se acumula)

Con delay until:
  Next_Release: 100ms, 200ms, 300ms, ... (tiempo absoluto)
  T=0ms → proceso(15ms) → delay until 100ms → T=100ms → proceso(10ms) → delay until 200ms
  Intervalo real: 100ms, 100ms, ... (no depende del tiempo de procesamiento)

Ejemplo de ejecución (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

Cada ciclo presenta un retraso de despertar de unos 0,2 a 0,3 milisegundos, pero se aprecia que ese retraso no se traslada al periodo siguiente. Incluso en el quinto ciclo, la desviación respecto al instante de referencia es inferior a un milisegundo. Si se hubiera escrito con delay Period;, ese retraso se habría ido acumulando en cada ciclo y tras 5 ciclos habría producido una diferencia visible. Cabe señalar que estas cifras corresponden a un ejemplo en un Linux de propósito general y no son valores garantizados en un entorno de tiempo real estricto.

Este patrón con delay until se usará en el resto de tareas periódicas de este artículo.

Exceso de periodo - deadline misscálculo 130mspróximo = T+100msdelay until T+100ms retorna casi de inmediatose detecta el retraso y se trata como sobrecargadelay until - basado en tiempo absolutocálculo 15mspróximo = T+100msdelay until T+100ms → despierta a 100mscálculo 10mspróximo = T+200ms → despierta a 200msIntervalo real: 100ms, 100ms...delay Period - deriva acumuladadelay 100ms → despierta a 115msT=0ms: cálculo 15mscálculo 10ms → 125msdelay 100ms → despierta a 225msIntervalo real: 115ms, 110ms...El error se acumula con el tiempoEvita la deriva acumulada

Figura 5: Comparación entre delay y delay until en la ejecución periódica, y el caso en que el procesamiento supera el periodo (deadline miss).

6. El perfil Ravenscar — un subconjunto de tiempo real verificable

Las funciones de tareas de Ada son potentes, pero en sistemas de seguridad extremadamente crítica esa potencia se convierte en un problema. La creación dinámica de tareas, la sentencia select, la sentencia abort y similares dificultan el análisis estático del tiempo de ejecución en el peor caso.

El perfil Ravenscar es la respuesta que Ada ofrece a este problema. Restringe las funciones de tareas a un subconjunto estáticamente analizable y determinista.

-- 04_ravenscar_profile.ada
-- Forma básica del perfil Ravenscar
-- En tiempo de compilación se especifica pragma Profile (Ravenscar); en gnat.adc
-- Compilación: gnatchop -w 04_ravenscar_profile.ada .
--         → se divide en ravenscar_state.ads / ravenscar_state.adb / ravenscar_demo.adb
--         preparar gnat.adc y luego 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;

Restricciones del perfil Ravenscar:

Función prohibida Motivo
Creación dinámica de tareas (new o tipos de acceso) La asignación de memoria en tiempo de ejecución no es determinista
Sentencia select No solo las alternativas múltiples, sino la sentencia select en su conjunto dificulta el análisis del flujo de control
Sentencia abort La interrupción asíncrona genera imprevisibilidad de estado
Ada.Task_Attributes Comportamiento dinámico en tiempo de ejecución
Cambio dinámico de prioridad Las premisas del análisis de planificación cambian en tiempo de ejecución
delay relativo Tiende a generar deriva acumulada; se usa el delay until de tiempo absoluto
Varias entradas por objeto protegido Aumentan las condiciones de bloqueo y los elementos a analizar
Terminación de tareas En Ravenscar todas las tareas se tratan como no terminables
Sentencia requeue Complica el seguimiento del flujo de control

Esta restricción hace que un programa conforme a Ravenscar adopte una forma fácil de someter a análisis estático de temporización. Es una propiedad exigida por normas de seguridad como DO-178C (software aeronáutico) o ISO 26262 (seguridad funcional automotriz). La lista anterior es un extracto de las principales restricciones; el perfil real incluye además reglas adicionales relacionadas con el runtime y la analizabilidad, como No_Task_Hierarchy o Detect_Blocking.

Funciones completas de tareas de AdaPerfil RavenscarRestriccionesPolíticas obligatoriasProhibición de creación dinámica de tareasProhibición de la sentencia selectProhibición de la sentencia abortProhibición de Task_AttributesLímite de 1 entrada por objeto protegidoProhibición de la sentencia requeueProhibición del delay relativousar delay untilProhibición del cambio dinámico de prioridadProhibición de la terminación de tareastodas las tareas no terminablesFIFO_Within_PrioritiesCeiling_LockingLo que facilita:análisis estático de temporizaciónDO-178Csoftware aeronáuticoISO 26262seguridad funcional automotrizIEC 62304software de dispositivos médicos

Figura 6: Restricciones y políticas obligatorias del perfil Ravenscar, y las normas de seguridad que su analizabilidad ayuda a cumplir.

Para activar el perfil Ravenscar, escriba lo siguiente en el archivo gnat.adc:

pragma Profile (Ravenscar);

7. Eventos de temporización — activación por tiempo sin sondeo

En muchos sistemas de tiempo real aparece con frecuencia el requisito de «despertar una tarea de alta prioridad cuando llegue un instante determinado». En una implementación ingenua esto se resolvería sondeando un temporizador, pero Ada ofrece un mecanismo más elegante: los eventos de temporización.

-- 05_timing_events.ada
-- Eventos de temporización (Ada.Real_Time.Timing_Events)
-- Mecanismo para despertar tareas de alta prioridad sin sondeo
-- Compilación: gnatchop -w 05_timing_events.ada .
--         → se divide en 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;

Funcionamiento de los eventos de temporización:

1. Set_Handler(Timer_1, T+100ms, S.Fire'Access)  ── registra un manejador para un tiempo absoluto
2. Transcurridos T+100ms ── el runtime invoca S.Fire **con la prioridad techo**
3. Fire pone la bandera Fired a True ── se abre la barrera
4. La tarea Reactor despierta desde Wait_For_Event

Lo importante es que, en este ejemplo, se especifica explícitamente Ceiling_Locking y, como el manejador Fire es un procedimiento de un objeto protegido, se ejecuta con la prioridad techo. El procedimiento protegido que se usa como manejador de un evento de temporización debe colocarse en un objeto protegido con una prioridad techo de nivel de interrupción, en este caso System.Interrupt_Priority'Last. Gracias a ello, no se produce inversión de prioridad durante el procesamiento del evento de temporización.

8. Colas de tiempo real mediante objetos protegidos

Un patrón habitual en los sistemas de tiempo real es el de productor-consumidor. Un sensor genera datos y una tarea de control los consume; en este caso es necesario gestionar de forma eficiente la exclusión mutua y el bloqueo del búfer.

Con los objetos protegidos y las barreras de entrada de Ada, esto puede implementarse como sincronización basada en barreras. Como el runtime gestiona internamente la exclusión mutua, no hace falta escribir directamente mutexes ni variables de condición en el código de la aplicación.

-- 06_protected_queue.ada
-- Compartición de datos de tiempo real mediante un objeto protegido
-- Pipeline: Producer -> Bounded_Buffer -> Consumer
-- En tiempo de compilación se especifica pragma Locking_Policy (Ceiling_Locking); en 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;

Puntos clave del diseño:

  • entry Put when Count < Buffer_Size — si el búfer está lleno, el Producer se bloquea automáticamente.
  • entry Get when Count > 0 — si el búfer está vacío, el Consumer se bloquea automáticamente.
  • pragma Priority (System.Any_Priority'Last) — gracias al bloqueo por techo, no se produce inversión de prioridad entre Producer y Consumer.
  • La condición de barrera se define a partir del estado interno del objeto protegido (Count) y se reevalúa automáticamente al liberarse el bloqueo.

En este código no aparecen mutexes, semáforos ni variables de condición del lado de la aplicación. La espera necesaria se expresa mediante las barreras de entrada del objeto protegido.

Estado inicialPut (se añade 1 elemento)Put / GetGet (se retira el último elemento)Put (se ocupa el último hueco)Get (se libera un hueco)Get se bloquea (barrera Count=0)Put se bloquea (barrera Count=Buffer_Size)Vacío / Count=0Parcial / Count=1..Buffer_Size-1Lleno / Count=Buffer_Size

Figura 7: Estados del búfer acotado y transiciones provocadas por Put y Get; las barreras Count=0 y Count=Buffer_Size bloquean Get y Put respectivamente.

Cuando Put tiene éxito se reevalúa la barrera de Get, y cuando Get tiene éxito se reevalúa la barrera de Put. Esto ocurre al completarse la operación protegida, con independencia del estado en que se encuentre el diagrama.

9. Medición del tiempo de ejecución — primer paso hacia la monitorización

Para evaluar la planificabilidad de un sistema de tiempo real hace falta conocer con precisión el tiempo de ejecución (tiempo de CPU) de cada tarea. El paquete Ada.Execution_Time de Ada proporciona el tiempo de consumo de CPU por tarea.

-- 07_execution_time.ada
-- Control del tiempo de ejecución (Execution_Time)
-- Mide el tiempo de consumo de CPU de cada tarea

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;

Tiempo de reloj de pared frente a tiempo de CPU:

Tiempo de reloj de pared (Wall Clock): Ada.Real_Time.Clock
  → Tiempo real transcurrido. Incluye el tiempo bloqueado o apropiado.

Tiempo de CPU (Execution Time): Ada.Execution_Time.Clock
  → Solo el tiempo en que esa tarea realmente se ejecutó en la CPU.
  → No se cuenta el tiempo bloqueado ni apropiado.

Esta distinción es el punto de partida de la monitorización del tiempo de ejecución y de la verificación del WCET. Mientras Busy_Worker espera con delay until, el tiempo de CPU no aumenta; solo aumenta durante el procesamiento de cálculo real. Durante el delay until Clock + Milliseconds(500) de la tarea principal, el tiempo de CPU también debería ser prácticamente cero. Sin embargo, la medición del tiempo de CPU no garantiza el verdadero WCET. El WCET, que incluye efectos de caché, del pipeline y de contención de memoria, requiere análisis estático y verificación por separado en el entorno de destino.

Tiempo de CPUDesglose: solo el cálculo realTiempo de CPU total: 120msTiempo de reloj de paredDesglose: cálculo + espera + bloqueo + apropiaciónTiempo total transcurrido: 500msDiferencia = tiempo de espera, bloqueo y apropiaciónEl tiempo de CPU observa el coste de cálculo reales un apoyo para la verificación y monitorización del WCETexcluye el tiempo de espera, bloqueo y apropiaciónAdvertenciala medición no garantiza el verdadero WCEThace falta análisis estático y verificación en el entorno de destino

Figura 8: Relación entre tiempo de reloj de pared y tiempo de CPU, y su papel como apoyo (no garantía) para la verificación del WCET.

10. Demo integral — sistema de tiempo real multiperiódico

Integramos ahora todos los elementos vistos hasta aquí —prioridad, Ceiling_Locking, delay until y objetos protegidos— para construir un sistema de tiempo real multiperiódico típico.

-- 08_multiperiodic.ada
-- Demo integral de un sistema de tiempo real multiperiódico
-- Tarea de lectura de sensor de periodo rápido (100ms)
-- Tarea de control de periodo lento (400ms)
-- Compartición de datos mediante 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;

Estructura del sistema:

El siguiente diagrama es un ejemplo de planificación que combina los instantes de release del código de ejemplo (el sensor rápido tiene un periodo de 100 ms y el control lento un periodo de 400 ms con un offset de 150 ms) con tiempos de ejecución supuestos con fines ilustrativos. Como el código en sí no incluye los 80 ms de cálculo de control, no se trata de un diagrama con mediciones reales. Cuando la prioridad del sensor rápido es mayor, si su release llega mientras se ejecuta el control lento, este último se interrumpe temporalmente. Por claridad, el diagrama dibuja una única interrupción representativa por cada periodo de control lento, pero en realidad el sensor rápido se libera en cada frontera de 100 ms.

Control lento ciclo 3 (release=950ms)Control lento ciclo 2 (release=550ms)Control lento ciclo 1 (release=150ms)1000-1010mssensor rápido #10como tiene P+3, interrumpe950-1000mscontrol lento #3 primera mitad1010-1040mscontrol lento #3 segunda mitad600-610mssensor rápido #6como tiene P+3, interrumpe550-600mscontrol lento #2 primera mitad610-640mscontrol lento #2 segunda mitad150-200mscontrol lento #1 primera mitad100-110mssensor rápido #1200-210mssensor rápido #2como tiene P+3, interrumpe210-240mscontrol lento #1 segunda mitadSuposición ilustrativasensor rápido: procesamiento 10mscontrol lento: procesamiento 80ms

Figura 9: Ejemplo ilustrativo de planificación multiperiódica: el sensor rápido interrumpe periódicamente al control lento en cada frontera de 100 ms.

Este patrón es la estructura típica de «captura de sensores rápida + bucle de control lento» que se observa con frecuencia en sistemas de control industrial y de robótica.

11. Ámbitos donde brillan las funciones de tiempo real de Ada

Las funciones de tiempo real de Ada aportan un valor especial en ámbitos como los siguientes.

Ada Annex Dfunciones de tiempo realAeroespacialDO-178CFerroviariofamilia EN 50128AutomotrizISO 26262Dispositivos médicosIEC 62304Control industrialfamilia IEC 61508Defensa y sistemas de alta fiabilidadControl de vueloámbito de aplicación con amplio historialControl de satélites y naves espacialesSistemas de señalizaciónControl automático de trenesCandidato para ECU relacionadas con la seguridadAplicación limitada y selectivaen un ámbito dominado por C / MISRA-CMarcapasosBombas de infusiónControl de robotsMáquinas herramienta de CNCComputadoras de misiónSistemas de operación a largo plazo

Figura 10: Ámbitos de aplicación de las funciones de tiempo real de Ada, organizados por sector regulatorio.

En el diagrama, solo el sector automotriz aparece marcado como «aplicación limitada y selectiva», y el motivo tiene más que ver con el tamaño del ecosistema existente que con la idoneidad del lenguaje en sí. El software de automoción se ha construido, en torno a C y MISRA-C, sobre especificaciones de API de estándares del sector como AUTOSAR, código suministrado por proveedores, compiladores y herramientas de verificación con certificación, e incluso la propia base de ingenieros disponibles. Cambiar de lenguaje, aunque solo afecte a un único componente, implica volver a preparar todo el instrumental que lo rodea junto con los procesos de aprovisionamiento y verificación. Por eso Ada tiende a posicionarse no como un estándar para todo el vehículo, sino como una opción usada de forma selectiva en determinados componentes que requieren garantías especialmente altas, o en organizaciones que ya cuentan con activos y una estructura de desarrollo en Ada. Dicho de otro modo, en ámbitos como el aeroespacial o el ferroviario, donde todo el ecosistema ya está orientado hacia la alta fiabilidad, esta barrera simplemente no existe.

12. Consideraciones y limitaciones

Las funciones de tiempo real de Ada son potentes, pero no son una solución universal.

1. Dependencia de la plataforma:

  • La correspondencia real de pragma Priority depende del entorno de ejecución (sistema operativo + runtime de GNAT). En Linux se traduce en SCHED_FIFO, pero en Windows puede no garantizarse una apropiación completa.

2. Restricciones de Ravenscar:

  • Como está prohibida la creación dinámica de tareas, es necesario declarar todas las tareas de forma estática en el arranque del sistema. Esto limita la libertad de diseño.

3. Límites de la medición del WCET:

  • Ada.Execution_Time es una medición, no una garantía. El verdadero WCET, que incluye fallos de caché y riesgos del pipeline, debe verificarse por separado con herramientas de análisis estático.

4. Sobrecarga:

  • La evaluación de las barreras de un objeto protegido se ejecuta automáticamente al completarse o cancelarse una entrada, y al salir del objeto protegido. En objetos protegidos invocados con alta frecuencia, hay que tener en cuenta esta sobrecarga.

5. Barrera de la cadena de herramientas:

  • Para aprovechar al máximo las funciones de tiempo real de Ada hace falta un compilador cruzado y un runtime adecuados. Especialmente en plataformas embebidas, esto implica depender del runtime proporcionado por el fabricante.

13. Resumen

En este artículo hemos recorrido paso a paso, con 8 ejemplos de código, las funciones de tiempo real que ofrece el Annex D de Ada.

Función Valor que aporta
Prioridad de tareas Planificación apropiativa basada en prioridad
Ceiling_Locking Prevención de la inversión de prioridad integrada en el lenguaje
delay until Ejecución periódica sin deriva acumulada
Perfil Ravenscar Subconjunto de tareas fácil de analizar estáticamente
Eventos de temporización Activación por tiempo sin necesidad de sondeo
Cola protegida Sincronización basada en barreras de objetos protegidos
Medición del tiempo de ejecución Monitorización del tiempo de CPU por tarea
Integración multiperiódica Diseño que permite coexistir con seguridad tareas de distintos periodos

La esencia de las funciones de tiempo real de Ada es que no son un añadido a posteriori. Las reglas de bloqueo que contienen la inversión de prioridad, la especificación de tiempo para la ejecución periódica, la monitorización del tiempo de ejecución: todo esto se ofrece como parte de la especificación del lenguaje. Por supuesto, el cumplimiento efectivo de los plazos sigue confirmándose mediante diseño y análisis, pero contar con un runtime de lenguaje que ya prepara esas premisas es una ventaja considerable.

Como siguiente paso, para probar el desarrollo de sistemas de tiempo real con Ada, instale la cadena de herramientas GNAT con Alire y compile el código de ejemplo de este artículo con gnatchop + gnatmake.

Para los fundamentos de la concurrencia en Ada (tareas, rendez-vous, objetos protegidos), consulte el artículo anterior, «Concurrencia segura en Ada».

14. Referencias

Artículos recientes con las mismas etiquetas para profundizar en temas cercanos.

Estas páginas sitúan el tema en un contexto más amplio de servicios y decisiones.

Preguntas frecuentes

Preguntas habituales en las consultas sobre el tema del artículo.

¿Qué es el Annex D de Ada?
Es un conjunto de funciones para sistemas de tiempo real estandarizadas como parte de la especificación del lenguaje Ada. Incluye planificación apropiativa basada en prioridad mediante FIFO_Within_Priorities, el protocolo Ceiling_Locking para evitar la inversión de prioridad, ejecución periódica en tiempo absoluto mediante delay until, el perfil Ravenscar, eventos de temporización y la medición del tiempo de ejecución por tarea con Ada.Execution_Time. Su rasgo distintivo es que no se trata de una biblioteca añadida a posteriori, sino que está integrado en el propio runtime del lenguaje.
¿Qué es la inversión de prioridad? ¿Cómo la evita Ada?
Es el fenómeno en el que una tarea de baja prioridad, mientras mantiene un bloqueo, es apropiada por una tarea de prioridad media, de modo que una tarea de alta prioridad que espera ese bloqueo queda bloqueada indefinidamente. Ocurrió realmente en la sonda Mars Pathfinder en 1997 y provocó reinicios repetidos de la nave. En Ada, el protocolo Ceiling_Locking se ofrece como una característica del lenguaje: una tarea que entra en un objeto protegido asciende automáticamente a la prioridad techo, lo que impide la apropiación por parte de tareas de prioridad media.
¿Qué es el perfil Ravenscar?
Es un perfil que, para sistemas de seguridad crítica, restringe las funciones de tareas de Ada a un subconjunto estáticamente analizable y determinista. Prohíbe la creación dinámica de tareas, la sentencia select, la sentencia abort, el delay relativo y la sentencia requeue, entre otras cosas. Esta restricción facilita el análisis estático de temporización y ayuda a cumplir las características exigidas por normas de seguridad como DO-178C (software aeronáutico) o ISO 26262 (seguridad funcional automotriz). En GNAT se activa escribiendo pragma Profile (Ravenscar) en el archivo gnat.adc.
¿Por qué usar delay until en lugar de delay en tareas periódicas?
Porque con delay, que especifica un tiempo relativo, el tiempo de procesamiento de cada iteración se va acumulando y el periodo se desplaza progresivamente (deriva acumulada). delay until determina el siguiente instante de activación a partir de un tiempo absoluto, de modo que aunque un procesamiento se retrase, los instantes de activación posteriores se mantienen correctos. Sin embargo, si el procesamiento supera el siguiente instante de despertar, delay until retorna casi de inmediato, por lo que es necesario diseñar por separado la detección de ese caso como deadline miss y tratarlo como sobrecarga.

Perfil del autor

Página de presentación del autor del artículo.

Go Komura

Representante de KomuraSoft LLC

Especializado en desarrollo de software para Windows, consultoría técnica e investigación de fallos, sobre todo en proyectos con sistemas existentes y errores difíciles de reproducir.

Volver al blog