Ada로 하는 실시간 시스템 프로그래밍 ── 우선순위·주기·실행 시간 제어 실전
· 업데이트: · Go Komura · 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의 실시간 기능은 그 분석을 하기 쉬운 예측 가능한 실행 모델을 언어 수준에서 제공합니다.
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[deadline<br/>완료해야 할 절대 시각]
P[주기<br/>반복 간격]
W[WCET<br/>최악 실행 시간]
J[jitter<br/>주기의 편차]
end
D --> Analysis[schedulability 분석]
P --> Analysis
W --> Analysis
J --> Analysis
HRT --> Analysis
SRT --> Analysis
Mechanism --> Analysis
Analysis --> Constraint[필요 조건: WCET <= deadline<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의 기본 형태
-- 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를 기준으로 상대적인 우선순위를 설계하는 것이 일반적입니다.
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) 메인 종료
실행 예 (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의 구조:
- 보호 객체에
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; |
절대 시각 기준이므로, 한 번의 처리가 늦어도 다음 시작 시각은 올바름 |
다만 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 패턴은 이후의 모든 주기 태스크에서 사용합니다.
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);를 지정한다
-- 빌드: 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처럼 런타임과 분석성에 관련된 추가 규칙도 들어갑니다.
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)
-- 높은 우선순위 태스크를 폴링 없이 깨우는 메커니즘
-- 빌드: 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)로 정의되어 있으며, 락 해제 시 자동으로 재평가됩니다.
이 코드에는 애플리케이션 쪽 뮤텍스, 세마포, 조건 변수가 등장하지 않습니다. 필요한 대기는 보호 객체의 엔트리 배리어로 표현합니다.
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[장기 운용 시스템]
그림에서 자동차만 「한정·선택적 적용」이라고 쓴 것은, 언어로서의 적성보다 기존 생태계의 크기에 따른 이유가 크기 때문입니다. 차량용 소프트웨어는 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에서의 안전한 동시성 처리 ── task와 protected object 실전 가이드
Ada의 언어 내장 동시성 처리인 task와 protected object 입문 기사입니다. rendezvous(entry/accept), 선택적 accept, protected object를 통한 상호 배제, 타임아웃이 있는 호출, task 우...
Ada의 generic programming ── 타입으로 계약을 쓰고, 재사용을 제로 비용으로 구현한다
Ada의 generic programming을 generic subprogram, generic package, formal subprogram, 타입 카테고리, 실무 설계 지침까지 체계적으로 설명합니다. 타입 안전한 재사용과 제로 비용 추상화의...
SPARK로 하는 형식 검증 입문 ── Ada의 계약에서 수학적 증명으로
Ada의 서브셋 언어 SPARK로 형식 검증을 다루는 실무 입문 기사입니다. 계약(Pre/Post)에서 증명으로 단계를 올리는 방법, GNATprove 사용법, 루프 불변조건, 데이터 플로 계약, 증명 레벨, 그리고 실제 프로젝트에 적용하는 방법...
Ada 언어의 매력 ── 타입으로 설계를 말하고, 수십 년 동안 동작하는 소프트웨어를 지탱하는 언어
Ada 언어의 매력을 소개합니다. 강한 타입 시스템, 범위 제약, 패키지로 명세와 구현을 분리하는 방식, Design by Contract, 언어에 내장된 task, SPARK에 의한 형식 검증, GNAT과 Alire 개발 환경까지, 고신뢰 소프...
디스크 사용률 100%는 무엇을 멈추면 해결될까 ── SysMain·Windows Search·Defender 구분법
Windows의 디스크 사용률이 100%가 되는 원인을 처리량·응답 시간·파일로 분리해 봅니다. SysMain의 일시 중지와 복귀, Windows Search 검색 범위의 재검토, Defender를 끄지 않는 조사 방법을 그림으로 설명합니다.
관련 토픽
이 기사와 가까운 토픽 페이지입니다. 기사를 출발점 삼아 관련 서비스와 다른 기사로 이어집니다.
Windows 기술 토픽
Windows 개발, 장애 조사, 기존 자산 활용에 관한 KomuraSoft LLC 기사를 모은 토픽 허브입니다.
자주 묻는 질문
이 기사 주제에 대해 상담 시 자주 나오는 질문을 모았습니다.
- 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로 검출하고 과부하로 다루는 설계가 따로 필요합니다.