Adaによるリアルタイムシステムプログラミング ── 優先度・周期・実行時間制御の実践

· 更新日: · · Ada, RealTime, Ravenscar, CeilingLocking, Tasking, Scheduling, PriorityInversion, ProgrammingLanguage, リアルタイム, 高信頼性

更新履歴(初版のみ・2026年06月14日公開)
初版公開
この記事を引用する(DOI: 10.5281/zenodo.21589887)

この記事はZenodoにアーカイブされています。常に最新版へ解決されるDOIと、いま表示している版に固定されたDOIの両方を下に示します。

小村 豪(2026)「Adaによるリアルタイムシステムプログラミング ── 優先度・周期・実行時間制御の実践」合同会社小村ソフト. https://doi.org/10.5281/zenodo.21589887 https://comcomponent.com/blog/ada-real-time-systems/

DOI(最新版)
10.5281/zenodo.21589887
DOI(この版)
10.5281/zenodo.21589888

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 つの実践的なコード例で段階的に解説します。各スニペットは独立した例として扱えますが、複数のコンパイル単位を含む 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起床によりスケジューラがLを中断Lはロック解放できないMが実行を継続(HもLも動けない)【優先度逆転】高優先度が無期限ブロックロックを獲得ロック獲得を試みる

低優先度タスクがロックを持ったまま中優先度タスクにプリエンプトされ、高優先度タスクが無期限にブロックされます。Mars Pathfinder で実際に行われた対策は VxWorks の priority inheritance の有効化でしたが、Ada は同種の問題に対し、別方式である Ceiling_Locking を言語機能として提供します。

3. タスク優先度の基礎 ── FIFO_Within_Priorities

FIFO_Within_Priorities は、Ada Annex D で指定できる標準的な優先度ベースのディスパッチポリシーです。ポリシーを明示しない場合の既定動作は実装定義ですが、GNAT では多くのターゲットでこの系統のポリシーが使われます。同一優先度内では 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 が最低です。
  • このデモのタスク本体は重い計算ではなく、delay until で指定時刻まで待機します。ここで確認したいのは、同時に実行可能になったときに高優先度タスクが先に実行機会を得ることです。
  • この図は FIFO_Within_Priorities のうち、異なる優先度間のプリエンプション側を示しています。同一優先度内の FIFO 順序を確認するには、同じ優先度の複数タスクを並べる別例が必要です。
  • 実際のシステムでは System.Default_Priority を基準に相対的な優先度を設計するのが一般的です。
低優先度タスク(Priority=First)高優先度タスク(Priority=Last)メインタスクスケジューラ低優先度タスク(Priority=First)高優先度タスク(Priority=Last)メインタスクスケジューラT=0ms: 両タスクは runnable開始ログを出力開始ログを出力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 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)と等しいため入場可能実行優先度が30に昇格Mが起床Lは天井優先度30で実行中M(20)はプリエンプトできないHが起床H(30)は天井チェックに合格ただしLがPO使用中なので待機優先度が10に復帰PO解放後にHを実行H(30) = 天井(30)なので競合解消後に入場できる保護操作に入る操作を実行保護操作から出る保護操作に入る保護操作から出る

設計指針: 保護オブジェクトのシーリング優先度は、その保護オブジェクトを使用するすべてのタスクの最高優先度以上に設定する。これを破り、天井優先度より高いアクティブ優先度のタスクが保護操作を呼び出すと、Ada では Program_Error によって設計ミスを検出できる。

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 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 を使う
保護オブジェクトあたり複数エントリ ブロッキング条件と解析対象が増える
タスクの終了 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 ハンドラが保護オブジェクトのプロシージャであるため、シーリング優先度で実行されることです。タイミングイベントのハンドラとして使う保護プロシージャは、割り込みレベルの天井優先度、ここでは System.Interrupt_Priority'Last を持つ保護オブジェクトに置きます。これにより、タイミングイベントの処理中に優先度逆転が発生しません。

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)で定義されており、ロック解放時に自動的に再評価される。

このコードにはアプリケーション側のミューテックス、セマフォ、条件変数は登場しません。必要な待ち合わせは保護オブジェクトのエントリバリアで表現します。

初期状態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 が来ると、低速制御は一時中断されます。図では見やすさのため各低速制御周期につき代表的な中断を1回だけ描いていますが、実際には 100ms 境界ごとに高速センサーが release されます。

低速制御 周期3(release=950ms)低速制御 周期2(release=550ms)低速制御 周期1(release=150ms)1000-1010ms高速センサー #10P+3なので中断950-1000ms低速制御 #3 前半1010-1040ms低速制御 #3 後半600-610ms高速センサー #6P+3なので中断550-600ms低速制御 #2 前半610-640ms低速制御 #2 後半150-200ms低速制御 #1 前半100-110ms高速センサー #1200-210ms高速センサー #2P+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 主流領域での限定的・選択的適用ペースメーカー輸液ポンプロボット制御NC工作機械ミッション計算機長期運用システム

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 のリアルタイム機能の本質は「後付けではない」ことです。優先度逆転を抑えるロック規則、周期実行のための時刻指定、実行時間モニタリングなどが言語仕様の一部として提供されています。もちろんデッドライン達成そのものは設計と解析で確認しますが、そのための前提を言語ランタイムが揃えてくれる点が大きな強みです。

Ada Annex Dリアルタイムシステムスケジューリング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. 参考

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

Windows I/Oの深層(第6回・最終回) ── フィルタードライバーとミニフィルター:ProcmonとウイルススキャンがI/Oに割り込める理由

Windowsのフィルタードライバーとミニフィルターを図解で解説する連載の最終回です。フィルターマネージャーとアルティチュード、pre/postコールバック、Procmonやウイルス対策が全I/Oを検査できる仕組み、「あの環境だけ遅い」の調査手順まで整理します。

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

よくある質問

この記事のテーマについて、相談時によくある質問をまとめています。

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ではなくdelay untilを使う理由は?
相対時間指定のdelayでは各反復の処理時間が加算され、周期が徐々にずれる累積ドリフトが発生するためです。delay untilは絶対時刻を基準に次の起動時刻を決めるため、1回の処理が遅れても次回以降の起動時刻は正しく保たれます。ただし処理が次回起床時刻を超えた場合、delay untilはほぼ即座に戻るため、deadline missとして検出し過負荷として扱う設計が別途必要です。

著者プロフィール

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

小村 豪

合同会社小村ソフト 代表

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

ブログ一覧に戻る