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의 실시간 기능은 이러한 분석을 수행하기 쉬운 예측 가능한 실행 모델을 언어 수준에서 제공합니다.
flowchart LR
HRT[하드 리얼타임] -->|기한 내 미달성 = 치명적 실패| Examples[비행 제어<br/>에어백<br/>심장 박동기]
SRT[소프트 리얼타임] -->|드문 초과는 허용| Examples2[동영상 전송<br/>게임<br/>UI]
Ada[Ada Annex D<br/>예측 가능성을 지원하는 메커니즘] --> Mechanism[FIFO_Within_Priorities<br/>Ceiling_Locking<br/>delay until]
subgraph Requirements[실시간 요건]
D[데드라인<br/>완료해야 하는 절대 시각]
P[주기<br/>반복 간격]
W[WCET<br/>최악 실행 시간]
J[지터<br/>주기의 편차]
end
D --> Analysis[스케줄 가능성 분석]
P --> Analysis
W --> Analysis
J --> Analysis
HRT --> Analysis
SRT --> Analysis
Mechanism --> Analysis
Analysis --> Constraint[필요조건: WCET <= 데드라인<br/>충분성은 응답 시간 분석으로 확인]
실시간 시스템에서 가장 위험한 현상 중 하나가 우선순위 역전입니다. 이 문제는 1997년 Mars Pathfinder에서 실제로 발생하여 탐사기가 리셋을 반복하는 원인이 되었습니다.
sequenceDiagram
participant S as 스케줄러
participant L as 저우선순위 태스크
participant H as 고우선순위 태스크
participant M as 중우선순위 태스크
participant R as 공유 리소스
L->>R: 락 획득
activate L
Note over L: 크리티컬 섹션 실행 중
Note over S,L: H 기상으로 스케줄러가 L을 중단
deactivate L
activate H
H->>R: 락 획득을 시도
Note over H: 락 대기로 블록!(L이 보유 중)
deactivate H
Note over S,L: H는 락 대기이므로 L을 재개
activate L
Note over L: 락 해제를 향해 계속...
Note over S,L: M 기상으로 스케줄러가 L을 중단
deactivate L
activate M
Note over L: L은 락을 해제할 수 없음
Note over M: M이 실행을 계속(H도 L도 움직일 수 없음)
Note over H: 【우선순위 역전】고우선순위가 무기한 블록
deactivate M
저우선순위 태스크가 락을 보유한 채 중우선순위 태스크에 의해 선점되어, 고우선순위 태스크가 무기한 블록됩니다. Mars Pathfinder에서 실제로 취해진 대책은 VxWorks의 priority inheritance를 활성화하는 것이었지만, Ada는 같은 종류의 문제에 대해 다른 방식인 Ceiling_Locking을 언어 기능으로 제공합니다.
3. 태스크 우선순위의 기초 ── FIFO_Within_Priorities
FIFO_Within_Priorities는 Ada Annex D에서 지정할 수 있는 표준적인 우선순위 기반 디스패치 정책입니다. 정책을 명시하지 않을 경우의 기본 동작은 구현 정의 사항이지만, GNAT에서는 많은 타깃에서 이 계열의 정책이 사용됩니다. 동일 우선순위 내에서는 FIFO(선입선출)로 실행되며, 더 높은 우선순위의 태스크는 더 낮은 우선순위의 태스크를 선점(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를 기준으로 상대적인 우선순위를 설계하는 것이 일반적입니다.
sequenceDiagram
participant S as 스케줄러
participant Main as 메인 태스크
participant HP as 고우선순위 태스크<br/>(Priority=Last)
participant LP as 저우선순위 태스크<br/>(Priority=First)
Main->>HP: 태스크 생성
Main->>LP: 태스크 생성
Note over HP,LP: T=0ms: 두 태스크 모두 runnable
S->>HP: 최고 우선순위인 HP를 선택
activate HP
Note over HP: 시작 로그를 출력
HP->>S: delay until T+100ms로 블록
deactivate HP
S->>LP: 다음으로 LP를 실행
activate LP
Note over LP: 시작 로그를 출력
LP->>S: delay until T+500ms로 블록
deactivate LP
Note over S: T=100ms: HP 기상
S->>HP: HP를 실행
activate HP
Note over HP: 완료 로그를 출력
deactivate HP
Note over S: T=500ms: LP 기상
S->>LP: LP를 실행
activate LP
Note over LP: 완료 로그를 출력
deactivate LP
Note over Main: (T=800ms) 메인 종료
Ada의 우선순위 범위(GNAT 기본값):
Priority'First = 0 (최저)
Priority'Last = 30 (최고, 단 OS에 따라 다름)
4. Ceiling_Locking ── 언어가 우선순위 역전을 방지하다
실시간 시스템에서 가장 다루기 어려운 문제 중 하나가 우선순위 역전(priority inversion)입니다. 고우선순위 태스크가 저우선순위 태스크가 보유한 락을 기다리는데, 그 저우선순위 태스크가 중우선순위 태스크에 의해 선점됨으로써 고우선순위 태스크가 무기한 블록되는 현상입니다.
Ada는 이 문제에 대해 Ceiling_Locking 프로토콜을 보호 객체에 직접 내장하고 있습니다.
-- 02_ceiling_locking.ada
-- Ceiling_Locking 프로토콜을 통한 우선순위 역전 방지
-- 구성 프라그마는 컨텍스트 절 앞에 배치
pragma Locking_Policy (Ceiling_Locking);
with Ada.Text_IO; use Ada.Text_IO;
with System; use System;
with Ada.Real_Time; use Ada.Real_Time;
procedure Ceiling_Locking_Demo is
Ceiling : constant System.Any_Priority := System.Any_Priority'Last;
protected Shared_Data is
pragma Priority (Ceiling);
procedure Write (V : Integer);
function Read return Integer;
private
Value : Integer := 0;
end Shared_Data;
protected body Shared_Data is
procedure Write (V : Integer) is
begin
Value := V;
end Write;
function Read return Integer is
begin
return Value;
end Read;
end Shared_Data;
task Producer is
pragma Priority (Priority'Last);
pragma Storage_Size (4 * 1024);
end Producer;
task Consumer is
pragma Priority (Priority'First);
pragma Storage_Size (4 * 1024);
end Consumer;
task body Producer is
begin
Put_Line ("[T=0.0s] Producer (high prio): about to write");
Shared_Data.Write (42);
Put_Line ("[T=0.0s] Producer (high prio): write done");
delay until Clock + Milliseconds (100);
end Producer;
task body Consumer is
begin
delay until Clock + Milliseconds (10);
Put_Line ("[T=0.01s] Consumer (low prio): about to read");
declare
V : Integer;
begin
V := Shared_Data.Read;
Put_Line ("[T=0.01s] Consumer (low prio): read done, got" &
Integer'Image (V));
end;
delay until Clock + Milliseconds (100);
end Consumer;
begin
Put_Line ("=== Ceiling_Locking Demo ===");
Put_Line ("Main: producer priority = Last, consumer priority = First");
Put_Line ("Ceiling = Any_Priority'Last, locking = Ceiling_Locking");
delay until Clock + Milliseconds (300);
Put_Line ("Main: done");
end Ceiling_Locking_Demo;
Ceiling_Locking의 동작 방식:
- 보호 객체에
pragma Priority (Ceiling)으로 실링 우선순위를 설정한다. - 어떤 태스크가 보호 객체에 들어오든, 진입 시 자동으로 실링 우선순위로 승격된다.
- 이를 통해 보호 객체를 사용 중인 태스크를 중우선순위 태스크가 선점할 수 없다.
- 보호 객체에서 나오면 원래의 우선순위로 돌아온다.
아래 그림은 직전 샘플 코드의 정확한 시각 추적이 아니라, 그림 2의 우선순위 역전 패턴이 Ceiling_Locking으로 어떻게 억제되는지를 보여주는 개념도입니다.
sequenceDiagram
participant S as 스케줄러
participant L as 저우선순위 태스크<br/>(우선순위=10)
participant M as 중우선순위 태스크<br/>(우선순위=20)
participant H as 고우선순위 태스크<br/>(우선순위=30)
participant PO as 보호 객체<br/>(천장 우선순위=30)
Note over PO: 활성 우선순위 > 천장 우선순위인 호출자는 Program_Error<br/>그림 속 H(30)는 천장(30)과 같으므로 진입 가능
L->>PO: 보호 조작에 진입
activate L
Note over L,PO: 실행 우선순위가 30으로 승격
Note over S: M이 기상
Note over S,L: L은 천장 우선순위 30으로 실행 중<br/>M(20)은 선점할 수 없음
Note over S: H가 기상
Note over S,H: H(30)는 천장 검사에 합격<br/>단 L이 PO를 사용 중이므로 대기
L->>PO: 조작을 실행
L->>PO: 보호 조작에서 나옴
deactivate L
Note over L: 우선순위가 10으로 복귀
Note over S,H: PO 해제 후 H를 실행
activate H
H->>PO: 보호 조작에 진입
Note over H,PO: H(30) = 천장(30)이므로 경쟁 해소 후 진입 가능
H->>PO: 보호 조작에서 나옴
deactivate H
설계 지침: 보호 객체의 실링 우선순위는 그 보호 객체를 사용하는 모든 태스크의 최고 우선순위 이상으로 설정한다. 이를 위반하여 천장 우선순위보다 높은 활성 우선순위를 가진 태스크가 보호 조작을 호출하면, Ada에서는
Program_Error를 통해 설계 오류를 검출할 수 있다.
C 언어의 pthread 뮤텍스로 동등한 것을 구현하려면 PTHREAD_PRIO_PROTECT 속성을 명시적으로 설정해야 하지만, Ada에서는 이것이 언어의 표준 기능입니다.
5. delay until ── 드리프트 없이 주기 태스크를 실행하다
실시간 시스템의 기본 패턴은 주기 태스크입니다. 일정 간격으로 반복 실행되는 태스크에서는 누적적인 타이밍 오차(드리프트)를 방지하는 것이 매우 중요합니다.
Ada의 delay until은 이 문제를 우아하게 해결합니다.
-- 03_periodic_task.ada
-- delay until을 이용한 주기 태스크 ── 누적 드리프트 방지
with Ada.Text_IO; use Ada.Text_IO;
with System; use System;
with Ada.Real_Time; use Ada.Real_Time;
procedure Periodic_Task_Demo is
Period_MS : constant Time_Span := Milliseconds (200);
Cycles : constant Positive := 5;
task Sensor_Reader is
pragma Priority (Priority'Last - 2);
pragma Storage_Size (4 * 1024);
end Sensor_Reader;
task body Sensor_Reader is
Start_Time : constant Time := Clock;
Next_Release : Time := Start_Time + Period_MS;
Cycle_Count : Natural := 0;
begin
Put_Line ("[Sensor] Periodic task starts, period=" &
To_Duration (Period_MS)'Image & "s, cycles=" &
Natural'Image (Cycles));
for I in 1 .. Cycles loop
delay until Next_Release;
Cycle_Count := Cycle_Count + 1;
Put_Line ("[Sensor] Cycle" & Natural'Image (Cycle_Count) &
" at" & Duration'Image (To_Duration (Clock - Start_Time)) & "s");
Next_Release := Next_Release + Period_MS;
end loop;
Put_Line ("[Sensor] Periodic task finished. Actual elapsed:" &
Duration'Image (To_Duration (Clock - Start_Time)) & "s");
end Sensor_Reader;
begin
Put_Line ("=== Periodic Task Demo (delay until) ===");
Put_Line ("Main: waiting for" & Natural'Image (Cycles) & " cycles...");
delay until Clock + Milliseconds (1500);
Put_Line ("Main: done");
end Periodic_Task_Demo;
왜 delay until인가:
| 방법 | 문제점 |
|---|---|
delay Period; |
각 반복의 처리 시간이 누적되어 주기가 점차 어긋남(누적 드리프트) |
delay until Next_Release; Next_Release := Next_Release + Period; |
절대 시각을 기준으로 하므로 한 번의 처리가 지연되더라도 다음 기동 시각이 정확함 |
다만 delay until은 처리 시간이 주기 이내에 들어온다는 것을 자동으로 보장하지는 않습니다. 처리가 다음 기상 시각을 넘어선 경우, 그 delay until은 거의 즉시 반환되며 시스템은 deadline miss로 처리해야 하는 상태가 됩니다.
delay의 경우:
T=0ms → 처리(15ms) → delay 100ms → T=115ms → 처리(10ms) → ...
실제 간격: 115ms, 110ms, ...(처리 시간이 누적됨)
delay until의 경우:
Next_Release: 100ms, 200ms, 300ms, ...(절대 시각)
T=0ms → 처리(15ms) → delay until 100ms → T=100ms → 처리(10ms) → delay until 200ms
실제 간격: 100ms, 100ms, ...(처리 시간에 좌우되지 않음)
이 delay until 패턴은 이후의 모든 주기 태스크에서 사용합니다.
flowchart TB
subgraph Bad["delay Period - 누적 드리프트"]
B1[T=0ms: 계산 15ms] --> B2[delay 100ms → 115ms에 기상]
B2 --> B3[계산 10ms → 125ms]
B3 --> B4[delay 100ms → 225ms에 기상]
B4 --> B5[실제 간격: 115ms, 110ms...]
end
subgraph Good["delay until - 절대 시각 기준"]
G1[다음 = T+100ms] --> G2[계산 15ms]
G2 --> G3[delay until T+100ms → 100ms에 기상]
G3 --> G4[계산 10ms]
G4 --> G5[다음 = T+200ms → 200ms에 기상]
G5 --> G6[실제 간격: 100ms, 100ms...]
end
subgraph Overrun["주기 초과 - deadline miss"]
O1[다음 = T+100ms] --> O2[계산 130ms]
O2 --> O3[delay until T+100ms는 즉시 반환]
O3 --> O4[지연을 감지하여 과부하로 처리]
end
Bad --> Drift[시간이 지남에 따라 오차가 누적]
Good --> Stable[누적 드리프트를 방지]
Good --> Overrun
6. Ravenscar 프로파일 ── 검증 가능한 실시간 서브셋
Ada의 태스크 기능은 강력하지만, 안전성이 극히 중요한 시스템에서는 「지나치게 강력한」 것이 문제가 됩니다. 동적 태스크 생성, select 문, abort 문 등은 최악 실행 시간의 정적 분석을 어렵게 만듭니다.
Ravenscar 프로파일은 이러한 문제에 대해 Ada가 제공하는 답입니다. 태스크 기능을 정적으로 분석 가능하고 결정론적인 서브셋으로 제한합니다.
-- 04_ravenscar_profile.ada
-- Ravenscar 프로파일의 기본형
-- 컴파일 시 gnat.adc에서 pragma Profile (Ravenscar); 를 지정한다
with Ada.Text_IO; use Ada.Text_IO;
with System; use System;
with Ada.Real_Time; use Ada.Real_Time;
package Ravenscar_State is
protected Signal is
pragma Priority (System.Default_Priority + 5);
entry Wait_For_Release;
procedure Release;
private
Released : Boolean := False;
end Signal;
task Periodic_Worker is
pragma Priority (System.Default_Priority + 1);
pragma Storage_Size (4 * 1024);
end Periodic_Worker;
task Monitor is
pragma Priority (System.Default_Priority);
pragma Storage_Size (4 * 1024);
end Monitor;
end Ravenscar_State;
with Ada.Text_IO; use Ada.Text_IO;
with System; use System;
with Ada.Real_Time; use Ada.Real_Time;
package body Ravenscar_State is
protected body Signal is
entry Wait_For_Release when Released is
begin
Released := False;
end Wait_For_Release;
procedure Release is
begin
Released := True;
end Release;
end Signal;
task body Periodic_Worker is
Start_Time : constant Time := Clock;
Next_Release : Time := Start_Time + Milliseconds (100);
Period : constant Time_Span := Milliseconds (100);
Cycle_Count : Natural := 0;
begin
Put_Line ("[Worker] Ravenscar periodic task starts");
for I in 1 .. 4 loop
delay until Next_Release;
Cycle_Count := Cycle_Count + 1;
Put_Line ("[Worker] Cycle" & Natural'Image (Cycle_Count) &
" at" & Duration'Image (To_Duration (Clock - Start_Time)) & "s");
Signal.Release;
Next_Release := Next_Release + Period;
end loop;
Put_Line ("[Worker] Finished demo, waiting (Ravenscar: No_Task_Termination)");
loop
delay until Clock + Seconds (1);
end loop;
end Periodic_Worker;
task body Monitor is
begin
Put_Line ("[Monitor] Waiting for signals...");
for I in 1 .. 4 loop
Signal.Wait_For_Release;
Put_Line ("[Monitor] Received signal" & Natural'Image (I));
end loop;
Put_Line ("[Monitor] All signals received, waiting (Ravenscar: No_Task_Termination)");
loop
delay until Clock + Seconds (1);
end loop;
end Monitor;
end Ravenscar_State;
with Ravenscar_State; use Ravenscar_State;
with Ada.Text_IO; use Ada.Text_IO;
with Ada.Real_Time; use Ada.Real_Time;
procedure Ravenscar_Demo is
begin
Put_Line ("=== Ravenscar Profile Demo ===");
Put_Line ("(compile with: gnatmake -gnatec=gnat.adc ravenscar_demo)");
Put_Line ("Main: waiting for Ravenscar tasks...");
delay until Clock + Milliseconds (800);
Put_Line ("Main: demo window elapsed; waiting forever (Ravenscar: No_Task_Termination)");
loop
delay until Clock + Seconds (1);
end loop;
end Ravenscar_Demo;
Ravenscar 프로파일의 제한:
| 금지되는 기능 | 이유 |
|---|---|
동적 태스크 생성(new나 액세스 타입) |
실행 시 메모리 할당이 비결정적 |
select 문 |
복수 대안뿐 아니라 select 문 전체가 제어 흐름 분석을 어렵게 함 |
abort 문 |
비동기 중단이 상태의 예측 불가능성을 낳음 |
Ada.Task_Attributes |
실행 시 동적 동작 |
| 동적 우선순위 변경 | 스케줄링 분석의 전제가 실행 중에 바뀜 |
상대 delay(delay) |
누적 드리프트를 발생시키기 쉬우므로 절대 시각의 delay until을 사용 |
| 보호 객체당 여러 엔트리 | 블로킹 조건과 분석 대상이 늘어남 |
| 태스크 종료 | Ravenscar에서는 모든 태스크를 비종료로 취급 |
requeue 문 |
제어 흐름 추적이 복잡해짐 |
이러한 제한을 통해 Ravenscar를 준수하는 프로그램은 정적 타이밍 분석을 수행하기 쉬운 형태가 됩니다. 이는 DO-178C(항공기 소프트웨어)나 ISO 26262(자동차 기능 안전) 같은 안전 규격에서 요구되는 특성입니다. 아래 목록은 주요 제한 사항의 발췌이며, 실제 프로파일에는 No_Task_Hierarchy나 Detect_Blocking 등 런타임과 분석 가능성에 관련된 추가 규칙도 포함됩니다.
flowchart TB
Full[완전한 Ada 태스크 기능] --> Profile[Ravenscar 프로파일]
Profile --> Restrict[제한]
Profile --> Policy[필수 정책]
Restrict --> R1[동적 태스크 생성 금지]
Restrict --> R2[select 문 금지]
Restrict --> R3[abort 문 금지]
Restrict --> R4[Task_Attributes 금지]
Restrict --> R5[보호 객체당 1개 엔트리로 제한]
Restrict --> R6[requeue 문 금지]
Restrict --> R7[상대 delay 금지<br/>delay until 사용]
Restrict --> R8[동적 우선순위 변경 금지]
Restrict --> R9[태스크 종료 금지<br/>모든 태스크 비종료]
Policy --> P1[FIFO_Within_Priorities]
Policy --> P2[Ceiling_Locking]
Restrict --> Benefit[수행하기 쉬워지는 것:<br/>정적 타이밍 분석]
Policy --> Benefit
Benefit --> DO178[DO-178C<br/>항공기 소프트웨어]
Benefit --> ISO26262[ISO 26262<br/>자동차 기능 안전]
Benefit --> IEC62304[IEC 62304<br/>의료기기 소프트웨어]
Ravenscar 프로파일을 활성화하려면 gnat.adc 파일에 다음을 기술합니다:
pragma Profile (Ravenscar);
7. 타이밍 이벤트 ── 폴링 없는 시각 기반 기상
많은 실시간 시스템에서는 「지정된 시각이 되면 고우선순위 태스크를 기상시킨다」는 요구 사항이 자주 나타납니다. 단순한 구현에서는 타이머를 폴링하게 되지만, Ada는 더 세련된 메커니즘──타이밍 이벤트──을 제공합니다.
-- 05_timing_events.ada
-- 타이밍 이벤트 (Ada.Real_Time.Timing_Events)
-- 고우선순위 태스크를 폴링 없이 기상시키는 메커니즘
pragma Locking_Policy (Ceiling_Locking);
with Ada.Text_IO; use Ada.Text_IO;
with System; use System;
with Ada.Real_Time; use Ada.Real_Time;
with Ada.Real_Time.Timing_Events; use Ada.Real_Time.Timing_Events;
package Signal_Pkg is
protected type Signal_Type is
pragma Priority (System.Interrupt_Priority'Last);
entry Wait_For_Event;
procedure Fire (Event : in out Timing_Event);
private
Fired : Boolean := False;
end Signal_Type;
S : Signal_Type;
end Signal_Pkg;
with Ada.Text_IO; use Ada.Text_IO;
with System; use System;
with Ada.Real_Time; use Ada.Real_Time;
with Ada.Real_Time.Timing_Events; use Ada.Real_Time.Timing_Events;
package body Signal_Pkg is
protected body Signal_Type is
entry Wait_For_Event when Fired is
begin
Fired := False;
end Wait_For_Event;
procedure Fire (Event : in out Timing_Event) is
begin
Fired := True;
end Fire;
end Signal_Type;
end Signal_Pkg;
with Signal_Pkg; use Signal_Pkg;
with Ada.Text_IO; use Ada.Text_IO;
with System; use System;
with Ada.Real_Time; use Ada.Real_Time;
with Ada.Real_Time.Timing_Events; use Ada.Real_Time.Timing_Events;
procedure Timing_Events_Demo is
pragma Priority (29);
Timer_1 : Timing_Event;
Timer_2 : Timing_Event;
task Reactor is
pragma Priority (System.Default_Priority + 5);
pragma Storage_Size (4 * 1024);
end Reactor;
task body Reactor is
begin
Put_Line ("[Reactor] Waiting for timing events...");
S.Wait_For_Event;
Put_Line ("[Reactor] Got event #1");
S.Wait_For_Event;
Put_Line ("[Reactor] Got event #2");
Put_Line ("[Reactor] Done");
end Reactor;
begin
Put_Line ("=== Timing Events Demo ===");
Put_Line ("Scheduling two timers at +100ms and +250ms...");
Set_Handler (Timer_1, Clock + Milliseconds (100), S.Fire'Access);
Set_Handler (Timer_2, Clock + Milliseconds (250), S.Fire'Access);
delay until Clock + Milliseconds (500);
Put_Line ("Main: done");
end Timing_Events_Demo;
타이밍 이벤트의 동작:
1. Set_Handler(Timer_1, T+100ms, S.Fire'Access) ── 절대 시각에 핸들러를 등록
2. T+100ms 경과 ── 런타임이 S.Fire를 **실링 우선순위로** 호출
3. Fire가 Fired 플래그를 True로 설정 ── 배리어가 열림
4. Reactor 태스크가 Wait_For_Event에서 기상
중요한 것은, 이 예제에서 Ceiling_Locking을 명시하고 있으며 Fire 핸들러가 보호 객체의 프로시저이기 때문에 실링 우선순위로 실행된다는 점입니다. 타이밍 이벤트의 핸들러로 사용하는 보호 프로시저는 인터럽트 수준의 천장 우선순위, 여기서는 System.Interrupt_Priority'Last를 가진 보호 객체에 둡니다. 이를 통해 타이밍 이벤트 처리 중에 우선순위 역전이 발생하지 않습니다.
8. 보호 객체를 이용한 실시간 큐
실시간 시스템에서 자주 나타나는 패턴으로 프로듀서·컨슈머가 있습니다. 센서가 데이터를 생성하고 제어 태스크가 이를 소비합니다──이때 버퍼의 배타 제어와 블로킹을 효율적으로 수행해야 합니다.
Ada의 보호 객체와 엔트리 배리어를 사용하면 배리어 기반 동기화로 구현할 수 있습니다. 내부적으로는 런타임이 상호 배제를 관리하므로 애플리케이션 코드에 뮤텍스나 조건 변수를 직접 작성할 필요가 없습니다.
-- 06_protected_queue.ada
-- 보호 객체를 이용한 실시간 데이터 공유
-- 파이프라인: Producer -> Bounded_Buffer -> Consumer
-- 컴파일 시 gnat.adc에서 pragma Locking_Policy (Ceiling_Locking); 를 지정한다
pragma Locking_Policy (Ceiling_Locking);
with Ada.Text_IO; use Ada.Text_IO;
with System; use System;
with Ada.Real_Time; use Ada.Real_Time;
procedure Protected_Queue_Demo is
Buffer_Size : constant := 4;
type Buf_Array is array (1 .. Buffer_Size) of Integer;
protected Bounded_Buffer is
pragma Priority (System.Any_Priority'Last);
entry Put (Item : Integer);
entry Get (Item : out Integer);
private
Buf : Buf_Array;
Count : Natural := 0;
Head : Positive := 1;
Tail : Positive := 1;
end Bounded_Buffer;
protected body Bounded_Buffer is
entry Put (Item : Integer) when Count < Buffer_Size is
begin
Buf (Tail) := Item;
Tail := (Tail mod Buffer_Size) + 1;
Count := Count + 1;
end Put;
entry Get (Item : out Integer) when Count > 0 is
begin
Item := Buf (Head);
Head := (Head mod Buffer_Size) + 1;
Count := Count - 1;
end Get;
end Bounded_Buffer;
task Producer is
pragma Priority (System.Default_Priority + 2);
pragma Storage_Size (4 * 1024);
end Producer;
task Consumer is
pragma Priority (System.Default_Priority + 1);
pragma Storage_Size (4 * 1024);
end Consumer;
task body Producer is
Next_Release : Time := Clock + Milliseconds (50);
Period : constant Time_Span := Milliseconds (50);
begin
for I in 1 .. 6 loop
Bounded_Buffer.Put (I);
Put_Line ("[Producer] Put" & Integer'Image (I));
delay until Next_Release;
Next_Release := Next_Release + Period;
end loop;
Put_Line ("[Producer] Done");
end Producer;
task body Consumer is
Item : Integer;
Next_Release : Time := Clock + Milliseconds (80);
Period : constant Time_Span := Milliseconds (80);
begin
delay until Clock + Milliseconds (30);
for I in 1 .. 6 loop
Bounded_Buffer.Get (Item);
Put_Line ("[Consumer] Got" & Integer'Image (Item));
delay until Next_Release;
Next_Release := Next_Release + Period;
end loop;
Put_Line ("[Consumer] Done");
end Consumer;
begin
Put_Line ("=== Protected Queue Demo (Ceiling_Locking) ===");
Put_Line ("Buffer size = 4; Producer every 50ms, Consumer every 80ms");
delay until Clock + Milliseconds (800);
Put_Line ("Main: done");
end Protected_Queue_Demo;
설계의 요점:
entry Put when Count < Buffer_Size── 버퍼가 가득 차면 Producer는 자동으로 블록된다.entry Get when Count > 0── 버퍼가 비어 있으면 Consumer는 자동으로 블록된다.pragma Priority (System.Any_Priority'Last)── 실링 락킹에 의해 Producer와 Consumer 사이에서 우선순위 역전이 발생하지 않는다.- 배리어 조건은 보호 객체의 내부 상태(
Count)로 정의되며, 락 해제 시 자동으로 재평가된다.
이 코드에는 애플리케이션 측의 뮤텍스, 세마포어, 조건 변수가 등장하지 않습니다. 필요한 대기는 보호 객체의 엔트리 배리어로 표현합니다.
stateDiagram-v2
Empty: 비어 있음 / Count=0
Partial: 일부 있음 / Count=1..Buffer_Size-1
Full: 가득 참 / Count=Buffer_Size
[*] --> Empty: 초기 상태
Empty --> Partial: Put (요소 1개 추가)
Partial --> Partial: Put / Get
Partial --> Empty: Get (마지막 요소를 꺼냄)
Partial --> Full: Put (마지막 빈 자리를 채움)
Full --> Partial: Get (빈 자리가 생김)
Empty --> Empty: Get은 블록 (배리어 Count=0)
Full --> Full: Put은 블록 (배리어 Count=Buffer_Size)
Put이 성공하면 Get 대기 배리어가, Get이 성공하면 Put 대기 배리어가 재평가됩니다. 이는 그림의 어느 상태에 있는지와 관계없이 보호 조작이 완료될 때 이루어집니다.
9. 실행 시간 계측 ── 실행 시간 모니터링의 첫걸음
실시간 시스템의 스케줄 가능성을 평가하려면 각 태스크의 실행 시간(CPU 시간)을 정확히 알아야 합니다. Ada의 Ada.Execution_Time 패키지는 태스크 단위의 CPU 소비 시간을 제공합니다.
-- 07_execution_time.ada
-- 실행 시간 제어 (Execution_Time)
-- 태스크별 CPU 소비 시간을 계측한다
with Ada.Text_IO; use Ada.Text_IO;
with System; use System;
with Ada.Real_Time; use Ada.Real_Time;
with Ada.Execution_Time;
use type Ada.Execution_Time.CPU_Time;
procedure Execution_Time_Demo is
package ET renames Ada.Execution_Time;
task Busy_Worker is
pragma Priority (System.Default_Priority + 1);
pragma Storage_Size (4 * 1024);
end Busy_Worker;
task body Busy_Worker is
Wall_Start : Time;
Cpu_Start : ET.CPU_Time;
Dummy : Integer := 0;
pragma Volatile (Dummy);
begin
Wall_Start := Clock;
Cpu_Start := ET.Clock;
Put_Line ("[Worker] Starting compute-bound work...");
for I in 1 .. 20_000_000 loop
Dummy := Dummy + 1;
end loop;
Put_Line ("[Worker] Dummy =" & Integer'Image (Dummy));
declare
Wall_Elapsed : constant Duration :=
To_Duration (Clock - Wall_Start);
Cpu_Span : constant Time_Span :=
ET.Clock - Cpu_Start;
begin
Put_Line ("[Worker] Done, wall time:" &
Duration'Image (Wall_Elapsed) & "s");
Put_Line ("[Worker] CPU time consumed:" &
Duration'Image (To_Duration (Cpu_Span)) & "s");
end;
end Busy_Worker;
Cpu_Start_Main : constant ET.CPU_Time := ET.Clock;
begin
Put_Line ("=== Execution Time Demo ===");
delay until Clock + Milliseconds (500);
declare
Cpu_Span : constant Time_Span := ET.Clock - Cpu_Start_Main;
begin
Put_Line ("Main: CPU time consumed after 500ms:" &
Duration'Image (To_Duration (Cpu_Span)) & "s");
end;
Put_Line ("Main: done");
end Execution_Time_Demo;
벽시계 시간 vs CPU 시간:
벽시계 시간 (Wall Clock): Ada.Real_Time.Clock
→ 실제 경과 시간. 블록 중이나 선점 중도 포함.
CPU 시간 (Execution Time): Ada.Execution_Time.Clock
→ 해당 태스크가 CPU상에서 실제로 실행된 시간만.
→ 블록 중・선점 중에는 카운트되지 않음.
이 구별은 실행 시간 모니터링과 WCET 검증의 출발점이 됩니다. Busy_Worker가 delay until로 대기하는 동안에는 CPU 시간이 증가하지 않고, 실제 계산 처리 중에만 증가합니다. 메인 태스크의 delay until Clock + Milliseconds(500) 동안에도 CPU 시간은 거의 0에 가까울 것입니다. 다만 CPU 시간의 실측이 진정한 WCET를 보증하는 것은 아닙니다. 캐시, 파이프라인, 메모리 경쟁 등을 포함한 WCET에는 별도로 정적 분석이나 대상 환경에서의 검증이 필요합니다.
flowchart LR
subgraph Wall[벽시계 시간]
W1[경과 시간 합계: 500ms] --> W2[내역: 계산 + 대기 + 블록 + 선점]
end
subgraph CPU[CPU 시간]
C1[CPU 시간 합계: 120ms] --> C2[내역: 실제 계산만]
end
Wall --> Diff[차이 = 대기・블록・선점 시간]
CPU --> Diff
Diff --> Insight[CPU 시간은 실제 계산 비용을 관측함<br/>WCET 검증・모니터링의 보조가 됨<br/>대기・블록・선점 시간을 제외함]
Insight --> Caveat[주의<br/>실측은 진정한 WCET를 보증하지 않음<br/>정적 분석이나 대상 환경 검증이 필요]
10. 종합 데모 ── 다중 주기 실시간 시스템
지금까지 배운 모든 요소──우선순위, Ceiling_Locking, delay until, 보호 객체──를 통합하여 전형적인 다중 주기 실시간 시스템을 구축합니다.
-- 08_multiperiodic.ada
-- 다중 주기 실시간 시스템 통합 데모
-- 빠른 주기(100ms)의 센서 읽기 태스크
-- 느린 주기(400ms)의 제어 태스크
-- Ceiling_Locking을 통한 데이터 공유
pragma Locking_Policy (Ceiling_Locking);
with Ada.Text_IO; use Ada.Text_IO;
with System; use System;
with Ada.Real_Time; use Ada.Real_Time;
procedure Multiperiodic_Demo is
package Int_IO is new Ada.Text_IO.Integer_IO (Integer);
protected Shared_Sensor is
pragma Priority (System.Any_Priority'Last);
procedure Write (V : Integer);
function Read return Integer;
private
Value : Integer := 0;
end Shared_Sensor;
protected body Shared_Sensor is
procedure Write (V : Integer) is
begin
Value := V;
end Write;
function Read return Integer is
begin
return Value;
end Read;
end Shared_Sensor;
task Fast_Sensor is
pragma Priority (System.Default_Priority + 3);
pragma Storage_Size (4 * 1024);
end Fast_Sensor;
task body Fast_Sensor is
Next_Release : Time := Clock + Milliseconds (100);
Period : constant Time_Span := Milliseconds (100);
Cycle : Natural := 0;
begin
Put_Line ("[Fast] Sensor reader starts (100ms period)");
for I in 1 .. 12 loop
delay until Next_Release;
Cycle := Cycle + 1;
Shared_Sensor.Write (Cycle * 10);
Next_Release := Next_Release + Period;
end loop;
Put_Line ("[Fast] Done");
end Fast_Sensor;
task Slow_Controller is
pragma Priority (System.Default_Priority + 2);
pragma Storage_Size (4 * 1024);
end Slow_Controller;
task body Slow_Controller is
Next_Release : Time := Clock + Milliseconds (150);
Period : constant Time_Span := Milliseconds (400);
Cycle : Natural := 0;
Raw : Integer;
begin
Put_Line ("[Slow] Controller starts (400ms period)");
for I in 1 .. 3 loop
delay until Next_Release;
Cycle := Cycle + 1;
Raw := Shared_Sensor.Read;
Put_Line ("[Slow] Cycle" & Natural'Image (Cycle) &
" reads sensor =" & Integer'Image (Raw));
Next_Release := Next_Release + Period;
end loop;
Put_Line ("[Slow] Done");
end Slow_Controller;
begin
Put_Line ("=== Multiperiodic Real-Time System Demo ===");
Put_Line ("Fast sensor (100ms) x 12 + Slow controller (400ms) x 3");
Put_Line ("Ceiling_Locking prevents priority inversion on shared data");
delay until Clock + Milliseconds (2000);
Put_Line ("Main: done");
end Multiperiodic_Demo;
시스템 구성:
아래 그림은 샘플 코드의 release 시각(고속 센서는 100ms 주기, 저속 제어는 150ms 오프셋의 400ms 주기)에 설명용 실행 시간을 가정한 스케줄 예시입니다. 코드 자체에는 80ms의 제어 계산이 들어 있지 않으므로 실측 그림이 아닙니다. 고속 센서의 우선순위가 높은 경우, 저속 제어 실행 중에 고속 센서의 release가 오면 저속 제어는 일시 중단됩니다. 그림에서는 보기 쉽게 각 저속 제어 주기마다 대표적인 중단을 1회만 그렸지만, 실제로는 100ms 경계마다 고속 센서가 release됩니다.
flowchart TB
Assumption["설명용 가정<br/>고속 센서: 10ms 처리<br/>저속 제어: 80ms 처리"]
subgraph Cycle1["저속 제어 주기1(release=150ms)"]
direction LR
C1F1["100-110ms<br/>고속 센서 #1"] --> C1S1["150-200ms<br/>저속 제어 #1 전반"]
C1S1 --> C1F2["200-210ms<br/>고속 센서 #2<br/>P+3이므로 중단"]
C1F2 --> C1S2["210-240ms<br/>저속 제어 #1 후반"]
end
subgraph Cycle2["저속 제어 주기2(release=550ms)"]
direction LR
C2S1["550-600ms<br/>저속 제어 #2 전반"] --> C2F6["600-610ms<br/>고속 센서 #6<br/>P+3이므로 중단"]
C2F6 --> C2S2["610-640ms<br/>저속 제어 #2 후반"]
end
subgraph Cycle3["저속 제어 주기3(release=950ms)"]
direction LR
C3S1["950-1000ms<br/>저속 제어 #3 전반"] --> C3F10["1000-1010ms<br/>고속 센서 #10<br/>P+3이므로 중단"]
C3F10 --> C3S2["1010-1040ms<br/>저속 제어 #3 후반"]
end
Assumption --> C1F1
C1S2 --> C2S1
C2S2 --> C3S1
이 패턴은 산업용 제어 시스템이나 로봇 제어에서 자주 볼 수 있는 「고속 센서 수집 + 저속 제어 루프」의 전형적인 구조입니다.
11. Ada 실시간 기능이 빛을 발하는 영역
Ada의 실시간 기능은 다음과 같은 분야에서 특히 그 가치를 발휘합니다.
flowchart TB
Ada[Ada Annex D<br/>실시간 기능] --> Aero[항공우주<br/>DO-178C]
Ada --> Rail[철도<br/>EN 50128 계열]
Ada --> Auto[자동차<br/>ISO 26262]
Ada --> Medical[의료기기<br/>IEC 62304]
Ada --> Industrial[산업 제어<br/>IEC 61508 계열]
Ada --> Defense[국방・고신뢰성 시스템]
Aero --> A1[비행 제어<br/>적용 사례가 많은 영역]
Aero --> A2[위성・우주기 제어]
Rail --> R1[신호 시스템]
Rail --> R2[자동 열차 제어]
Auto --> Au1[안전 관련 ECU의 후보]
Auto --> Au2[C / MISRA-C가 주류인 영역에서의<br/>한정적・선택적 적용]
Medical --> M1[심장 박동기]
Medical --> M2[주입 펌프]
Industrial --> I1[로봇 제어]
Industrial --> I2[NC 공작 기계]
Defense --> D1[미션 컴퓨터]
Defense --> D2[장기 운용 시스템]
12. 주의점과 한계
Ada의 실시간 기능은 강력하지만 만능은 아닙니다.
1. 플랫폼 의존성:
pragma Priority의 실제 매핑은 실행 환경(OS + GNAT 런타임)에 의존합니다. Linux에서는SCHED_FIFO로 매핑되지만, Windows에서는 완전한 선점이 보장되지 않는 경우가 있습니다.
2. Ravenscar의 제약:
- 동적 태스크 생성이 금지되어 있으므로 시스템 기동 시에 모든 태스크를 정적으로 선언해야 합니다. 이는 설계의 자유도를 제한합니다.
3. WCET 측정의 한계:
Ada.Execution_Time은 측정일 뿐 보증이 아닙니다. 캐시 미스나 파이프라인 해저드를 포함한 진정한 WCET는 정적 분석 도구로 별도로 검증해야 합니다.
4. 오버헤드:
- 보호 객체의 배리어 평가는 엔트리의 완료・취소 시, 그리고 보호 객체에서 나갈 때 자동으로 실행됩니다. 높은 빈도로 호출되는 보호 객체에서는 이 오버헤드를 고려해야 합니다.
5. 툴체인의 장벽:
- Ada의 실시간 기능을 완전히 활용하려면 적절한 크로스 컴파일러와 런타임이 필요합니다. 특히 임베디드 타깃에서는 벤더가 제공하는 런타임에 의존하게 됩니다.
13. 정리
이 글에서는 Ada의 Annex D가 제공하는 실시간 기능을 8개의 코드 예제로 단계적으로 살펴보았습니다.
| 기능 | 제공하는 가치 |
|---|---|
| 태스크 우선순위 | 선점형 우선순위 기반 스케줄링 |
| Ceiling_Locking | 언어에 내장된 우선순위 역전 방지 |
delay until |
누적 드리프트를 방지하는 주기 실행 |
| Ravenscar 프로파일 | 정적 분석을 수행하기 쉬운 태스크 서브셋 |
| 타이밍 이벤트 | 폴링이 필요 없는 시각 기반 기상 |
| 보호 큐 | 보호 객체의 배리어 기반 동기화 |
| 실행 시간 계측 | 태스크 단위의 CPU 시간 모니터링 |
| 다중 주기 통합 | 서로 다른 주기의 태스크를 안전하게 공존시키는 설계 |
Ada의 실시간 기능이 지닌 본질은 「후속으로 덧붙인 것이 아니다」라는 점입니다. 우선순위 역전을 억제하는 락 규칙, 주기 실행을 위한 시각 지정, 실행 시간 모니터링 등이 언어 규격의 일부로 제공됩니다. 물론 데드라인 달성 그 자체는 설계와 분석으로 확인해야 하지만, 그것을 위한 전제를 언어 런타임이 갖춰 준다는 점이 큰 강점입니다.
mindmap
root((Ada Annex D<br/>실시간 시스템))
스케줄링
FIFO_Within_Priorities
태스크 우선순위
선점
우선순위 역전 방지
Ceiling_Locking 프로토콜
자동 우선순위 승격
천장 우선순위 규칙
주기 실행
delay until
누적 드리프트 방지
절대 시각 기준
Ravenscar 프로파일
정적 태스크 집합
결정론적 분석
상대 delay 금지
DO-178C / ISO 26262
타이밍 이벤트
폴링 불필요한 기상
보호 핸들러 등록
핸들러는 천장 우선순위로 실행
보호 객체/큐
엔트리 배리어
비어 있음/일부/가득 참 상태 관리
배리어 기반 동기화
실행 시간 모니터링
태스크별 CPU 시간
벽시계 vs CPU 시간
WCET 검증・모니터링의 보조
통합 설계
다중 주기 설계
배리어를 통한 보호
언어 수준의 안전성
다음 단계로 Ada를 이용한 실시간 시스템 개발을 실제로 시도해 보려면, Alire로 GNAT 툴체인을 설치하고, 이 글의 샘플 코드를 gnatchop + gnatmake로 빌드해 보시기 바랍니다.
또한 Ada 동시성 처리의 기본(태스크, 랑데부, 보호 객체)에 대해서는 이전 글 「Ada의 안전한 동시성 처리」를 참조하시기 바랍니다.
14. 참고 자료
관련 기사
같은 태그를 공유하는 최신 기사입니다. 더 가까운 주제로 지식을 넓힐 수 있습니다.
Ada의 안전한 동시성 처리 ── 태스크와 보호 객체 실전 가이드
Ada의 언어 내장 동시성 처리인 태스크와 보호 객체를 다루는 실전 입문 글입니다. 랑데부(entry/accept), 선택적 accept, 생산자·소비자, 보호 객체를 통한 배타 제어, 배리어가 있는 보호 엔트리, 타임아웃이 있는 호출, 태스크 ...
Ada 제네릭 프로그래밍 ── 타입으로 계약을 작성하고 재사용을 제로 코스트로 실현하기
Ada의 제네릭 프로그래밍을 총칭 서브프로그램, 총칭 패키지, 가인수 서브프로그램, 타입 카테고리, 정식 패키지 파라미터, 실무 설계 지침까지 체계적으로 설명합니다. Mermaid 다이어그램과 실제 예제를 곁들여 타입 안전한 재사용과 제로 코스트...
SPARK를 이용한 형식 검증 입문 —— Ada의 계약에서 수학적 증명으로
Ada의 서브셋 언어인 SPARK를 이용한 형식 검증의 실전 입문 글입니다. 계약(Pre/Post)에서 증명으로 단계를 밟아가는 방법, GNATprove 사용법, 루프 불변조건, 데이터 흐름 계약, 증명 레벨, 그리고 실제 프로젝트 적용 방법까지...
Ada 언어의 매력 —— 타입으로 설계를 말하고, 수십 년간 동작하는 소프트웨어를 지탱하는 언어
Ada 언어의 매력을 소개합니다. 강한 타입, 범위 제약, 패키지를 통한 사양과 구현의 분리, 계약에 의한 설계, 언어에 내장된 태스크, SPARK를 통한 정형 검증, GNAT와 Alire를 이용한 개발 환경까지, 고신뢰 소프트웨어를 지탱하는 설...
Windows의 「프로세서 스케줄링」을 「백그라운드 서비스」로 바꾸면 무슨 일이 일어나는가 - quantum, 우선도 부스트, P 코어 / E 코어까지 정리
Windows의 프로세서 스케줄링을 백그라운드 서비스로 바꾸면 quantum 배분과 foreground 우대가 어떻게 달라지는지, 음성 드롭아웃과의 관계, 그리고 P 코어와 E 코어 시대에 QoS와 hybrid scheduling이 차지하는 역할...
관련 토픽
이 기사와 가까운 토픽 페이지입니다. 기사를 출발점 삼아 관련 서비스와 다른 기사로 이어집니다.
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로 감지하고 과부하로 처리하는 설계를 별도로 마련해야 합니다.
저자 프로필
기사 저자의 프로필 페이지입니다.
Go Komura
합동회사 코무라소프트 대표
Windows 소프트웨어 개발, 기술 상담, 장애 조사를 중심으로 재현이 어려운 장애 조사와 기존 자산이 남아 있는 프로젝트에 강점이 있습니다.
공개 링크