Программирование систем реального времени на Ada — приоритеты, периодичность и контроль времени выполнения на практике

· · Ada, Реальное время, Ravenscar, CeilingLocking, Таскинг, Планирование, Инверсия приоритета, Язык программирования, Реальное время, Высокая надёжность

1. Введение — глубокая связь Ada и реального времени

В предыдущей статье «Безопасная конкурентность в Ada» мы разобрали основы безопасной конкурентности в Ada на базе задач (task) и защищённых объектов (protected object). В этой статье мы двигаемся дальше — в более жёстко ограниченную область, системы реального времени.

В системах реального времени «правильность» означает не только логическую корректность результата вычислений, но и то, что результат получен в срок. Правильный ответ, полученный на миллисекунду позже дедлайна, так же опасен, как и неправильный.

Ada отвечает на это требование обширным набором функций реального времени, стандартизированным как Annex D (Real-Time Systems) спецификации языка. Это не «библиотека, добавленная поверх», а гарантии реального времени, встроенные непосредственно в среду выполнения языка.

Функции реального времени в Ada (Annex D):
- Приоритеты задач и вытеснение (FIFO_Within_Priorities)
- Протокол Ceiling_Locking (предотвращение инверсии приоритета)
- Периодическое выполнение по абсолютному времени через delay until
- Профиль Ravenscar (подмножество для систем с критичной безопасностью)
- Тайминговые события (пробуждение по времени без опроса)
- Мониторинг времени выполнения (Ada.Execution_Time)
- Мультипериодическое планирование

В этой статье мы последовательно разберём эти возможности на 8 практических примерах кода. Каждый фрагмент можно рассматривать как самостоятельный пример, но примеры 04 и 05, содержащие несколько единиц компиляции, нужно сначала разделить с помощью gnatchop, а затем собрать через gnatmake.

Все фрагменты кода из этой статьи опубликованы на GitHub в виде справочного набора, организованного по файлам — по одному на главу.

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

2. Что такое система реального времени

Начнём с уточнения терминологии.

Понятие Описание
Жёсткое реальное время (hard real-time) Превышение дедлайна означает критический отказ системы (управление полётом, подушки безопасности, кардиостимуляторы)
Мягкое реальное время (soft real-time) Превышение дедлайна нежелательно, но редкие случаи допустимы (потоковое видео, игры)
Дедлайн (deadline) Абсолютное время, к которому задача должна завершиться
Период (period) Интервал времени, с которым задача запускается повторно
WCET (Worst-Case Execution Time) Наихудшее время выполнения задачи
Джиттер (jitter) Разброс (нестабильность) периодического выполнения

При проектировании систем реального времени важным необходимым условием является выполнение неравенства «WCET <= дедлайн» для каждой задачи. Однако само по себе это не гарантирует соблюдение сроков системой в целом — отдельно требуется анализ времени отклика, учитывающий время блокировки, назначение приоритетов, джиттер, прерывания, а также поведение среды выполнения и ОС. На практике, чтобы сохранить запас, обычно стремятся к WCET < дедлайн. Функции реального времени Ada предоставляют на уровне языка предсказуемую модель выполнения, которая облегчает такой анализ.

Требования реального времениПревышение срока = критический отказРедкие превышения допустимыДедлайнАбсолютное время завершенияПериодИнтервал повторенияWCETНаихудшее время выполненияДжиттерРазброс периодаЖёсткое реальное времяУправление полётомПодушка безопасностиКардиостимуляторМягкое реальное времяВидеопотокиИгрыUIAda Annex DМеханизмы предсказуемостиFIFO_Within_PrioritiesCeiling_Lockingdelay untilАнализ планируемостиНеобходимое условие: WCET &lt;= дедлайнДостаточность проверяется анализом времени отклика

Одно из самых опасных явлений в системах реального времени — инверсия приоритета. Эта проблема реально возникла в 1997 году на аппарате Mars Pathfinder и стала причиной повторяющихся перезагрузок.

Разделяемый ресурсСреднеприоритетная задачаВысокоприоритетная задачаНизкоприоритетная задачаПланировщикРазделяемый ресурсСреднеприоритетная задачаВысокоприоритетная задачаНизкоприоритетная задачаПланировщикВыполняется критическая секцияПробуждается H, планировщик приостанавливает LБлокировка! (удерживается L)H ждёт блокировку, поэтому L возобновляетсяПродолжает выполнение к освобождению блокировки...Пробуждается M, планировщик приостанавливает LL не может освободить блокировкуM продолжает выполнение (и H, и L заблокированы)[ИНВЕРСИЯ ПРИОРИТЕТА] Высокий приоритет заблокирован на неопределённый срокЗахватывает блокировкуПытается захватить блокировку

Низкоприоритетная задача, удерживающая блокировку, вытесняется среднеприоритетной задачей, и высокоприоритетная задача оказывается заблокированной на неопределённый срок. Реальным решением, применённым на Mars Pathfinder, было включение механизма priority inheritance в VxWorks, но Ada решает проблему того же рода другим способом — предоставляя Ceiling_Locking как встроенную возможность языка.

3. Основы приоритетов задач — FIFO_Within_Priorities

FIFO_Within_Priorities — это стандартная приоритетная политика диспетчеризации, которую можно задать в Ada Annex D. Если политика явно не указана, поведение по умолчанию определяется реализацией, но во многих целевых платформах GNAT используется политика именно этого семейства. В рамках одного уровня приоритета задачи выполняются по принципу FIFO (первым пришёл — первым обслужен), а задача с более высоким приоритетом вытесняет (прерывает) задачу с более низким приоритетом.

-- 01_task_priority.ada
-- Основы приоритетов задач и FIFO_Within_Priorities
-- Конфигурационная прагма должна стоять перед контекстными предложениями (with)

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;

Ключевые моменты:

  • pragma Priority присваивает каждой задаче статический приоритет. Priority'Last — наивысший, Priority'First — наинизший.
  • Тела задач в этом примере не выполняют тяжёлых вычислений, а просто ожидают до указанного момента через delay until. Здесь важно показать, что когда обе задачи одновременно готовы к выполнению, первой получает возможность выполниться задача с более высоким приоритетом.
  • Эта диаграмма демонстрирует именно сторону вытеснения между разными уровнями приоритета в рамках FIFO_Within_Priorities. Чтобы показать порядок FIFO внутри одного уровня приоритета, нужен отдельный пример с несколькими задачами одинакового приоритета.
  • На практике приоритеты обычно проектируют относительно System.Default_Priority.
Низкоприоритетная задача(Priority=First)Высокоприоритетная задача(Priority=Last)Главная задачаПланировщикНизкоприоритетная задача(Priority=First)Высокоприоритетная задача(Priority=Last)Главная задачаПланировщикT=0мс: обе задачи готовы к выполнениюВыводит стартовое сообщениеВыводит стартовое сообщениеT=100мс: пробуждается HPВыводит сообщение о завершенииT=500мс: пробуждается LPВыводит сообщение о завершении(T=800мс) главная задача завершенаСоздаёт задачуСоздаёт задачуВыбирает HP как наивысший приоритетБлокируется до T+100мсДалее выполняет LPБлокируется до T+500мсВыполняет HPВыполняет LP
Диапазон приоритетов Ada (по умолчанию в GNAT):
  Priority'First  = 0   (наинизший)
  Priority'Last   = 30  (наивысший, зависит от ОС)

4. Ceiling_Locking — предотвращение инверсии приоритета средствами языка

Одна из самых неприятных проблем в системах реального времени — инверсия приоритета (priority inversion). Высокоприоритетная задача ожидает блокировку, удерживаемую низкоприоритетной задачей, а та в это время вытесняется среднеприоритетной задачей — в результате высокоприоритетная задача оказывается заблокированной на неопределённый срок.

Ada решает эту проблему, встраивая протокол Ceiling_Locking непосредственно в защищённые объекты.

-- 02_ceiling_locking.ada
-- Предотвращение инверсии приоритета протоколом Ceiling_Locking
-- Конфигурационная прагма должна стоять перед контекстными предложениями (with)

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;

Как работает Ceiling_Locking:

  1. Для защищённого объекта через pragma Priority (Ceiling) устанавливается потолочный приоритет (ceiling priority).
  2. При входе в защищённый объект любая задача автоматически повышается до потолочного приоритета.
  3. Благодаря этому задачу, использующую защищённый объект, не может вытеснить среднеприоритетная задача.
  4. При выходе из защищённого объекта приоритет возвращается к исходному значению.

Диаграмма ниже — не точная временная трасса приведённого выше примера кода, а концептуальная схема того, как Ceiling_Locking подавляет паттерн инверсии приоритета из диаграммы в разделе 2.

Защищённый объект(потолок=30)Высокоприоритетная задача(приоритет=30)Среднеприоритетная задача(приоритет=20)Низкоприоритетная задача(приоритет=10)ПланировщикЗащищённый объект(потолок=30)Высокоприоритетная задача(приоритет=30)Среднеприоритетная задача(приоритет=20)Низкоприоритетная задача(приоритет=10)ПланировщикВызывающий с активным приоритетом > потолка получает Program_ErrorH(30) на схеме равен потолку (30), поэтому вход разрешёнАктивный приоритет повышается до 30Пробуждается ML выполняется с потолочным приоритетом 30M (20) не может его вытеснитьПробуждается HH (30) проходит проверку потолкано ждёт, так как PO занят LПриоритет возвращается к 10После освобождения PO выполняется HH (30) = потолок (30), вход возможен после снятия конкуренцииВходит в защищённую операциюВыполняет операциюВыходит из защищённой операцииВходит в защищённую операциюВыходит из защищённой операции

Правило проектирования: потолочный приоритет защищённого объекта должен быть не ниже наивысшего приоритета среди всех задач, использующих этот объект. Если это правило нарушено и защищённую операцию вызывает задача с активным приоритетом выше потолочного, Ada может обнаружить эту ошибку проектирования через исключение Program_Error.

Чтобы добиться того же эффекта с мьютексами pthread в C, нужно явно задать атрибут PTHREAD_PRIO_PROTECT, тогда как в Ada это стандартная возможность языка.

5. delay until — выполнение периодических задач без дрейфа

Базовый паттерн систем реального времени — периодическая задача, которая запускается через фиксированный интервал. Крайне важно предотвращать накопление временной ошибки (дрейфа) при таком повторяющемся выполнении.

delay until в Ada элегантно решает эту проблему.

-- 03_periodic_task.ada
-- Периодическая задача через delay until — предотвращение накопительного дрейфа

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;

Почему именно delay until:

Подход Проблема
delay Period; Время обработки каждой итерации накапливается, период постепенно уплывает (кумулятивный дрейф)
delay until Next_Release; Next_Release := Next_Release + Period; Опирается на абсолютное время, поэтому даже при задержке одной итерации время следующего запуска остаётся корректным

Однако delay until автоматически не гарантирует, что время обработки укладывается в период. Если обработка превысила время следующего пробуждения, соответствующий вызов delay until возвращает управление практически мгновенно, и система оказывается в состоянии, которое следует трактовать как пропуск дедлайна (deadline miss).

Со случаем delay:
  T=0мс → обработка(15мс) → delay 100мс → T=115мс → обработка(10мс) → ...
  Фактические интервалы: 115мс, 110мс, ... (время обработки накапливается)

Со случаем delay until:
  Next_Release: 100мс, 200мс, 300мс, ... (абсолютное время)
  T=0мс → обработка(15мс) → delay until 100мс → T=100мс → обработка(10мс) → delay until 200мс
  Фактические интервалы: 100мс, 100мс, ... (не зависят от времени обработки)

Этот паттерн delay until используется во всех периодических задачах, встречающихся далее в статье.

Превышение периода - deadline missвычисления 130мсСледующий = T+100мсdelay until T+100мс возвращается мгновенноОбнаружение задержки и обработка как перегрузкиdelay until - на основе абсолютного временивычисления 15мсСледующий = T+100мсdelay until T+100мс → пробуждение на 100мсвычисления 10мсСледующий = T+200мс → пробуждение на 200мсФактические интервалы: 100мс, 100мс...delay Period - кумулятивный дрейфdelay 100мс → пробуждение на 115мсT=0мс: вычисления 15мсвычисления 10мс → 125мсdelay 100мс → пробуждение на 225мсФактические интервалы: 115мс, 110мс...Ошибка накапливается со временемПредотвращает кумулятивный дрейф

6. Профиль Ravenscar — верифицируемое подмножество для реального времени

Возможности таскинга Ada мощны, но в системах, где критична безопасность, эта мощь сама становится проблемой: динамическое создание задач, операторы select и abort затрудняют статический анализ наихудшего времени выполнения.

Профиль Ravenscar — ответ Ada на эту проблему. Он ограничивает возможности таскинга статически анализируемым, детерминированным подмножеством.

-- 04_ravenscar_profile.ada
-- Основы профиля Ravenscar
-- Указывается при компиляции через pragma Profile (Ravenscar); в файле gnat.adc

with Ada.Text_IO;               use Ada.Text_IO;
with System;                    use System;
with Ada.Real_Time;             use Ada.Real_Time;

package Ravenscar_State is

   protected Signal is
      pragma Priority (System.Default_Priority + 5);
      entry Wait_For_Release;
      procedure Release;
   private
      Released : Boolean := False;
   end Signal;

   task Periodic_Worker is
      pragma Priority (System.Default_Priority + 1);
      pragma Storage_Size (4 * 1024);
   end Periodic_Worker;

   task Monitor is
      pragma Priority (System.Default_Priority);
      pragma Storage_Size (4 * 1024);
   end Monitor;

end Ravenscar_State;

with Ada.Text_IO;               use Ada.Text_IO;
with System;                    use System;
with Ada.Real_Time;             use Ada.Real_Time;

package body Ravenscar_State is

   protected body Signal is
      entry Wait_For_Release when Released is
      begin
         Released := False;
      end Wait_For_Release;

      procedure Release is
      begin
         Released := True;
      end Release;
   end Signal;

   task body Periodic_Worker is
      Start_Time   : constant Time := Clock;
      Next_Release : Time := Start_Time + Milliseconds (100);
      Period       : constant Time_Span := Milliseconds (100);
      Cycle_Count  : Natural := 0;
   begin
      Put_Line ("[Worker] Ravenscar periodic task starts");

      for I in 1 .. 4 loop
         delay until Next_Release;

         Cycle_Count := Cycle_Count + 1;
         Put_Line ("[Worker] Cycle" & Natural'Image (Cycle_Count) &
                   " at" & Duration'Image (To_Duration (Clock - Start_Time)) & "s");
         Signal.Release;
         Next_Release := Next_Release + Period;
      end loop;

       Put_Line ("[Worker] Finished demo, waiting (Ravenscar: No_Task_Termination)");
      loop
         delay until Clock + Seconds (1);
      end loop;
   end Periodic_Worker;

   task body Monitor is
   begin
      Put_Line ("[Monitor] Waiting for signals...");

      for I in 1 .. 4 loop
         Signal.Wait_For_Release;
         Put_Line ("[Monitor] Received signal" & Natural'Image (I));
      end loop;

      Put_Line ("[Monitor] All signals received, waiting (Ravenscar: No_Task_Termination)");
      loop
         delay until Clock + Seconds (1);
      end loop;
   end Monitor;

end Ravenscar_State;

with Ravenscar_State; use Ravenscar_State;
with Ada.Text_IO;     use Ada.Text_IO;
with Ada.Real_Time;   use Ada.Real_Time;

procedure Ravenscar_Demo is
begin
   Put_Line ("=== Ravenscar Profile Demo ===");
   Put_Line ("(compile with: gnatmake -gnatec=gnat.adc ravenscar_demo)");
   Put_Line ("Main: waiting for Ravenscar tasks...");
   delay until Clock + Milliseconds (800);
   Put_Line ("Main: demo window elapsed; waiting forever (Ravenscar: No_Task_Termination)");
   loop
      delay until Clock + Seconds (1);
   end loop;
end Ravenscar_Demo;

Ограничения профиля Ravenscar:

Запрещённая возможность Причина
Динамическое создание задач (new и типы доступа) Выделение памяти во время выполнения недетерминировано
Оператор select Сложность анализа управляющего потока создаёт не только выбор альтернативы, но и вся конструкция select в целом
Оператор abort Асинхронное прерывание делает состояние непредсказуемым
Ada.Task_Attributes Динамическое поведение во время выполнения
Динамическое изменение приоритета Предположения анализа планирования меняются во время выполнения
Относительный delay Легко порождает кумулятивный дрейф — используйте абсолютный delay until
Несколько входов (entry) на один защищённый объект Увеличивает число условий блокировки и объём анализа
Завершение задач В Ravenscar все задачи считаются незавершающимися
Оператор requeue Усложняет отслеживание управляющего потока

Благодаря этим ограничениям программа, соответствующая Ravenscar, становится гораздо удобнее для статического анализа временных характеристик. Это свойство требуется стандартами безопасности, такими как DO-178C (авиационное ПО) и ISO 26262 (функциональная безопасность автомобилей). Список выше — это выдержка из основных ограничений; реальный профиль включает дополнительные правила, связанные со средой выполнения и анализируемостью, например No_Task_Hierarchy и Detect_Blocking.

Полные возможности таскинга AdaПрофиль RavenscarОграниченияОбязательные политикиЗапрет динамического создания задачЗапрет оператора selectЗапрет оператора abortЗапрет Task_AttributesОграничение: 1 вход на защищённый объектЗапрет оператора requeueЗапрет относительного delayиспользуется delay untilЗапрет динамического изменения приоритетовЗапрет завершения задачвсе задачи незавершающиесяFIFO_Within_PrioritiesCeiling_LockingОблегчает:статический анализ временных характеристикDO-178CАвиационное ПОISO 26262Функциональная безопасность автомобилейIEC 62304ПО медицинских приборов

Чтобы включить профиль Ravenscar, добавьте следующую строку в файл gnat.adc:

pragma Profile (Ravenscar);

7. Тайминговые события — пробуждение по времени без опроса

Во многих системах реального времени часто встречается требование «разбудить высокоприоритетную задачу в определённый момент времени». Наивная реализация опрашивала бы таймер, но Ada предлагает более элегантный механизм — тайминговые события (timing events).

-- 05_timing_events.ada
-- Тайминговые события (Ada.Real_Time.Timing_Events)
-- Механизм пробуждения высокоприоритетной задачи без опроса

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;

Как работают тайминговые события:

1. Set_Handler(Timer_1, T+100мс, S.Fire'Access)  — регистрирует обработчик на абсолютное время
2. Проходит T+100мс — runtime вызывает S.Fire **на потолочном приоритете**
3. Fire устанавливает флаг Fired в True — барьер открывается
4. Задача Reactor пробуждается из Wait_For_Event

Важно, что в этом примере явно указан Ceiling_Locking, и поскольку обработчик Fire — это защищённая процедура, она выполняется на потолочном приоритете. Защищённую процедуру, используемую как обработчик тайминговых событий, следует помещать в защищённый объект с потолочным приоритетом уровня прерывания — в данном случае System.Interrupt_Priority'Last. Благодаря этому во время обработки тайминговых событий инверсия приоритета не возникает.

8. Очередь реального времени на основе защищённых объектов

Часто встречающийся в системах реального времени паттерн — производитель-потребитель (producer-consumer): датчик генерирует данные, а управляющая задача их потребляет. Здесь важно эффективно организовать взаимное исключение доступа к буферу и блокировку.

Защищённые объекты и барьеры входа (entry barrier) Ada позволяют реализовать это как синхронизацию на основе барьеров. Взаимное исключение внутри управляется средой выполнения, поэтому прикладному коду не нужно напрямую использовать мьютексы или условные переменные.

-- 06_protected_queue.ada
-- Совместное использование данных реального времени через защищённый объект
-- Конвейер: Producer -> Bounded_Buffer -> Consumer
-- Указывается при компиляции через pragma Locking_Policy (Ceiling_Locking); в файле 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;

Ключевые моменты проектирования:

  • entry Put when Count < Buffer_Size — когда буфер заполнен, Producer автоматически блокируется.
  • entry Get when Count > 0 — когда буфер пуст, Consumer автоматически блокируется.
  • pragma Priority (System.Any_Priority'Last) — благодаря Ceiling_Locking между Producer и Consumer не возникает инверсии приоритета.
  • Условия барьера определяются внутренним состоянием защищённого объекта (Count) и автоматически переоцениваются при освобождении блокировки.

В этом коде нет прикладных мьютексов, семафоров или условных переменных. Всё необходимое ожидание выражается через барьеры входа защищённого объекта.

Начальное состояниеPut (добавлен 1 элемент)Put / GetGet (извлечён последний элемент)Put (заполнено последнее место)Get (освобождается место)Get блокируется (барьер Count=0)Put блокируется (барьер Count=Buffer_Size)Пусто / Count=0Частично заполнен / Count=1..Buffer_Size-1Заполнен / Count=Buffer_Size

После успешного Put переоцениваются барьеры ожидающих Get, а после успешного Get — барьеры ожидающих Put. Эта переоценка происходит при завершении защищённой операции независимо от того, в каком состоянии на диаграмме находится объект.

9. Измерение времени выполнения — первый шаг к мониторингу

Чтобы оценить планируемость системы реального времени, нужно точно знать время выполнения (процессорное время) каждой задачи. Пакет Ada.Execution_Time в Ada предоставляет учёт потреблённого процессорного времени в разрезе задач.

-- 07_execution_time.ada
-- Контроль времени выполнения (Execution_Time)
-- Измеряет потребление процессорного времени по каждой задаче

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;

Астрономическое время vs процессорное время:

Астрономическое время (Wall Clock): Ada.Real_Time.Clock
  → Фактически прошедшее время. Включает время блокировки и вытеснения.

Процессорное время (Execution Time): Ada.Execution_Time.Clock
  → Только время, которое задача реально выполнялась на CPU.
  → Время блокировки и вытеснения не учитывается.

Это различие — отправная точка для мониторинга времени выполнения и верификации WCET. Пока Busy_Worker ожидает в delay until, процессорное время не увеличивается — оно растёт только во время реальных вычислений. Аналогично, пока главная задача ждёт в delay until Clock + Milliseconds(500), её процессорное время должно оставаться практически нулевым. Однако фактическое измерение процессорного времени не гарантирует истинный WCET: он зависит от кэша, конвейера, конкуренции за память и требует отдельной верификации через статический анализ или тестирование на целевой платформе.

Процессорное времяВключает только: реальные вычисленияСуммарное CPU-время: 120мсАстрономическое времяВключает: вычисления + ожидание + блокировку + вытеснениеСуммарное прошедшее время: 500мсРазница = время ожидания, блокировки, вытесненияПроцессорное время отражает реальную стоимость вычисленийПомогает при верификации и мониторинге WCETИсключает время ожидания, блокировки и вытесненияВниманиеИзмерение не гарантирует истинный WCETТребуется статический анализ или проверка на целевой платформе

10. Комплексная демонстрация — мультипериодическая система реального времени

Теперь объединим всё, что мы изучили — приоритеты, Ceiling_Locking, delay until и защищённые объекты — и построим типичную мультипериодическую систему реального времени.

-- 08_multiperiodic.ada
-- Комплексная демонстрация мультипериодической системы реального времени
-- Задача чтения датчика с быстрым периодом (100мс)
-- Управляющая задача с медленным периодом (400мс)
-- Совместный доступ к данным через 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;

Архитектура системы:

Диаграмма ниже строится на реальных моментах release из примера кода (быстрый датчик — период 100мс, медленное управление — период 400мс со смещением 150мс), но использует иллюстративные значения времени выполнения. Это не результат реального измерения: в самом коде нет вычисления управления длительностью 80мс. Если у быстрого датчика более высокий приоритет, то release датчика во время выполнения медленного управления приводит к его временному прерыванию. Для наглядности на диаграмме показано только по одному характерному прерыванию на каждый цикл медленного управления, хотя на практике быстрый датчик получает release на каждой границе в 100мс.

Медленное управление, цикл 3 (release=950мс)Медленное управление, цикл 2 (release=550мс)Медленное управление, цикл 1 (release=150мс)1000-1010мсБыстрый датчик #10прерывает как P+3950-1000мсМедленное управление #3, часть A1010-1040мсМедленное управление #3, часть B600-610мсБыстрый датчик #6прерывает как P+3550-600мсМедленное управление #2, часть A610-640мсМедленное управление #2, часть B150-200мсМедленное управление #1, часть A100-110мсБыстрый датчик #1200-210мсБыстрый датчик #2прерывает как P+3210-240мсМедленное управление #1, часть BИллюстративное допущениеБыстрый датчик: обработка 10мсМедленное управление: обработка 80мс

Этот паттерн — «быстрый сбор данных с датчиков + медленный контур управления» — типичная структура, часто встречающаяся в промышленных системах управления и робототехнике.

11. Где особенно раскрываются функции реального времени Ada

Функции реального времени Ada особенно ценны в следующих областях.

Ada Annex DФункции реального времениАвиакосмическая отрасльDO-178CЖелезнодорожный транспортсерия EN 50128АвтомобилестроениеISO 26262Медицинские приборыIEC 62304Промышленное управлениесерия IEC 61508Оборона и высоконадёжные системыУправление полётомобласть с богатым опытом примененияУправление спутниками и космическими аппаратамиСигнальные системыАвтоматическое управление поездамиКандидат для ECU, связанных с безопасностьюОграниченное, выборочное применениев области, где доминируют C / MISRA-CКардиостимуляторыИнфузионные насосыУправление роботамиСтанки с ЧПУБортовые вычислители миссийСистемы долгосрочной эксплуатации

12. Ограничения и нюансы

Функции реального времени Ada мощны, но не являются панацеей.

1. Зависимость от платформы:

  • Фактическое отображение pragma Priority зависит от среды выполнения (ОС + среда выполнения GNAT). В Linux оно отображается на SCHED_FIFO, а в Windows полное вытеснение может быть не гарантировано.

2. Ограничения Ravenscar:

  • Поскольку динамическое создание задач запрещено, все задачи должны быть статически объявлены при запуске системы. Это ограничивает свободу проектирования.

3. Ограничения измерения WCET:

  • Ada.Execution_Time даёт измерение, а не гарантию. Истинный WCET, учитывающий промахи кэша и конфликты конвейера, нужно отдельно верифицировать инструментами статического анализа.

4. Накладные расходы:

  • Переоценка барьеров защищённого объекта выполняется автоматически при завершении или отмене входа, а также при выходе из защищённого объекта. Для часто вызываемых защищённых объектов эти накладные расходы нужно учитывать.

5. Барьер инструментария:

  • Чтобы полноценно использовать функции реального времени Ada, требуются подходящие кросс-компилятор и среда выполнения. Особенно для встраиваемых целевых платформ это означает зависимость от поставляемой производителем среды выполнения.

13. Заключение

В этой статье мы последовательно рассмотрели функции реального времени, которые предоставляет Annex D в Ada, на 8 примерах кода.

Функция Ценность
Приоритеты задач Вытесняющее приоритетное планирование
Ceiling_Locking Предотвращение инверсии приоритета, встроенное в язык
delay until Периодическое выполнение без кумулятивного дрейфа
Профиль Ravenscar Подмножество таскинга, удобное для статического анализа
Тайминговые события Пробуждение по времени без опроса
Защищённая очередь Синхронизация на основе барьеров защищённого объекта
Измерение времени выполнения Мониторинг процессорного времени по задачам
Мультипериодическая интеграция Проектирование безопасного сосуществования задач с разными периодами

Суть функций реального времени Ada в том, что они «не добавлены задним числом». Правила блокировки, подавляющие инверсию приоритета, указание абсолютного времени для периодического выполнения, мониторинг времени выполнения — всё это предоставляется как часть спецификации языка. Соблюдение дедлайнов, разумеется, всё равно нужно подтверждать проектированием и анализом, но огромное преимущество в том, что необходимые для этого предпосылки уже обеспечивает сама среда выполнения языка.

Ada Annex DСистемы реальноговремениПланированиеFIFO_Within_PrioritiesПриоритеты задачВытеснениеПредотвращениеинверсии приоритетаПротокол Ceiling_LockingАвтоматическоеповышение приоритетаПравило потолочногоприоритетаПериодическоевыполнениеdelay untilПредотвращаеткумулятивный дрейфНа основе абсолютноговремениПрофиль RavenscarСтатический набор задачДетерминированныйанализЗапрет относительногоdelayDO-178C / ISO 26262Тайминговые событияПробуждение без опросаРегистрация защищённогообработчикаОбработчик выполняетсяна потолочном приоритетеЗащищённые объекты /очередиБарьеры входаУправление состояниямипусто/частично/полноСинхронизация на основебарьеровМониторинг временивыполненияПроцессорное время позадачамАстрономическое времяvs процессорноеПомощь в верификации имониторинге WCETКомплексноепроектированиеМультипериодическоепроектированиеЗащита через барьерыБезопасность на уровнеязыка

В качестве следующего шага, чтобы попробовать разработку систем реального времени на Ada самостоятельно, установите набор инструментов GNAT через Alire и соберите примеры кода из этой статьи с помощью gnatchop + gnatmake.

Основы конкурентности в Ada (задачи, рандеву, защищённые объекты) рассмотрены в предыдущей статье «Безопасная конкурентность в Ada».

14. Справочные материалы

Недавние статьи с теми же тегами помогут подробнее изучить близкие темы.

Эти страницы показывают тему статьи в более широком контексте услуг и решений.

Частые вопросы

Вопросы, которые часто возникают при консультациях по теме статьи.

Что такое Annex D в Ada?
Это набор функций для систем реального времени, стандартизированный как часть спецификации языка Ada. Он включает приоритетное вытесняющее планирование на основе FIFO_Within_Priorities, протокол Ceiling_Locking для предотвращения инверсии приоритета, периодическое выполнение по абсолютному времени через delay until, профиль Ravenscar, тайминговые события и измерение времени выполнения по задачам средствами Ada.Execution_Time. Отличительная особенность в том, что эти возможности встроены не в виде библиотеки поверх языка, а непосредственно в языковую среду выполнения (runtime).
Что такое инверсия приоритета и как Ada её предотвращает?
Это ситуация, когда низкоприоритетная задача, удерживающая блокировку, вытесняется среднеприоритетной задачей, из-за чего высокоприоритетная задача, ожидающая ту же блокировку, оказывается заблокированной на неопределённый срок. Именно это произошло в 1997 году на аппарате Mars Pathfinder и стало причиной повторяющихся перезагрузок. В Ada протокол Ceiling_Locking предоставляется как встроенная возможность языка: задача, входящая в защищённый объект, автоматически повышается до потолочного (ceiling) приоритета, что делает невозможным её вытеснение среднеприоритетными задачами.
Что такое профиль Ravenscar?
Профиль, который ограничивает возможности таскинга Ada статически анализируемым, детерминированным подмножеством — для систем, где критична безопасность. Он запрещает динамическое создание задач, операторы select и abort, относительный delay, оператор requeue и ряд других конструкций. Благодаря этим ограничениям становится значительно проще проводить статический анализ временных характеристик, что облегчает соответствие требованиям стандартов безопасности, таких как DO-178C (авиационное ПО) и ISO 26262 (функциональная безопасность автомобилей). В GNAT профиль включается директивой pragma Profile (Ravenscar) в файле gnat.adc.
Почему для периодических задач используют delay until, а не delay?
Потому что при использовании относительного delay время обработки каждой итерации накапливается, и период постепенно «уплывает» — возникает кумулятивный дрейф. delay until определяет момент следующего запуска относительно абсолютного времени, поэтому даже если одна итерация выполняется дольше обычного, время следующего запуска остаётся корректным. Однако если обработка превышает время следующего пробуждения, delay until возвращает управление почти мгновенно, поэтому отдельно нужно предусмотреть обнаружение такой ситуации как пропуска дедлайна (deadline miss) и обработку перегрузки.

Об авторе

Страница с профилем автора статьи.

Го Комура

Представитель KomuraSoft LLC

Специализируется на разработке программного обеспечения для Windows, техническом консалтинге и расследовании сбоев, особенно в проектах с унаследованными системами и трудно воспроизводимыми ошибками.

Публичные ссылки

Вернуться в блог