用 Ada 构建实时系统编程 ── 优先级、周期、执行时间控制实战

· · Ada, RealTime, Ravenscar, CeilingLocking, Tasking, Scheduling, PriorityInversion, ProgrammingLanguage, 实时系统, 高可靠性

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. 什么是实时系统

首先从整理术语开始。

概念 说明
硬实时 超过期限意味着系统发生致命失败(飞行控制、安全气囊、心脏起搏器)
软实时 超过期限并不理想,但偶尔超过仍可接受(视频流、游戏)
期限 (deadline) 任务必须完成的绝对时刻
周期 (period) 任务重复启动的时间间隔
WCET (Worst-Case Execution Time) 任务的最坏情况执行时间
抖动 (jitter) 周期执行的波动幅度

在实时系统设计中,每个任务必须满足「WCET <= 期限」,这是重要的必要条件。但仅凭这一点并不能保证整个系统都能按期完成。还需要另外进行涵盖阻塞时间、优先级分配、抖动、中断,以及运行时与 OS 行为的响应时间分析。实务中为了保留余量,通常会以 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 实际采取的对策,是启用 VxWorks 的 priority inheritance,而 Ada 针对同类问题则以另一种方式──Ceiling_Locking──作为语言功能提供。

3. 任务优先级的基础 ── FIFO_Within_Priorities

FIFO_Within_Priorities 是 Ada Annex D 中可以指定的标准优先级式派发策略。未明确指定策略时的默认行为属于实现定义,但 GNAT 在许多目标平台上都采用这一类策略。同一优先级内以 FIFO(先进先出)方式执行,优先级较高的任务会抢占(preempt)优先级较低的任务。

-- 01_task_priority.ada
-- 任务优先级与 FIFO_Within_Priorities 的基本形式
-- 配置 pragma 要放在 context 子句之前

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=0ms: 两个任务都可执行输出开始日志输出开始日志T=100ms: HP 唤醒输出完成日志T=500ms: LP 唤醒输出完成日志(T=800ms) 主任务结束创建任务创建任务选择优先级最高的 HP因 delay until T+100ms 而阻塞接着执行 LP因 delay until T+500ms 而阻塞执行 HP执行 LP
Ada 优先级的范围(GNAT 默认值):
  Priority'First  = 0   (最低)
  Priority'Last   = 30  (最高,但依 OS 而异)

4. Ceiling_Locking ── 由语言防止优先级反转

实时系统中最棘手的问题之一就是优先级反转 (priority inversion)。高优先级任务等待低优先级任务持有的锁,而该低优先级任务又被中优先级任务抢占,导致高优先级任务被无限期阻塞的现象。

Ada 针对这个问题,将 Ceiling_Locking 协议 直接内置于保护对象中。

-- 02_ceiling_locking.ada
-- 以 Ceiling_Locking 协议防止优先级反转
-- 配置 pragma 要放在 context 子句之前

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) 设定天花板优先级
  2. 无论哪个任务进入保护对象,进入时都会自动提升到天花板优先级
  3. 因此,使用中的保护对象不会被中优先级任务抢占。
  4. 离开保护对象后恢复原来的优先级。

下图不是前一段示例代码的精确时间追踪,而是概念图,说明图 2 的优先级反转模式在 Ceiling_Locking 下如何被抑制。

保护对象(天花板优先级=30)高优先级任务(优先级=30)中优先级任务(优先级=20)低优先级任务(优先级=10)调度器保护对象(天花板优先级=30)高优先级任务(优先级=30)中优先级任务(优先级=20)低优先级任务(优先级=10)调度器调用方的活动优先级若高于天花板优先级则会 Program_Error图中 H(30) 等于天花板(30),因此可以进入执行优先级提升为 30M 唤醒L 以天花板优先级 30 执行中M(20) 无法抢占H 唤醒H(30) 通过天花板检查但因 L 正在使用 PO 而等待优先级恢复为 10PO 释放后执行 HH(30) = 天花板(30),竞争解除后可进入进入保护操作执行操作离开保护操作进入保护操作离开保护操作

设计原则: 保护对象的天花板优先级,应设定为使用该保护对象的所有任务中最高优先级以上。若违反此原则,让活动优先级高于天花板优先级的任务调用保护操作,Ada 会通过 Program_Error 检测出这个设计错误。

在 C 语言中,要用 pthread mutex 实现相同效果,必须明确设置 PTHREAD_PRIO_PROTECT 属性,但在 Ada 中这是语言的标准功能。

5. delay until ── 让周期性任务执行时不产生漂移

实时系统的基本模式是周期性任务。在固定间隔重复执行的任务中,防止累积性的计时误差(漂移)极为重要。

Ada 的 delay until 能优雅地解决这个问题。

-- 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=0ms → 处理(15ms) → delay 100ms → T=115ms → 处理(10ms) → ...
  实际间隔:115ms, 110ms, ...(处理时间累积)

delay until 的情况:
  Next_Release: 100ms, 200ms, 300ms, ...(绝对时刻)
  T=0ms → 处理(15ms) → delay until 100ms → T=100ms → 处理(10ms) → delay until 200ms
  实际间隔:100ms, 100ms, ...(不受处理时间影响)

这种 delay until 模式,之后所有周期性任务都会沿用。

周期超时 - deadline miss计算130ms下一次 = T+100msdelay until T+100ms 立即返回检测延迟并视为过载delay until - 以绝对时刻为基准计算15ms下一次 = T+100msdelay until T+100ms → 于100ms唤醒计算10ms下一次 = T+200ms → 于200ms唤醒实际间隔: 100ms, 100ms...delay Period - 累积漂移delay 100ms → 于115ms唤醒T=0ms: 计算15ms计算10ms → 125msdelay 100ms → 于225ms唤醒实际间隔: 115ms, 110ms...误差随时间累积防止累积漂移

6. Ravenscar 配置文件 ── 可验证的实时子集

Ada 的任务功能虽然强大,但在安全性极为重要的系统中,「太过强大」反而成为问题。动态任务创建、select 语句、abort 语句等,都会让最坏执行时间的静态分析变得困难。

Ravenscar 配置文件 正是 Ada 针对这类问题给出的答案,它将任务功能限制在可静态分析、具确定性的子集内。

-- 04_ravenscar_profile.ada
-- Ravenscar 配置文件的基本形式
-- 编译时需在 gnat.adc 中指定 pragma Profile (Ravenscar);

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) 容易产生累积漂移,因此使用绝对时刻的 delay until
每个保护对象多个入口(entry) 会增加阻塞条件与分析对象
任务终止 在 Ravenscar 中所有任务都视为不会终止
requeue 语句 使控制流追踪更复杂

通过这些限制,符合 Ravenscar 规范的程序会变成便于进行静态时序分析的形式。这正是 DO-178C(航空软件)、ISO 26262(汽车功能安全)等安全标准所要求的特性。下方列表只是主要限制的摘录,实际的配置文件还包含 No_Task_HierarchyDetect_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 提供了更精巧的机制──定时事件

-- 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+100ms, S.Fire'Access)  ── 在绝对时刻注册处理器
2. 经过 T+100ms ── 运行时以**天花板优先级**调用 S.Fire
3. Fire 将 Fired 标志设为 True ── 屏障打开
4. Reactor 任务从 Wait_For_Event 中唤醒

重点在于,这个示例明确指定了 Ceiling_Locking,且 Fire 处理器是保护对象的过程(procedure),因此以天花板优先级执行。作为定时事件处理器使用的保护过程,应放在具有中断级别天花板优先级(此处为 System.Interrupt_Priority'Last)的保护对象中。这样一来,处理定时事件时就不会发生优先级反转。

8. 用保护对象实现实时队列

实时系统中常见的模式是生产者消费者。传感器产生数据,控制任务消费数据──这时需要高效地处理缓冲区的互斥控制与阻塞。

只要使用 Ada 的保护对象入口屏障,就能实现基于屏障的同步机制。内部由运行时管理互斥,因此应用代码不需要直接编写 mutex 或条件变量。

-- 06_protected_queue.ada
-- 用保护对象实现实时数据共享
-- 管线: Producer -> Bounded_Buffer -> Consumer
-- 编译时需在 gnat.adc 中指定 pragma Locking_Policy (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 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) ── 借助天花板锁定,Producer 与 Consumer 之间不会发生优先级反转
  • 屏障条件由保护对象的内部状态(Count)定义,并在锁释放时自动重新求值。

这段代码中完全没有出现应用层的 mutex、semaphore 或条件变量。所有需要的等待,都以保护对象的入口屏障来表示。

初始状态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. 执行时间度量 ── 执行时间监控的第一步

要评估实时系统的可调度性,必须准确掌握每个任务的执行时间(CPU 时间)。Ada 的 Ada.Execution_Time 包提供了以任务为单位的 CPU 消耗时间。

-- 07_execution_time.ada
-- 执行时间控制 (Execution_Time)
-- 度量每个任务的 CPU 消耗时间

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 CPU 时间:

墙钟时间 (Wall Clock): Ada.Real_Time.Clock
  → 实际经过的时间,包含阻塞中与被抢占期间。

CPU 时间 (Execution Time): Ada.Execution_Time.Clock
  → 该任务实际在 CPU 上执行的时间。
  → 阻塞中、被抢占期间不计入。

这个区分,是执行时间监控与 WCET 验证的出发点。Busy_Workerdelay until 等待期间 CPU 时间不会增加,只有在实际计算处理中才会增加。主任务的 delay until Clock + Milliseconds(500) 期间,CPU 时间也应该几乎为零。不过,CPU 时间的实测并不能保证真正的 WCET。包含缓存、流水线、内存竞争等因素的 WCET,需要另外用静态分析或在目标环境上进行验证。

CPU时间组成: 仅实际计算CPU时间合计: 120ms墙钟时间组成: 计算 + 等待 + 阻塞 + 被抢占经过时间合计: 500ms差值 = 等待・阻塞・被抢占时间CPU时间可观察实际计算成本有助于WCET验证与监控排除等待・阻塞・被抢占时间注意实测不保证真正的WCET需要静态分析或目标环境验证

10. 综合演示 ── 多周期实时系统

整合前面学到的所有元素──优先级、Ceiling_Locking、delay until、保护对象──构建一个典型的多周期实时系统。

-- 08_multiperiodic.ada
-- 多周期实时系统集成演示
-- 快周期(100ms)的传感器读取任务
-- 慢周期(400ms)的控制任务
-- 通过 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 时刻(快速传感器为 100ms 周期,慢速控制为偏移 150ms 的 400ms 周期)为基础,并假设一组用于说明的执行时间所画出的调度示例。由于代码本身并没有 80ms 的控制运算,因此这不是实测图。当快速传感器的优先级较高时,若快速传感器的 release 出现在慢速控制执行期间,慢速控制就会暂时被中断。为了方便阅读,图中每个慢速控制周期只画出一次代表性的中断,但实际上快速传感器会在每个 100ms 边界被 release。

慢速控制 周期3(release=950ms)慢速控制 周期2(release=550ms)慢速控制 周期1(release=150ms)1000-1010ms快速传感器 #10因P+3而中断950-1000ms慢速控制 #3 前半1010-1040ms慢速控制 #3 后半600-610ms快速传感器 #6因P+3而中断550-600ms慢速控制 #2 前半610-640ms慢速控制 #2 后半150-200ms慢速控制 #1 前半100-110ms快速传感器 #1200-210ms快速传感器 #2因P+3而中断210-240ms慢速控制 #1 后半用于说明的假设快速传感器: 处理10ms慢速控制: 处理80ms

这种模式,是工业控制系统或机器人控制中常见的「快速传感器采集 + 慢速控制回路」典型结构。

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 实际对应到的行为取决于运行环境(OS + GNAT 运行时)。在 Linux 上会映射到 SCHED_FIFO,但在 Windows 上,完全的抢占式调度未必能得到保证。

2. Ravenscar 的约束:

  • 由于禁止动态任务创建,所有任务必须在系统启动时就静态声明完成。这会限制设计上的自由度。

3. WCET 度量的局限:

  • Ada.Execution_Time 提供的是度量,而不是保证。包含缓存未命中、流水线冲突等因素的真正 WCET,需要另外用静态分析工具验证。

4. 开销:

  • 保护对象的屏障求值,会在入口完成或取消时,以及离开保护对象时自动执行。若保护对象被高频调用,就需要考虑这部分的开销。

5. 工具链的门槛:

  • 要充分发挥 Ada 实时功能的价值,需要合适的交叉编译器与运行时。尤其在嵌入式目标平台上,往往要依赖厂商提供的运行时。

13. 总结

本文通过 8 个代码示例,逐步梳理了 Ada 的 Annex D 所提供的实时功能。

功能 提供的价值
任务优先级 抢占式的基于优先级调度
Ceiling_Locking 语言内置的优先级反转防止机制
delay until 防止累积漂移的周期执行
Ravenscar 配置文件 便于静态分析的任务子集
定时事件 无需轮询的时刻驱动唤醒
保护队列 保护对象的屏障式同步
执行时间度量 以任务为单位的 CPU 时间监控
多周期集成 让不同周期的任务安全共存的设计

Ada 实时功能的本质在于「不是后加的」。防止优先级反转的锁定规则、用于周期执行的时刻指定、执行时间监控等,都作为语言规范的一部分提供。当然,能否真正达成期限,仍要靠设计与分析来确认,但语言运行时已经替你准备好了所需的前提条件,这正是最大的优势所在。

Ada Annex D实时系统调度FIFO_Within_Priorities任务优先级抢占优先级反转防止Ceiling_Locking 协议自动优先级提升天花板优先级规则周期执行delay until防止累积漂移以绝对时刻为基准Ravenscar 配置文件静态任务集合确定性分析禁止相对delayDO-178C / ISO 26262定时事件无需轮询的唤醒注册保护处理器处理器以天花板优先级执行保护对象/队列入口屏障空/部分/满的状态管理屏障式同步执行时间监控各任务CPU时间墙钟时间 vs CPU时间有助于WCET验证・监控集成设计多周期设计以屏障保护语言层面的安全性

接下来,如果想实际尝试用 Ada 开发实时系统,可以到 Alire 安装 GNAT 工具链,并用 gnatchop + gnatmake 构建本文的示例代码。

另外,关于 Ada 并发处理的基础(任务、会合、保护对象),请参考上一篇文章「Ada 中的安全并发处理」。

14. 参考资料

共享相同标签的最新文章。可以围绕相近的主题进一步加深理解。

常见问题

汇总了咨询这一主题时常见的问题。

Ada 的 Annex D 是什么?
这是作为 Ada 语言规范一部分而标准化的一组实时系统功能。内容包括 FIFO_Within_Priorities 提供的基于优先级的抢占式调度、防止优先级反转的 Ceiling_Locking 协议、以 delay until 进行绝对时刻周期执行、Ravenscar 配置文件、定时事件,以及通过 Ada.Execution_Time 按任务度量执行时间等。其特点在于这些机制不是后加的库,而是直接内置于语言运行时本身。
什么是优先级反转?Ada 如何防止它?
这是指低优先级任务持有锁时被中优先级任务抢占,导致等待该锁的高优先级任务被无限期阻塞的现象。1997 年的 Mars Pathfinder 探测器就曾实际发生过这个问题,导致探测器不断重启。Ada 将 Ceiling_Locking 协议作为语言功能提供:进入保护对象的任务会自动提升到天花板优先级,因此可以防止中优先级任务造成的抢占。
Ravenscar 配置文件是什么?
这是为安全性极为重要的系统设计的配置文件,将 Ada 的任务功能限制在可静态分析、具确定性的子集中。动态任务创建、select 语句、abort 语句、相对 delay、requeue 语句等都被禁止。通过这些限制,可以更容易进行静态时序分析,也更容易满足 DO-178C(航空软件)、ISO 26262(汽车功能安全)等安全标准所要求的特性。在 GNAT 中,在 gnat.adc 文件中写入 pragma Profile (Ravenscar) 即可启用。
在周期性任务中,为什么用 delay until 而不是 delay?
因为使用相对时间的 delay 会让每次循环的处理时间累加起来,导致周期逐渐偏移,产生累积漂移。delay until 是以绝对时刻为基准决定下一次执行时刻,因此即使某一次处理延迟,之后的执行时刻仍能保持正确。不过,如果处理时间超过下一次应该唤醒的时刻,delay until 几乎会立即返回,因此需要另外设计检测 deadline miss 并视为过载的机制。

作者简介

本文作者的个人简介页面。

Go Komura

小村软件有限公司 代表

以 Windows 软件开发、技术咨询与故障排查为中心,擅长难以复现的故障调查,以及既有资产仍在运行的项目。

返回博客列表