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 の仕組み:

  1. 保護オブジェクトに pragma Priority (Ceiling)シーリング優先度を設定する。
  2. どのタスクが保護オブジェクトに入っても、進入時に自動的にシーリング優先度へ昇格する。
  3. これにより、保護オブジェクトを使用中のタスクを、中優先度のタスクがプリエンプトできない。
  4. 保護オブジェクトから出ると元の優先度に戻る。

設計指針: 保護オブジェクトのシーリング優先度は、その保護オブジェクトを使用するすべてのタスクの最高優先度以上に設定する。これが 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_Workerdelay 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. 参考

同じタグを共有する最新の記事です。さらに近い話題で知識を深められます。

このテーマと近いトピックページです。記事を起点に、関連するサービスや他の記事へ進めます。

著者プロフィール

記事の著者プロフィールページです。

小村 豪

合同会社小村ソフト 代表

Windows ソフト開発、技術相談、不具合調査を中心に、既存資産が残る案件や原因が見えにくい障害調査に強みがあります。

ブログ一覧に戻る