Adaによるリアルタイムシステムプログラミング ── 優先度・周期・実行時間制御の実践
· 小村 豪 · Ada, RealTime, Ravenscar, CeilingLocking, Tasking, Scheduling, PriorityInversion, ProgrammingLanguage, リアルタイム, 高信頼性
1. はじめに ── Ada とリアルタイムの深い関係
前回の記事「Adaにおける安全な並行処理」では、Ada のタスクと保護オブジェクトによる安全な並行処理の基礎を解説しました。今回はその延長線上にある、より制約の厳しい領域——リアルタイムシステム——に踏み込みます。
リアルタイムシステムにおいて「正しさ」とは、論理的な計算結果の正しさだけでなく、その結果が期限内に得られることも含みます。1ミリ秒遅れた正しい答えは、誤った答えと同じくらい危険です。
Ada はこの要件に対し、言語仕様の Annex D(Real-Time Systems) として標準化された包括的なリアルタイム機能群を提供しています。これは「ライブラリで後付け」ではなく、言語ランタイムそのものに組み込まれたリアルタイム保証です。
Ada のリアルタイム機能(Annex D):
- タスク優先度とプリエンプション(FIFO_Within_Priorities)
- Ceiling_Locking プロトコル(優先度逆転の防止)
- delay until による絶対時刻周期実行
- Ravenscar プロファイル(安全重要なサブセット)
- タイミングイベント(ポーリング不要の時刻起床)
- 実行時間モニタリング(Ada.Execution_Time)
- マルチ周期スケジューリング
この記事では、これらを 8 つの実践的なコード例で段階的に解説します。各スニペットは独立してコンパイル・実行可能です。
なお、この記事に登場するコード断片は、章ごとにファイルへ整理した参照用コード集として GitHub で公開しています。
ada-real-time-systems - komurasoft-blog-samples (GitHub)
2. リアルタイムシステムとは何か
まず、用語の整理から始めましょう。
| 概念 | 説明 |
|---|---|
| ハードリアルタイム | デッドライン超過がシステムの致命的な失敗を意味する(飛行制御、エアバッグ、ペースメーカー) |
| ソフトリアルタイム | デッドライン超過が望ましくないが、まれな超過は許容される(動画ストリーミング、ゲーム) |
| デッドライン (deadline) | タスクが完了しなければならない絶対時刻 |
| 周期 (period) | タスクが繰り返し起動される時間間隔 |
| WCET (Worst-Case Execution Time) | タスクの最悪実行時間 |
| ジッタ (jitter) | 周期実行のばらつき |
リアルタイムシステムの設計では、すべてのタスクについて「WCET < デッドライン」が成立しなければなりません。Ada のリアルタイム機能は、この分析と保証を言語レベルで支援します。
3. タスク優先度の基礎 ── FIFO_Within_Priorities
Ada のデフォルトのタスクスケジューリングポリシーは FIFO_Within_Priorities です。同一優先度内では FIFO(先入れ先出し)で実行され、より高い優先度のタスクは低い優先度のタスクをプリエンプト(横取り)します。
-- 01_task_priority.ada
-- タスク優先度と FIFO_Within_Priorities の基本形
-- 構成プラグマはコンテキスト条項より前に置く
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が最低です。- 高優先度タスクは 100ms の処理を即座に開始・完了し、低優先度タスク(500ms)の実行中でもプリエンプトできます。
- 実際のシステムでは
System.Default_Priorityを基準に相対的な優先度を設計するのが一般的です。
Ada の優先度の範囲(GNAT のデフォルト):
Priority'First = 0 (最低)
Priority'Last = 30 (最高、ただし OS に依存)
4. Ceiling_Locking ── 優先度逆転を言語が防ぐ
リアルタイムシステムで最も厄介な問題の一つが優先度逆転 (priority inversion) です。高優先度タスクが低優先度タスクの保持するロックを待ち、その低優先度タスクが中優先度タスクにプリエンプトされることで、高優先度タスクが無期限にブロックされる現象です。
Ada はこの問題に対し、Ceiling_Locking プロトコルを保護オブジェクトに直接組み込んでいます。
-- 02_ceiling_locking.ada
-- 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 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)でシーリング優先度を設定する。 - どのタスクが保護オブジェクトに入っても、進入時に自動的にシーリング優先度へ昇格する。
- これにより、保護オブジェクトを使用中のタスクを、中優先度のタスクがプリエンプトできない。
- 保護オブジェクトから出ると元の優先度に戻る。
設計指針: 保護オブジェクトのシーリング優先度は、その保護オブジェクトを使用するすべてのタスクの最高優先度以上に設定する。これが Ceiling_Locking の鉄則である。
C 言語の pthread ミューテックスで同等のことを実現するには 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; |
絶対時刻基準なので、たとえ1回の処理が遅れても次の起動時刻は正しい |
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 のパターンは、この先のすべての周期タスクで使用します。
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: done");
end Ravenscar_Demo;
Ravenscar プロファイルの制限:
| 禁止される機能 | 理由 |
|---|---|
動的タスク生成 (new やアクセスタイプ) |
実行時のメモリ割り当てが非決定的 |
select 文の複数オープン代替 |
実行時間の静的解析が困難 |
abort 文 |
非同期中断が状態の予測不能性を生む |
Ada.Task_Attributes |
実行時動的振る舞い |
| タスク終了の動的検出 | 決定論的な終了解析を困難にする |
requeue 文 |
制御フロー追跡が複雑化 |
この制限により、Ravenscar に準拠したプログラムは静的タイミング解析が可能になります。これは DO-178C(航空機ソフトウェア)や ISO 26262(自動車機能安全)などの安全規格で求められる特性です。
Ravenscar プロファイルを有効にするには、gnat.adc ファイルに以下を記述します:
pragma Profile (Ravenscar);
7. タイミングイベント ── ポーリングなしの時刻駆動起床
多くのリアルタイムシステムでは、「指定時刻になったら高優先度タスクを起床させる」という要件が頻出します。素朴な実装ではタイマーをポーリングすることになりますが、Ada はより洗練された仕組み——タイミングイベント——を提供します。
-- 05_timing_events.ada
-- タイミングイベント (Ada.Real_Time.Timing_Events)
-- 高優先度タスクをポーリングなしで起床させる仕組み
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 から起床
重要なのは、Fire ハンドラが保護オブジェクトのプロシージャであるため、シーリング優先度で実行されることです。これにより、タイミングイベントの処理中に優先度逆転が発生しません。
8. 保護オブジェクトによるリアルタイムキュー
リアルタイムシステムでよく現れるパターンとして、プロデューサー・コンシューマーがあります。センサーがデータを生成し、制御タスクがそれを消費する——このとき、バッファの排他制御とブロッキングを効率的に行う必要があります。
Ada の保護オブジェクトとエントリバリアを使えば、これをロックフリーで実装できます。
-- 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)で定義されており、ロック解放時に自動的に再評価される。
このコードにはミューテックスもセマフォも条件変数も登場しません。必要な同期はすべて保護オブジェクトのバリアが処理します。
9. 実行時間の計測 ── WCET 分析の第一歩
リアルタイムシステムのスケジューリング可能性を評価するには、各タスクの実行時間(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;
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 時間はほとんどゼロのはずです。
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;
システム構成:
優先度順(高→低):
System.Any_Priority'Last ── Shared_Sensor (Ceiling_Locking)
Default_Priority + 3 ── Fast_Sensor (周期 100ms, 12回)
Default_Priority + 2 ── Slow_Controller (周期 400ms, 3回)
動作:
Fast_Sensor が 4 回書き込むごとに Slow_Controller が 1 回読み取る。
Fast_Sensor の優先度が高いので、書き込みは常に遅延なく完了する。
シーリングロッキングにより、どちらが保護オブジェクトを使用中でも
プリエンプトは発生しない。
このパターンは、産業用制御システムやロボット制御でよく見られる「高速センサー収集+低速制御ループ」の典型的な構造です。
11. Ada リアルタイム機能が輝く場面
Ada のリアルタイム機能は、以下のような分野で特にその価値を発揮します。
航空宇宙:
- 飛行制御システム(Airbus A380、Boeing 787)
- 衛星姿勢制御(多数の ESA 衛星)
- ドローン飛行制御
鉄道:
- 信号システム
- 自動列車制御 (ATC)
- 踏切制御
自動車:
- エンジン制御ユニット (ECU)
- ブレーキ・バイ・ワイヤ
- 先進運転支援システム (ADAS)
医療機器:
- ペースメーカー
- 輸液ポンプ
- 放射線治療装置
これらの分野では、DO-178C(航空機ソフトウェア)、ISO 26262(自動車機能安全)、IEC 62304(医療機器ソフトウェア)といった安全規格への適合が必須であり、Ravenscar プロファイルはそのための重要な構成要素です。
12. 注意点と限界
Ada のリアルタイム機能は強力ですが、万能ではありません。
1. プラットフォーム依存:
pragma Priorityの実際のマッピングは実行環境(OS + GNAT ランタイム)に依存します。Linux 上ではSCHED_FIFOにマップされますが、Windows 上では完全なプリエンプションが保証されない場合があります。
2. Ravenscar の制約:
- 動的タスク生成が禁止されているため、システム起動時にすべてのタスクを静的に宣言する必要があります。これは設計の自由度を制限します。
3. WCET の測定限界:
Ada.Execution_Timeは測定であって保証ではありません。キャッシュミスやパイプラインハザードを含む真の WCET は、静的解析ツールで別途検証する必要があります。
4. オーバーヘッド:
- 保護オブジェクトのバリア評価は、エントリの完了・キャンセル時、および保護オブジェクトからの退出時に自動実行されます。高頻度で呼び出される保護オブジェクトでは、このオーバーヘッドを考慮する必要があります。
5. ツールチェーンの壁:
- Ada のリアルタイム機能をフル活用するには、適切なクロスコンパイラとランタイムが必要です。特に組み込みターゲットでは、ベンダー提供のランタイムに依存することになります。
13. まとめ
この記事では、Ada の Annex D が提供するリアルタイム機能を 8 つのコード例で段階的に見てきました。
| 機能 | 提供する価値 |
|---|---|
| タスク優先度 | プリエンプティブな優先度ベーススケジューリング |
| Ceiling_Locking | 言語組み込みの優先度逆転防止 |
delay until |
ドリフトのない正確な周期実行 |
| Ravenscar プロファイル | 静的解析可能・認証可能なタスクサブセット |
| タイミングイベント | ポーリング不要の時刻駆動起床 |
| 保護キュー | ロックフリーのプロデューサー・コンシューマー |
| 実行時間計測 | タスク単位の CPU 時間モニタリング |
| マルチ周期統合 | 異なる周期のタスクを安全に共存させる設計 |
Ada のリアルタイム機能の本質は「後付けではない」ことです。優先度逆転の防止も、周期実行の正確さも、WCET モニタリングも、すべて言語仕様の一部として提供されています。これは「リアルタイム性を言語ランタイムに委ねられる」という設計上の大きな強みです。
次のステップとして、Ada によるリアルタイムシステム開発を実際に試すには、Alire で GNAT ツールチェーンをインストールし、この記事のサンプルコードを gnatchop + gnatmake でビルドしてみてください。
なお、Ada の並行処理の基本(タスク、ランデブー、保護オブジェクト)については、前回の記事「Adaにおける安全な並行処理」を参照してください。
14. 参考
関連する記事
同じタグを共有する最新の記事です。さらに近い話題で知識を深められます。
Adaにおける安全な並行処理 ── タスクと保護オブジェクトの実践ガイド
Adaの言語組み込み並行処理であるタスクと保護オブジェクトの実践的な入門記事です。ランデブー(entry/accept)、選択的アクセプト、プロデューサー・コンシューマー、保護オブジェクトによる排他制御、バリア付き保護エントリ、タイムアウト付き呼び出し、タスク優先度とリアル...
Ada言語の魅力 ── 型で設計を語り、数十年動き続けるソフトウェアを支える言語
Ada言語の魅力を紹介します。強い型付け、範囲制約、パッケージによる仕様と実装の分離、契約による設計、言語組み込みのタスク、SPARKによる形式検証、GNATとAlireでの開発環境まで、高信頼ソフトウェアを支える設計思想を整理します。
Fableが使えなくなったけど諦めるな ── OpenRouter Fusion+中華LLM+レビュー層で凌ぐ
Fableには遠く及びません。ですがOpenRouter Fusionで5つの中華LLMを組み合わせ、gpt-5.5-pro または Codex PRレビューを重ねれば、素のgpt-5.5よりはずっとマシなコーディングエージェントになります。
WindowsのMFCとは何か ── 既存資産を保守するための基礎知識
Microsoft Foundation Classes(MFC)の概要、Win32との関係、アプリケーション構造、メッセージマップ、Document/View、DDX/DDV、保守時の注意点を整理します。
Windows PCを廃棄する前にやっておきたいこと ── データ消去・アカウント解除・バックアップの実務チェックリスト
Windows PCを廃棄・譲渡・売却・リース返却する前にやっておきたいことを、バックアップ、データ消去、BitLocker、Microsoftアカウント、OneDrive、仕事用アカウント、開発者PC特有の秘密情報、廃棄証跡の観点から整理します。
関連トピック
このテーマと近いトピックページです。記事を起点に、関連するサービスや他の記事へ進めます。
Windows技術トピック
Windows 開発、不具合調査、既存資産活用の技術トピックをまとめた入口です。
著者プロフィール
記事の著者プロフィールページです。
小村 豪
合同会社小村ソフト 代表
Windows ソフト開発、技術相談、不具合調査を中心に、既存資産が残る案件や原因が見えにくい障害調査に強みがあります。
公開リンク