Ada로 하는 실시간 시스템 프로그래밍 ── 우선순위·주기·실행 시간 제어 실전

· 업데이트: · · Ada, RealTime, Ravenscar, CeilingLocking, Tasking, Scheduling, PriorityInversion, ProgrammingLanguage, 실시간, 고신뢰성

수정 이력(5건, 최종 수정 2026년 09월 03일)

이 글에 적용한 변경 사항의 기록입니다. 보관해 둔 수정 전 버전은 DOI가 부여된 고정 URL에서 읽을 수 있습니다.

관련 기사 링크를 한국어 permalink에 맞추는 등 CI가 지적한 표시용 수정을 반영했습니다. 본문의 기술적인 주장은 바꾸지 않았습니다.
permalink·저자 표기·지식 맵 래퍼·깨진 내부 링크 등 CI가 지적한 표시용 수정을 반영했습니다. 본문의 기술적인 주장은 바꾸지 않았습니다.
기사 앞부분에 「이 기사의 지식 맵」 절을 추가했습니다. 본문에서 다루는 개념과 그 관계를 요약·그림·상세 페이지 링크로 정리한 것입니다. 본문의 주장은 바꾸지 않았습니다.
외부 리뷰(1283건)에 대응하여 본문을 업데이트했습니다. 개별 변경 내용은 아래 이력을 참고하세요.
바로 앞 표와 완전히 중복되던 그림을 삭제했습니다. 우선순위와 주기 태스크 예에 대해 GNAT 13.3.0과 Ubuntu 24.04에서 실제로 빌드·실행한 출력을 실었고(시작 로그 순서가 실행마다 바뀌는 점도 관측 그대로 주석했습니다), 대상 독자와 검증 환경, `gnatchop`으로 나눈 결과를 추가했습니다. 종합 데모는 이 환경에서 끝까지 실행되지 않았기 때문에 출력은 싣지 않았습니다.
최초 공개
이 글을 인용하기(DOI(등록된 아카이브): 10.5281/zenodo.21635332)

아래 DOI는 이전에 등록된 아카이브를 가리키며 현재 본문과 다를 수 있습니다. 현재 본문을 참조할 때는 이 페이지의 URL을 사용하세요.

Go Komura (2026). 「Ada로 하는 실시간 시스템 프로그래밍 ── 우선순위·주기·실행 시간 제어 실전」. 합동회사 코무라소프트. https://comcomponent.com/ko/blog/ada-real-time-systems/

DOI(등록된 아카이브)
10.5281/zenodo.21635332
DOI(마지막 등록 버전)
10.5281/zenodo.21635333

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합니다.

대상 독자와 전제 지식: 이전 기사에서 다룬 태스크·rendezvous·보호 객체의 기초를 아는 독자를 가정합니다. 임베디드 기기나 고신뢰 시스템의 제어 소프트웨어에 관심 있는 개발자용이며, Ada 문법 자체의 입문은 다루지 않습니다.

검증 환경: 이 기사의 8개 예는 GNAT 13.3.0 (Ubuntu 24.04, x86-64)에서 빌드할 수 있음을 확인했습니다. 본문에 실행 결과를 실은 예 (3장·5장)의 출력도 같은 환경에서 수집한 것입니다. 우선순위나 선점이 실제로 어떻게 적용되는지는 OS와 GNAT 런타임에 의존하므로 (12장), 출력의 세부는 환경에 따라 달라집니다.

참고로, 이 기사에 나오는 코드 조각은 장마다 파일로 정리한 참조용 코드집으로 GitHub에 공개하고 있습니다.

ada-real-time-systems - komurasoft-blog-samples (GitHub)

그림의 실선은 항상 성립하는 관계, 점선은 조건이 붙는 관계입니다(성립 조건은 상세 페이지의 관계별 설명에 적혀 있습니다). 관계 전체 목록(총 21건, 근거와 확신도 포함)과 주요 개념의 정의는 지식 맵 상세 페이지에 정리되어 있습니다(일본어). 데이터: JSON-LD / Turtle

2. 실시간 시스템이란 무엇인가

먼저 용어부터 정리합니다.

개념 설명
하드 리얼타임 deadline 초과가 시스템의 치명적인 실패를 뜻함 (비행 제어, 에어백, 페이스메이커)
소프트 리얼타임 deadline 초과가 바람직하지 않지만, 드문 초과는 허용됨 (동영상 스트리밍, 게임)
deadline 태스크가 완료해야 하는 절대 시각
period (주기) 태스크가 반복해서 시작되는 시간 간격
WCET (Worst-Case Execution Time) 태스크의 최악 실행 시간
jitter (지터) 주기 실행의 편차
schedulability (스케줄 가능성) 「그 태스크 집합이 모든 기한을 지키며 실행할 수 있는가」라는 성질. 이를 이론적으로 확인하는 작업이 schedulability 분석 (응답 시간 분석 등)
Ravenscar 프로파일 (레이븐스카) Ada의 태스크 기능을 정적 분석하기 쉬운 서브셋으로 제한하는 규약. 이름은 제정 회의가 열린 영국 마을 Ravenscar에서 유래합니다 (6장)

실시간 시스템 설계에서는 각 태스크에 대해 「WCET <= deadline」이 성립하는 것이 중요한 필요 조건이 됩니다. 다만 그것만으로 시스템 전체의 기한 달성이 보장되는 것은 아닙니다. 블로킹 시간, 우선순위 할당, jitter, 인터럽트, 런타임과 OS의 동작을 포함한 응답 시간 분석이 따로 필요합니다. 실무에서는 여유를 남기기 위해 WCET < deadline을 목표로 합니다. Ada의 실시간 기능은 그 분석을 하기 쉬운 예측 가능한 실행 모델을 언어 수준에서 제공합니다.

실시간 요건기한 내 미달 = 치명적 실패드문 초과는 허용deadline완료해야 할 절대 시각주기반복 간격WCET최악 실행 시간jitter주기의 편차하드 리얼타임비행 제어에어백페이스메이커소프트 리얼타임동영상 전송게임UIAda Annex D예측 가능성을 뒷받침하는 메커니즘FIFO_Within_PrioritiesCeiling_Lockingdelay untilschedulability 분석필요 조건: WCET &lt;= deadline충분성은 응답 시간 분석으로 확인

실시간 시스템에서 가장 위험한 현상 중 하나가 우선순위 역전입니다. 이 문제는 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의 기본 형태
-- configuration pragma는 context clause보다 앞에 둡니다

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가 최저입니다.
  • 이 데모의 task body는 무거운 계산이 아니라, 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를 실행

실행 예 (GNAT 13.3.0 / Ubuntu 24.04, x86-64):

$ gnatchop -w 01_task_priority.ada .     # → task_priority_demo.adb
$ gnatmake task_priority_demo.adb
$ ./task_priority_demo
[T=0.0s] High priority task started
[T=0.0s] Low priority task started
=== Task Priority Demo (FIFO_Within_Priorities) ===
Main: waiting for tasks to complete...
[T=0.1s] High priority task completed
[T=0.5s] Low priority task completed
Main: done

두 태스크의 시작 로그가 메인의 제목 행보다 먼저 나온다는 점에 주의해야 합니다. 선언부의 태스크는 둘러싼 서브프로그램의 body에 들어가기 전에 활성화되므로, 이런 순서가 될 수 있습니다. 또한 이 시작 로그 2행의 순서는 실행할 때마다 바뀔 수 있습니다. 12장과 같이 pragma Priority가 실제 스케줄링에 어떻게 반영되는지는 OS와 GNAT 런타임에 달려 있고, 범용 Linux에서는 우선순위대로의 시작 순서가 보장되지 않기 때문입니다. 이 예에서 안정적으로 관측할 수 있는 것은 delay until로 지정한 0.1초 / 0.5초의 완료 순서 쪽입니다.

Ada의 우선순위 범위 (GNAT의 기본값):
  Priority'First  = 0   (최저)
  Priority'Last   = 30  (최고, 다만 OS에 의존)

4. Ceiling_Locking ── 언어가 우선순위 역전을 막다

실시간 시스템에서 가장 골치 아픈 문제 중 하나가 우선순위 역전 (priority inversion) 입니다. 높은 우선순위 태스크가 낮은 우선순위 태스크가 가진 락을 기다리고, 그 낮은 우선순위 태스크가 중간 우선순위 태스크에 선점됨으로써, 높은 우선순위 태스크가 무기한으로 블록되는 현상입니다.

Ada는 이 문제에 대해 Ceiling_Locking 프로토콜을 보호 객체에 직접 넣었습니다.

-- 02_ceiling_locking.ada
-- Ceiling_Locking 프로토콜을 이용한 우선순위 역전 방지
-- configuration pragma는 context clause보다 앞에 둡니다

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, ... (처리 시간에 좌우되지 않음)

실행 예 (GNAT 13.3.0 / Ubuntu 24.04, x86-64):

$ gnatchop -w 03_periodic_task.ada .     # → periodic_task_demo.adb
$ gnatmake periodic_task_demo.adb
$ ./periodic_task_demo
[Sensor] Periodic task starts, period= 0.200000000s, cycles= 5
=== Periodic Task Demo (delay until) ===
Main: waiting for 5 cycles...
[Sensor] Cycle 1 at 0.200326876s
[Sensor] Cycle 2 at 0.400159550s
[Sensor] Cycle 3 at 0.600183089s
[Sensor] Cycle 4 at 0.800327451s
[Sensor] Cycle 5 at 1.000260472s
[Sensor] Periodic task finished. Actual elapsed: 1.000297359s
Main: done

각 사이클에는 0.2~0.3밀리초 정도의 깨어남 지연이 나오지만, 그 지연이 다음 주기로 넘어가지 않는다는 것을 읽을 수 있습니다. 5번째 사이클에서도 기준 시각에서의 어긋남은 1밀리초 미만입니다. delay Period;로 썼다면, 이 지연이 매번 쌓여 5사이클 뒤에는 눈에 보이는 차가 됩니다. 참고로 이 수치는 범용 Linux에서의 한 예이며, 하드 리얼타임 환경에서 보장되는 값이 아닙니다.

이 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);를 지정한다
-- 빌드: gnatchop -w 04_ravenscar_profile.ada .
--         → ravenscar_state.ads / ravenscar_state.adb / ravenscar_demo.adb 로 분할된다
--         gnat.adc를 준비한 뒤 gnatmake ravenscar_demo.adb

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나 access 타입) 실행 시 메모리 할당이 비결정적
select 문 여러 대안만이 아니라, select 문 전체가 제어 흐름 분석을 어렵게 함
abort 문 비동기 중단이 상태의 예측 불가능성을 만듦
Ada.Task_Attributes 실행 시 동적 동작
동적 우선순위 변경 스케줄링 분석의 전제가 실행 중에 바뀜
상대 delay (delay) 누적 드리프트가 생기기 쉬우므로, 절대 시각의 delay until을 씀
보호 객체당 여러 엔트리 블로킹 조건과 분석 대상이 늘어남
태스크의 종료 Ravenscar에서는 모든 태스크를 비종료로 다룸
requeue 문 제어 흐름 추적이 복잡해짐

이 제한 덕분에 Ravenscar를 따른 프로그램은 정적 타이밍 분석을 하기 쉬운 형태가 됩니다. 이는 DO-178C (항공기 소프트웨어)나 ISO 26262 (자동차 기능 안전) 같은 안전 규격에서 요구하는 특성입니다. 아래 목록은 주요 제한의 발췌이며, 실제 프로파일에는 No_Task_Hierarchy나 Detect_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)
-- 높은 우선순위 태스크를 폴링 없이 깨우는 메커니즘
-- 빌드: gnatchop -w 05_timing_events.ada .
--         → signal_pkg.ads / signal_pkg.adb / timing_events_demo.adb 로 분할된다
--         gnatmake timing_events_demo.adb

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) ── Ceiling_Locking에 의해 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_Worker가 delay 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 공작 기계임무 컴퓨터장기 운용 시스템

그림에서 자동차만 「한정·선택적 적용」이라고 쓴 것은, 언어로서의 적성보다 기존 생태계의 크기에 따른 이유가 크기 때문입니다. 차량용 소프트웨어는 AUTOSAR 같은 업계 표준 API 사양, 공급자가 제공하는 코드, 인증을 통과한 컴파일러와 검증 도구, 그리고 기술자 인구까지가 C와 MISRA-C를 전제로 쌓여 있습니다. 언어를 바꾼다는 것은, 대상이 1컴포넌트뿐이라 해도, 그 주변의 도구와 조달·검증 프로세스를 한꺼번에 다시 갖춘다는 뜻입니다. 그래서 Ada는 차량 전체의 표준이라기보다, 특히 높은 보장이 필요한 일부 컴포넌트나, 이미 Ada 자산과 개발 체제를 가진 조직에서 선택적으로 쓰이는 위치에 놓이기 쉽습니다. 거꾸로 말하면, 항공우주나 철도처럼 생태계 자체가 고신뢰 쪽으로 기울어 있는 분야에서는 이 장벽이 애초에 없습니다.

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 실시간 기능의 본질은 「나중에 붙인 것이 아니다」는 점입니다. 우선순위 역전을 막는 락 규칙, 주기 실행을 위한 시각 지정, 실행 시간 모니터링 등이 언어 사양의 일부로 제공됩니다. 물론 deadline 달성 자체는 설계와 분석으로 확인하지만, 그 전제를 언어 런타임이 갖춰 준다는 점이 큰 강점입니다.

다음 단계로, Ada로 실시간 시스템 개발을 실제로 해 보려면 Alire로 GNAT 툴체인을 설치하고, 이 기사의 샘플 코드를 gnatchop + gnatmake로 빌드해 보세요.

참고로 Ada 동시성 처리의 기본(태스크, rendezvous, 보호 객체)은 이전 기사 「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 소프트웨어 개발, 기술 상담, 장애 조사를 중심으로 재현이 어려운 장애 조사와 기존 자산이 남아 있는 프로젝트에 강점이 있습니다.

블로그 목록으로 돌아가기