用 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 的实时功能,正是在语言层面提供了一套便于进行这类分析的可预测执行模型。
flowchart LR
HRT[硬实时] -->|超过期限 = 致命失败| Examples[飞行控制<br/>安全气囊<br/>心脏起搏器]
SRT[软实时] -->|偶尔超过可接受| Examples2[视频传输<br/>游戏<br/>UI]
Ada[Ada Annex D<br/>支撑可预测性的机制] --> Mechanism[FIFO_Within_Priorities<br/>Ceiling_Locking<br/>delay until]
subgraph Requirements[实时性要求]
D[期限<br/>应完成的绝对时刻]
P[周期<br/>重复间隔]
W[WCET<br/>最坏执行时间]
J[抖动<br/>周期的波动]
end
D --> Analysis[可调度性分析]
P --> Analysis
W --> Analysis
J --> Analysis
HRT --> Analysis
SRT --> Analysis
Mechanism --> Analysis
Analysis --> Constraint[必要条件: WCET <= 期限<br/>充分性需以响应时间分析确认]
实时系统中最危险的现象之一是优先级反转。这个问题曾在 1997 年的 Mars Pathfinder 上实际发生,导致探测器不断重启。
sequenceDiagram
participant S as 调度器
participant L as 低优先级任务
participant H as 高优先级任务
participant M as 中优先级任务
participant R as 共享资源
L->>R: 获取锁
activate L
Note over L: 关键区段执行中
Note over S,L: H 唤醒,调度器中断 L
deactivate L
activate H
H->>R: 尝试获取锁
Note over H: 因等待锁而阻塞!(L 持有中)
deactivate H
Note over S,L: H 在等待锁,因此恢复执行 L
activate L
Note over L: 继续朝释放锁推进...
Note over S,L: M 唤醒,调度器中断 L
deactivate L
activate M
Note over L: L 无法释放锁
Note over M: M 持续执行(H 和 L 都动不了)
Note over H: 【优先级反转】高优先级被无限期阻塞
deactivate M
低优先级任务持有锁时被中优先级任务抢占,导致高优先级任务被无限期阻塞。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为基准来设计相对优先级。
sequenceDiagram
participant S as 调度器
participant Main as 主任务
participant HP as 高优先级任务<br/>(Priority=Last)
participant LP as 低优先级任务<br/>(Priority=First)
Main->>HP: 创建任务
Main->>LP: 创建任务
Note over HP,LP: T=0ms: 两个任务都可执行
S->>HP: 选择优先级最高的 HP
activate HP
Note over HP: 输出开始日志
HP->>S: 因 delay until T+100ms 而阻塞
deactivate HP
S->>LP: 接着执行 LP
activate LP
Note over LP: 输出开始日志
LP->>S: 因 delay until T+500ms 而阻塞
deactivate LP
Note over S: T=100ms: HP 唤醒
S->>HP: 执行 HP
activate HP
Note over HP: 输出完成日志
deactivate HP
Note over S: T=500ms: LP 唤醒
S->>LP: 执行 LP
activate LP
Note over LP: 输出完成日志
deactivate LP
Note over Main: (T=800ms) 主任务结束
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 的工作原理:
- 在保护对象上以
pragma Priority (Ceiling)设定天花板优先级。 - 无论哪个任务进入保护对象,进入时都会自动提升到天花板优先级。
- 因此,使用中的保护对象不会被中优先级任务抢占。
- 离开保护对象后恢复原来的优先级。
下图不是前一段示例代码的精确时间追踪,而是概念图,说明图 2 的优先级反转模式在 Ceiling_Locking 下如何被抑制。
sequenceDiagram
participant S as 调度器
participant L as 低优先级任务<br/>(优先级=10)
participant M as 中优先级任务<br/>(优先级=20)
participant H as 高优先级任务<br/>(优先级=30)
participant PO as 保护对象<br/>(天花板优先级=30)
Note over PO: 调用方的活动优先级若高于天花板优先级则会 Program_Error<br/>图中 H(30) 等于天花板(30),因此可以进入
L->>PO: 进入保护操作
activate L
Note over L,PO: 执行优先级提升为 30
Note over S: M 唤醒
Note over S,L: L 以天花板优先级 30 执行中<br/>M(20) 无法抢占
Note over S: H 唤醒
Note over S,H: H(30) 通过天花板检查<br/>但因 L 正在使用 PO 而等待
L->>PO: 执行操作
L->>PO: 离开保护操作
deactivate L
Note over L: 优先级恢复为 10
Note over S,H: PO 释放后执行 H
activate H
H->>PO: 进入保护操作
Note over H,PO: H(30) = 天花板(30),竞争解除后可进入
H->>PO: 离开保护操作
deactivate H
设计原则: 保护对象的天花板优先级,应设定为使用该保护对象的所有任务中最高优先级以上。若违反此原则,让活动优先级高于天花板优先级的任务调用保护操作,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 模式,之后所有周期性任务都会沿用。
flowchart TB
subgraph Bad["delay Period - 累积漂移"]
B1[T=0ms: 计算15ms] --> B2[delay 100ms → 于115ms唤醒]
B2 --> B3[计算10ms → 125ms]
B3 --> B4[delay 100ms → 于225ms唤醒]
B4 --> B5[实际间隔: 115ms, 110ms...]
end
subgraph Good["delay until - 以绝对时刻为基准"]
G1[下一次 = T+100ms] --> G2[计算15ms]
G2 --> G3[delay until T+100ms → 于100ms唤醒]
G3 --> G4[计算10ms]
G4 --> G5[下一次 = T+200ms → 于200ms唤醒]
G5 --> G6[实际间隔: 100ms, 100ms...]
end
subgraph Overrun["周期超时 - deadline miss"]
O1[下一次 = T+100ms] --> O2[计算130ms]
O2 --> O3[delay until T+100ms 立即返回]
O3 --> O4[检测延迟并视为过载]
end
Bad --> Drift[误差随时间累积]
Good --> Stable[防止累积漂移]
Good --> Overrun
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_Hierarchy、Detect_Blocking 等与运行时及可分析性相关的额外规则。
flowchart TB
Full[完整的 Ada 任务功能] --> Profile[Ravenscar 配置文件]
Profile --> Restrict[限制]
Profile --> Policy[必需策略]
Restrict --> R1[禁止动态任务创建]
Restrict --> R2[禁止 select 语句]
Restrict --> R3[禁止 abort 语句]
Restrict --> R4[禁止 Task_Attributes]
Restrict --> R5[限制每个保护对象仅 1 个入口]
Restrict --> R6[禁止 requeue 语句]
Restrict --> R7[禁止相对delay<br/>使用delay until]
Restrict --> R8[禁止动态修改优先级]
Restrict --> R9[禁止任务终止<br/>所有任务不终止]
Policy --> P1[FIFO_Within_Priorities]
Policy --> P2[Ceiling_Locking]
Restrict --> Benefit[变得容易进行:<br/>静态时序分析]
Policy --> Benefit
Benefit --> DO178[DO-178C<br/>航空软件]
Benefit --> ISO26262[ISO 26262<br/>汽车功能安全]
Benefit --> IEC62304[IEC 62304<br/>医疗器械软件]
要启用 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 或条件变量。所有需要的等待,都以保护对象的入口屏障来表示。
stateDiagram-v2
Empty: 空 / Count=0
Partial: 部分 / Count=1..Buffer_Size-1
Full: 满 / Count=Buffer_Size
[*] --> Empty: 初始状态
Empty --> Partial: Put (新增1个元素)
Partial --> Partial: Put / Get
Partial --> Empty: Get (取出最后一个元素)
Partial --> Full: Put (填满最后的空位)
Full --> Partial: Get (产生空位)
Empty --> Empty: Get 会阻塞 (屏障 Count=0)
Full --> Full: Put 会阻塞 (屏障 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_Worker 在 delay until 等待期间 CPU 时间不会增加,只有在实际计算处理中才会增加。主任务的 delay until Clock + Milliseconds(500) 期间,CPU 时间也应该几乎为零。不过,CPU 时间的实测并不能保证真正的 WCET。包含缓存、流水线、内存竞争等因素的 WCET,需要另外用静态分析或在目标环境上进行验证。
flowchart LR
subgraph Wall[墙钟时间]
W1[经过时间合计: 500ms] --> W2[组成: 计算 + 等待 + 阻塞 + 被抢占]
end
subgraph CPU[CPU时间]
C1[CPU时间合计: 120ms] --> C2[组成: 仅实际计算]
end
Wall --> Diff[差值 = 等待・阻塞・被抢占时间]
CPU --> Diff
Diff --> Insight[CPU时间可观察实际计算成本<br/>有助于WCET验证与监控<br/>排除等待・阻塞・被抢占时间]
Insight --> Caveat[注意<br/>实测不保证真正的WCET<br/>需要静态分析或目标环境验证]
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。
flowchart TB
Assumption["用于说明的假设<br/>快速传感器: 处理10ms<br/>慢速控制: 处理80ms"]
subgraph Cycle1["慢速控制 周期1(release=150ms)"]
direction LR
C1F1["100-110ms<br/>快速传感器 #1"] --> C1S1["150-200ms<br/>慢速控制 #1 前半"]
C1S1 --> C1F2["200-210ms<br/>快速传感器 #2<br/>因P+3而中断"]
C1F2 --> C1S2["210-240ms<br/>慢速控制 #1 后半"]
end
subgraph Cycle2["慢速控制 周期2(release=550ms)"]
direction LR
C2S1["550-600ms<br/>慢速控制 #2 前半"] --> C2F6["600-610ms<br/>快速传感器 #6<br/>因P+3而中断"]
C2F6 --> C2S2["610-640ms<br/>慢速控制 #2 后半"]
end
subgraph Cycle3["慢速控制 周期3(release=950ms)"]
direction LR
C3S1["950-1000ms<br/>慢速控制 #3 前半"] --> C3F10["1000-1010ms<br/>快速传感器 #10<br/>因P+3而中断"]
C3F10 --> C3S2["1010-1040ms<br/>慢速控制 #3 后半"]
end
Assumption --> C1F1
C1S2 --> C2S1
C2S2 --> C3S1
这种模式,是工业控制系统或机器人控制中常见的「快速传感器采集 + 慢速控制回路」典型结构。
11. Ada 实时功能大放异彩的场景
Ada 的实时功能特别能在以下领域发挥价值。
flowchart TB
Ada[Ada Annex D<br/>实时功能] --> Aero[航空航天<br/>DO-178C]
Ada --> Rail[铁路<br/>EN 50128系列]
Ada --> Auto[汽车<br/>ISO 26262]
Ada --> Medical[医疗器械<br/>IEC 62304]
Ada --> Industrial[工业控制<br/>IEC 61508系列]
Ada --> Defense[国防・高可靠性系统]
Aero --> A1[飞行控制<br/>应用经验丰富的领域]
Aero --> A2[卫星・航天器控制]
Rail --> R1[信号系统]
Rail --> R2[自动列车控制]
Auto --> Au1[安全相关ECU的候选方案]
Auto --> Au2[在以C / MISRA-C为主流的领域中<br/>有限且有选择的应用]
Medical --> M1[心脏起搏器]
Medical --> M2[输液泵]
Industrial --> I1[机器人控制]
Industrial --> I2[数控机床]
Defense --> D1[任务计算机]
Defense --> D2[长期运行系统]
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 实时功能的本质在于「不是后加的」。防止优先级反转的锁定规则、用于周期执行的时刻指定、执行时间监控等,都作为语言规范的一部分提供。当然,能否真正达成期限,仍要靠设计与分析来确认,但语言运行时已经替你准备好了所需的前提条件,这正是最大的优势所在。
mindmap
root((Ada Annex D<br/>实时系统))
调度
FIFO_Within_Priorities
任务优先级
抢占
优先级反转防止
Ceiling_Locking 协议
自动优先级提升
天花板优先级规则
周期执行
delay until
防止累积漂移
以绝对时刻为基准
Ravenscar 配置文件
静态任务集合
确定性分析
禁止相对delay
DO-178C / ISO 26262
定时事件
无需轮询的唤醒
注册保护处理器
处理器以天花板优先级执行
保护对象/队列
入口屏障
空/部分/满的状态管理
屏障式同步
执行时间监控
各任务CPU时间
墙钟时间 vs CPU时间
有助于WCET验证・监控
集成设计
多周期设计
以屏障保护
语言层面的安全性
接下来,如果想实际尝试用 Ada 开发实时系统,可以到 Alire 安装 GNAT 工具链,并用 gnatchop + gnatmake 构建本文的示例代码。
另外,关于 Ada 并发处理的基础(任务、会合、保护对象),请参考上一篇文章「Ada 中的安全并发处理」。
14. 参考资料
相关文章
共享相同标签的最新文章。可以围绕相近的主题进一步加深理解。
Ada 中的安全并发处理 ── 任务与保护对象实战指南
本文是关于 Ada 语言内置并发处理机制——任务(task)与保护对象(protected object)的实战入门文章。内容涵盖会合(entry/accept)、选择性接受(select)、生产者消费者模式、保护对象实现的互斥控制、带屏障的保护入口、带超时的调用,以及任务...
Ada 的泛型编程 —— 用类型编写契约,零成本实现代码复用
系统讲解 Ada 的泛型编程,涵盖泛型子程序、泛型包、形式子程序参数、类型类别、正式包参数,以及实务中的设计方针。结合 Mermaid 图与实例,整理类型安全的复用与零成本抽象的理念。
Ada 语言的魅力 —— 用类型阐述设计,支撑运行数十年软件的语言
介绍 Ada 语言的魅力。从强类型、范围约束、用包分离规格与实现、契约式设计、语言内置的任务(task)、SPARK 形式化验证,到 GNAT 与 Alire 开发环境,整理支撑高可靠软件的设计理念。
SPARK形式化验证入门 ── 从Ada的契约走向数学证明
本文是使用Ada子集语言SPARK进行形式化验证的实务入门文章,整理从契约(Pre/Post)进阶到证明的方法、GNATprove的使用方式、循环不变式、数据流契约、证明等级,以及在实际项目中导入的方法。
用 PerfView 与 dotnet-trace 定位“变慢”的原因 ── .NET 性能排查实务入门
当业务应用出现“变慢”“CPU 占满”“偶尔卡死”时,该用哪个工具看什么?本文梳理 PerfView 与 dotnet-trace 的分工、CPU 采样的解读方法(inclusive/exclusive)、用 ThreadTime 排查阻塞时间,以及与 EventSourc...
常见问题
汇总了咨询这一主题时常见的问题。
- 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 软件开发、技术咨询与故障排查为中心,擅长难以复现的故障调查,以及既有资产仍在运行的项目。