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 のリアルタイム機能は、その分析を行いやすい予測可能な実行モデルを言語レベルで提供します。
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(先入れ先出し)で実行され、より高い優先度のタスクは低い優先度のタスクをプリエンプト(横取り)します。
-- 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を基準に相対的な優先度を設計するのが一般的です。
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: 両タスクは runnable
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 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 ミューテックスで同等のことを実現するには 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 のパターンは、この先のすべての周期タスクで使用します。
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 を使う |
| 保護オブジェクトあたり複数エントリ | ブロッキング条件と解析対象が増える |
| タスクの終了 | 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 ハンドラが保護オブジェクトのプロシージャであるため、シーリング優先度で実行されることです。タイミングイベントのハンドラとして使う保護プロシージャは、割り込みレベルの天井優先度、ここでは 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)で定義されており、ロック解放時に自動的に再評価される。
このコードにはアプリケーション側のミューテックス、セマフォ、条件変数は登場しません。必要な待ち合わせは保護オブジェクトのエントリバリアで表現します。
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 が来ると、低速制御は一時中断されます。図では見やすさのため各低速制御周期につき代表的な中断を1回だけ描いていますが、実際には 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[NC工作機械]
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. まとめ
この記事では、Ada の Annex D が提供するリアルタイム機能を 8 つのコード例で段階的に見てきました。
| 機能 | 提供する価値 |
|---|---|
| タスク優先度 | プリエンプティブな優先度ベーススケジューリング |
| 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の言語組み込み並行処理であるタスクと保護オブジェクトの入門記事です。ランデブー(entry/accept)、選択的アクセプト、保護オブジェクトによる排他制御、タイムアウト付き呼び出し、タスク優先度まで整理します。
Adaのジェネリックプログラミング ── 型で契約を書き、再利用をゼロコストで実現する
Adaのジェネリックプログラミングを、総称サブプログラム、総称パッケージ、仮サブプログラム、型カテゴリ、実務上の設計指針まで体系的に解説します。型安全な再利用とゼロコスト抽象化の考え方を整理します。
SPARKによる形式検証入門 ── Adaの契約から数学的証明へ
Adaのサブセット言語SPARKによる形式検証の実践的な入門記事です。契約(Pre/Post)から証明へステップアップする方法、GNATproveの使い方、ループ不変条件、データフロー契約、証明レベル、そして実プロジェクトへの適用方法までを整理します。
Ada言語の魅力 ── 型で設計を語り、数十年動き続けるソフトウェアを支える言語
Ada言語の魅力を紹介します。強い型付け、範囲制約、パッケージによる仕様と実装の分離、契約による設計、言語組み込みのタスク、SPARKによる形式検証、GNATとAlireでの開発環境まで、高信頼ソフトウェアを支える設計思想を整理します。
Windows I/Oの深層(第6回・最終回) ── フィルタードライバーとミニフィルター:ProcmonとウイルススキャンがI/Oに割り込める理由
Windowsのフィルタードライバーとミニフィルターを図解で解説する連載の最終回です。フィルターマネージャーとアルティチュード、pre/postコールバック、Procmonやウイルス対策が全I/Oを検査できる仕組み、「あの環境だけ遅い」の調査手順まで整理します。
関連トピック
このテーマと近いトピックページです。記事を起点に、関連するサービスや他の記事へ進めます。
Windows技術トピック
Windows 開発、不具合調査、既存資産活用の技術トピックをまとめた入口です。
よくある質問
この記事のテーマについて、相談時によくある質問をまとめています。
- 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 ソフト開発、技術相談、不具合調査を中心に、既存資産が残る案件や原因が見えにくい障害調査に強みがあります。
公開リンク