Ada를 활용한 실시간 시스템 프로그래밍 ── 우선순위·주기·실행 시간 제어의 실전

· · Ada, 실시간, Ravenscar, 천장 잠금, 태스킹, 스케줄링, 우선순위 역전, 프로그래밍 언어, 실시간 시스템, 고신뢰성

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(선입선출)로 실행되며, 더 높은 우선순위의 태스크는 더 낮은 우선순위의 태스크를 선점(preempt)합니다.

-- 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; 절대 시각을 기준으로 하므로 한 번의 처리가 지연되더라도 다음 기동 시각이 정확함

다만 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 시간은 거의 0에 가까울 것입니다. 다만 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. 참고 자료

같은 태그를 공유하는 최신 기사입니다. 더 가까운 주제로 지식을 넓힐 수 있습니다.

이 기사와 가까운 토픽 페이지입니다. 기사를 출발점 삼아 관련 서비스와 다른 기사로 이어집니다.

자주 묻는 질문

이 기사 주제에 대해 상담 시 자주 나오는 질문을 모았습니다.

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은 절대 시각을 기준으로 다음 기동 시각을 결정하므로, 한 번의 처리가 지연되더라도 이후의 기동 시각은 올바르게 유지됩니다. 다만 처리가 다음 기상 시각을 초과한 경우 delay until은 거의 즉시 반환되므로, 이를 deadline miss로 감지하고 과부하로 처리하는 설계를 별도로 마련해야 합니다.

저자 프로필

기사 저자의 프로필 페이지입니다.

Go Komura

합동회사 코무라소프트 대표

Windows 소프트웨어 개발, 기술 상담, 장애 조사를 중심으로 재현이 어려운 장애 조사와 기존 자산이 남아 있는 프로젝트에 강점이 있습니다.

블로그 목록으로 돌아가기