임베디드 · 심화
인터럽트·RTOS·실시간 설계
실시간 분석 - 마감 시간 지키기
WCET, RMS 스케줄 가능성 검사, 지터 원인
개발자KR · 원고 갱신
이 장에서 배우는 것
컨베이어 제어기는 계산 결과뿐 아니라 결과를 내놓는 시각도 맞아야 한다. 분류 게이트를 정확한 방향으로 움직이더라도 제품이 지나간 뒤라면 제어에 실패한 것이다. 앞 장에서 공유 자원을 안전하게 다루는 방법을 살펴보았다. 이제 태스크가 서로 기다리고 선점되는 시간을 포함해, 정해진 마감 시간까지 일을 끝낼 수 있는지 판단한다.
이 장에서는 실행 시간에 상한을 두고, 주기가 짧은 태스크에 높은 우선순위를 주었을 때의 스케줄 가능성을 계산한다. PC 예제는 운영체제의 실제 시간을 재는 대신 논리 시각을 진행한다. 따라서 실행 결과를 반복해서 확인할 수 있으며, 분석에 사용한 가정과 실제 장치에서 검증해야 할 항목도 구분할 수 있다.
- 최악 실행 시간(Worst-Case Execution Time, WCET)과 응답 시간을 구별한다.
- 주기 단조 스케줄링(Rate Monotonic Scheduling, RMS)의 이용률 검사와 응답 시간 분석을 적용한다.
- 지터(jitter)를 발생 위치와 측정 기준에 따라 나눈다.
- 컨베이어 태스크의 실행 기록을 계산 결과와 비교하고, PC 시뮬레이션의 한계를 설명한다.
문제 상황
공장 컨베이어에 세 가지 주기 작업이 있다고 하자. 게이트 제어는 제품 위치를 확인하고 출력 명령을 갱신한다. 속도 제어는 센서 값을 바탕으로 구동 출력을 계산한다. 기록 작업은 최근 상태를 고정 길이 버퍼에 정리한다. 여기서 기록은 저장 장치의 쓰기 완료를 기다리는 작업이 아니라, CPU에서 수행하는 메모리 내 정리 작업이다.
개별 시험에서는 모두 빨리 끝났다. 그러나 제품이 연달아 들어오자 기록 작업이 실행 중인 순간에도 게이트 작업의 시작 시각이 돌아왔다. 평균 CPU 사용률만 보고 여유가 있다고 판단했지만, 게이트 명령이 늦게 반영되는 경우가 생겼다. 평균 사용률은 특정 작업이 마감 전에 끝나는지를 직접 말해 주지 않는다.
먼저 시간 계약을 수치로 정한다. 아래 표의 실행 시간은 실제 장치에서 이미 검증되었다고 가정한 예산이다. 예제에서는 논리 시간 한 칸을 1ms로 해석한다. 실제 PC에서 프로그램이 1ms마다 실행된다는 뜻은 아니다.
| 태스크 | 실행 예산 C | 주기 T | 상대 마감 D |
|---|---|---|---|
| gate: 게이트 제어 | 1ms | 4ms | 4ms |
| speed: 속도 제어 | 2ms | 8ms | 8ms |
| log: 기록 정리 | 2ms | 16ms | 16ms |
상대 마감은 작업이 실행 가능해진 시점부터 허용되는 시간이다. 예를 들어 16ms에 준비된 기록 작업의 절대 마감 시각은 32ms다. 이 예제에서는 모든 태스크의 상대 마감이 주기와 같다. 다른 마감 조건에 같은 검사를 그대로 적용하지 않도록 이 가정을 먼저 적어 둔다.
실행 시간과 응답 시간의 경계
주기마다 생기는 한 번의 실행 요청을 작업이라고 부른다. 작업이 실행 가능한 상태가 되는 사건은 릴리스(release)다. 실행 시간 C는 그 작업이 CPU를 차지해 실제로 실행한 시간이다. 응답 시간 R은 릴리스부터 완료까지의 경과 시간이다. 응답 시간에는 더 높은 우선순위 작업 때문에 기다린 시간도 들어간다.
WCET는 정해진 하드웨어와 소프트웨어 조건에서 작업이 소비할 수 있는 실행 시간의 상한이다. 여러 번 실행해서 얻은 최댓값은 관측 최댓값이며, 그것만으로 모든 경로의 상한이 입증되지는 않는다. 오류 처리 분기, 반복문의 최대 반복 횟수, 주변장치 접근 지연, 메모리 대기 상태를 빠뜨리면 실행 예산이 실제보다 작아진다.
측정할 때에는 최대 길이 입력, 센서 경계값, 통신 오류 등 긴 경로를 유도하는 조건을 넣는다. MCU의 사이클 계수기나 GPIO 파형으로 시간을 확인할 수 있다. 다만 다른 인터럽트가 끼어든 경과 시간을 그대로 C로 넣고, 분석에서도 같은 인터럽트 간섭을 더하면 중복 계산이 된다. 어떤 지연을 C에 포함했는지 기록해야 한다.
빌드 최적화, CPU 주파수, 플래시 대기 설정이 바뀌면 기존 측정의 전제도 바뀐다. 측정값에 임의의 비율을 더하는 방법은 예산을 잡는 보조 수단이지만 상한의 증명은 아니다. PC에서 잰 함수 실행 시간 역시 MCU의 WCET로 옮겨 쓸 수 없다.
그림에서 기록 작업은 0ms에 준비되지만 3ms에 처음 실행된다. 4ms에는 게이트 작업에 선점되고, 5ms에 재개하여 6ms에 끝난다. CPU를 사용한 시간은 2ms이고 응답 시간은 6ms다. 완료 시각에서 시작 시각만 빼면 최초 대기를 놓치므로, 응답 시간을 계산할 때에는 릴리스 시각을 보관해야 한다.
RMS로 마감 준수를 검사하기
RMS는 주기가 짧을수록 높은 고정 우선순위를 부여한다. 이 예제의 순서는 gate, speed, log다. 이름이나 코드 길이로 우선순위를 정하지 않는다. 다음 계산은 단일 CPU, 선점 가능 실행, 독립적인 주기 작업, D=T, 실행 중 자발적인 대기 없음이라는 조건을 사용한다. 문맥 전환과 스케줄러 비용은 일단 0으로 둔다.
이용률로 빠르게 통과시키기
CPU 이용률 U는 각 태스크의 C/T를 더한 값이다. 태스크 수가 n일 때 아래 경계 이하이면, 앞의 조건을 만족하는 RMS 태스크 집합은 스케줄 가능하다. 이 경계는 충분조건이다. 경계를 넘었다고 곧바로 실패라고 판단하지 않는다.
U = Σ(Ci / Ti)
충분조건: U ≤ n × (2^(1/n) - 1)
U = 1/4 + 2/8 + 2/16 = 0.625
n = 3일 때 경계 ≈ 0.779763
0.625 ≤ 0.779763이므로 충분조건을 만족한다.
경계의 성격은 작은 반례로 확인할 수 있다. C=1, T=2인 태스크와 C=2, T=4인 태스크를 함께 실행하면 이용률은 1이다. 두 태스크의 일반적인 충분조건 경계는 약 0.828이지만, 높은 우선순위 작업 사이에 낮은 우선순위 작업이 나뉘어 실행되며 4에 완료한다. 이론 모델에서는 마감을 지킨다. 다만 실제 비용을 추가할 여유는 없다.
응답 시간으로 개별 마감 확인하기
응답 시간 분석(Response-Time Analysis, RTA)은 높은 우선순위 작업의 간섭을 반복해서 더한다. hp(i)는 태스크 i보다 우선순위가 높은 태스크의 집합이다. ceil은 나눗셈 결과를 올림한다. 릴리스 시점부터 응답 시간 후보 구간 안에 높은 우선순위 작업이 몇 번 들어올 수 있는지 세기 위해 사용한다.
R(0) = Ci
R(k+1) = Ci + Σ[j ∈ hp(i)] ceil(R(k) / Tj) × Cj
값이 더 이상 바뀌지 않고 R ≤ Di이면 통과한다.
반복 중 R > Di이면 이 모델의 검사를 통과하지 못한다.
gate는 간섭을 받지 않으므로 R=1이다. speed는 2에서 시작하여 2+ceil(2/4)×1=3이 되고, 다시 계산해도 3이다. log는 2에서 시작해 5, 6, 6으로 수렴한다. 응답 시간이 길어지면서 그 안에 들어오는 gate 실행 횟수가 한 번에서 두 번으로 늘어나는 점이 핵심이다.
여기서는 모든 작업이 동시에 준비되는 경우가 각 태스크의 최악 응답을 만드는 조건에 해당한다. 실행 시간이 매번 예산만큼 소비되는 동시 릴리스부터 검사하면 가장 큰 간섭을 확인할 수 있다. 임의의 오프셋, 릴리스 지연, 자원 대기가 있는 시스템에 이 결론을 그대로 확대하지 않는다.
앞 장에서 다룬 뮤텍스 때문에 낮은 우선순위 태스크를 기다린다면 블로킹 상한 B를 별도로 구해야 한다. 적절한 자원 접근 규약으로 그 상한을 얻었다면 식의 기본 항을 C+B로 확장한다. 우선순위 상속은 대기 시간을 없애는 기능이 아니다. 인터럽트 처리와 실행 중 입출력 대기도 모델에 맞게 추가해야 한다.
지터는 어느 시각의 흔들림인가
지터는 기준 시각에 대한 편차가 실행마다 달라지는 현상이다. 여기서는 주기마다 같은 단계에서 측정한 지연의 최댓값과 최솟값 차이를 지터 폭으로 정의한다. 최대 지연 자체와 구별하기 위해, 결과에는 지연 범위와 그 차이를 함께 표시한다.
계획된 릴리스 시각과 실제 릴리스 시각 사이에는 릴리스 지연이 생길 수 있다. 타이머 처리 지연, 긴 인터럽트 마스킹 구간, 틱 해상도가 그 원인이다. 실제 릴리스부터 첫 실행까지의 시작 지연은 높은 우선순위 작업의 간섭이나 비선점 구간 때문에 생긴다. 실행 경로가 달라지면 완료 시점의 편차도 달라진다.
예를 들어 시작 지연이 매번 3ms라면 지터 폭은 0ms지만 시작이 빠른 것은 아니다. 지연이 1ms, 3ms, 2ms라면 지터 폭은 2ms다. 이 값만으로 어느 경우가 마감을 지키는지 결정할 수는 없다. 완료까지의 응답 시간도 함께 검사해야 한다.
일을 끝낸 뒤 일정 시간을 쉬는 방식은 실행 시간이 다음 주기의 기준점에 누적된다. 예정 시각을 기준으로 다음 릴리스를 잡으면 이런 누적을 줄일 수 있다. 그러나 높은 우선순위 간섭까지 사라지는 것은 아니다. 시작의 규칙성과 마감 준수는 서로 관련되지만 별도로 확인할 항목이다.
| 예제의 개념 | FreeRTOS 대응 | 적용할 때 확인할 점 |
|---|---|---|
| 주기가 짧은 순서의 배열 | xTaskCreate의 우선순위 인자 | 큰 숫자가 높은 우선순위다. RMS 순서를 직접 지정한다. |
| 예정된 주기마다 작업 준비 | xTaskDelayUntil | 이전 예정 깨움 시각을 기준으로 한다. 틱 변환과 반환값을 확인한다. |
| 논리 시각 읽기 | xTaskGetTickCount | 틱 해상도보다 짧은 실행 시간 측정에는 별도 계수기가 필요하다. |
| 한 칸 뒤 우선순위 재선택 | 선점형 스케줄러 | configUSE_PREEMPTION 설정을 확인한다. 아래 코드는 커널 구현이 아니다. |
API의 사실 관계는 FreeRTOS 상대 지연 API 설명과 주기 지연 API 설명에서 확인할 수 있다. 상대 지연과 예정 시각 기준 지연의 차이를 실제 포팅 때 점검한다.
완성 코드
아래 프로그램을 realtime.c로 저장한다. hal_sim은 논리 시각만 관리하며 운영체제의 대기 함수를 사용하지 않는다. 협동형 실행기는 한 번 호출될 때 작업을 한 칸 진행하고 제어를 돌려준다. 각 칸의 시작에서 가장 높은 우선순위의 준비 작업을 다시 고른다.
이 예제에서는 모든 릴리스와 실행량이 정수 칸에 맞춰져 있다. 따라서 한 칸마다 양보하는 실행으로도 선점형 RMS의 시간표를 재현할 수 있다. 실제 태스크 함수 전체를 끝까지 호출하는 협동형 스케줄러에는 이 분석을 그대로 적용할 수 없다. 예제의 한 칸은 CPU 실행 예산을 소비한다는 모델이며, 실제 제어 연산은 수행하지 않는다.
#include <stdio.h>
enum { TASK_COUNT = 3, SIM_END = 32 };
typedef struct {
const char *name;
int c;
int t;
} Task;
typedef struct {
int remaining;
int released;
int started;
int completed;
int min_start;
int max_start;
int max_response;
} Job;
typedef struct {
int now;
} HalSim;
static const Task tasks[TASK_COUNT] = {
{ "gate", 1, 4 },
{ "speed", 2, 8 },
{ "log", 2, 16 }
};
static int hal_sim_now(const HalSim *hal)
{
return hal->now;
}
static void hal_sim_step(HalSim *hal)
{
++hal->now;
}
static int ceil_div(int value, int divisor)
{
return value / divisor + (value % divisor != 0);
}
static int response_bound(int index)
{
int r = tasks[index].c;
for (;;) {
int next = tasks[index].c;
for (int higher = 0; higher < index; ++higher) {
next += ceil_div(r, tasks[higher].t)
* tasks[higher].c;
}
if (next > tasks[index].t || next == r) {
return next;
}
r = next;
}
}
static int simulate(void)
{
HalSim hal = { 0 };
Job jobs[TASK_COUNT] = { 0 };
int busy = 0;
int idle = 0;
for (int i = 0; i < TASK_COUNT; ++i) {
jobs[i].min_start = SIM_END;
}
while (hal_sim_now(&hal) < SIM_END) {
int now = hal_sim_now(&hal);
int chosen = -1;
for (int i = 0; i < TASK_COUNT; ++i) {
if (now % tasks[i].t == 0) {
if (jobs[i].remaining != 0) {
printf("unfinished at release: %s\n",
tasks[i].name);
return 1;
}
jobs[i].remaining = tasks[i].c;
jobs[i].released = now;
jobs[i].started = 0;
}
}
for (int i = 0; i < TASK_COUNT; ++i) {
if (jobs[i].remaining > 0) {
chosen = i;
break;
}
}
if (chosen >= 0) {
Job *job = &jobs[chosen];
if (!job->started) {
int delay = now - job->released;
if (delay < job->min_start) {
job->min_start = delay;
}
if (delay > job->max_start) {
job->max_start = delay;
}
job->started = 1;
}
--job->remaining;
++busy;
} else {
++idle;
}
hal_sim_step(&hal);
now = hal_sim_now(&hal);
if (chosen >= 0 && jobs[chosen].remaining == 0) {
Job *job = &jobs[chosen];
int response = now - job->released;
++job->completed;
if (response > job->max_response) {
job->max_response = response;
}
if (response > tasks[chosen].t) {
printf("deadline miss: %s at %d\n",
tasks[chosen].name, now);
return 1;
}
}
for (int i = 0; i < TASK_COUNT; ++i) {
if (jobs[i].remaining > 0
&& now >= jobs[i].released + tasks[i].t) {
printf("deadline miss: %s at %d\n",
tasks[i].name, now);
return 1;
}
}
}
printf("sim horizon=%d busy=%d idle=%d misses=0\n",
SIM_END, busy, idle);
for (int i = 0; i < TASK_COUNT; ++i) {
const Job *job = &jobs[i];
printf("%s jobs=%d max_response=%d "
"start_delay=[%d,%d] jitter=%d\n",
tasks[i].name, job->completed,
job->max_response,
job->min_start, job->max_start,
job->max_start - job->min_start);
}
return 0;
}
int main(void)
{
const double rms_bound_three = 0.7797631496846196;
double utilization = 0.0;
int schedulable = 1;
for (int i = 0; i < TASK_COUNT; ++i) {
utilization += (double)tasks[i].c / tasks[i].t;
}
printf("RMS U=%.3f bound=%.3f sufficient=%s\n",
utilization, rms_bound_three,
utilization <= rms_bound_three ? "yes" : "no");
for (int i = 0; i < TASK_COUNT; ++i) {
int r = response_bound(i);
int pass = r <= tasks[i].t;
printf("RTA %s R=%d D=%d %s\n",
tasks[i].name, r, tasks[i].t,
pass ? "pass" : "fail");
if (!pass) {
schedulable = 0;
}
}
if (!schedulable) {
return 1;
}
return simulate();
}
줄별 해설
TASK_COUNT는 세 태스크, SIM_END는 관찰 구간의 끝인 32를 뜻한다. Task는 고정된 시간 계약이고 Job은 현재 작업과 누적 통계를 담는다. D=T이므로 마감 필드를 따로 두지 않았다. 배열은 반드시 짧은 주기부터 배치한다.
HalSim의 now는 시뮬레이터가 소유하는 시각이다. hal_sim_step을 호출할 때만 한 칸 증가한다. 이 구조에서는 PC의 부하나 실행 속도가 시간표를 바꾸지 않는다. 컴파일 명령에 스레드 옵션을 넣지만 프로그램 자체는 단일 스레드로 실행된다.
ceil_div는 몫에 나머지의 존재 여부를 더한다. 정수 나눗셈만 사용해 올림을 계산하며, 분자에 제수를 더하는 형태의 불필요한 덧셈을 피한다. 모든 실행 예산과 주기는 양수라는 전제가 있다.
response_bound는 자기 실행 예산에서 반복을 시작한다. 배열 앞쪽 태스크의 간섭을 더하고, 결과가 고정되거나 마감을 넘으면 반환한다. 마감을 넘겨 반환한 값은 수렴한 최악 응답 시간이 아니라 실패를 확인한 중간 값일 수 있다. 이 예제의 작은 상수에서는 정수 범위가 충분하다. 외부에서 큰 값을 받는 분석 도구로 확장한다면 곱셈과 합산의 범위 검사가 필요하다.
simulate의 첫 반복문은 시작 지연의 최솟값을 초기화한다. 관찰 구간에 모든 태스크가 실행된다는 현재 설정을 이용한다. 다음에는 현재 시각이 주기의 배수인 태스크를 모두 준비시킨다. 남은 실행량이 있는 작업을 새 작업으로 덮어쓰지 않도록 검사도 넣었다.
chosen을 고르는 반복문은 준비된 배열 원소 중 첫 번째를 선택한다. 첫 실행에서만 시작 지연을 기록하므로 선점 후 재개를 새로운 시작으로 세지 않는다. 남은 실행량을 하나 줄인 뒤 논리 시각을 증가시켜, 구간 [0, 1)에서 실행한 작업의 완료 시각이 1이 되도록 했다.
완료 작업은 응답 시간이 주기보다 큰지 검사한다. 미완료 작업은 현재 시각이 마감 이상인지 검사한다. 마감 시각에 막 완료한 작업은 성공이며, 같은 시각에 실행량이 남아 있다면 실패다. 마지막 출력의 misses=0은 모든 검사에 통과한 경로에서만 나온다.
main은 충분조건 결과와 개별 응답 시간 결과를 따로 출력한다. 충분조건이 실패해도 응답 시간 검사를 계속하며, 그 검사까지 통과해야 시뮬레이션으로 넘어간다. 이용률 경계 상수는 태스크가 세 개일 때의 값이다. 태스크 수를 바꿀 때 이 상수도 함께 바꿔야 한다.
실행 결과
macOS 또는 Linux에서 다음 명령으로 컴파일하고 실행한다. 아래 결과는 코드의 논리 시간 규칙에서 계산되는 예상 출력이다. 모든 숫자의 시간 단위는 한 칸이며, 이 장의 시간 계약에서는 1ms에 해당한다.
cc -std=c11 -Wall -Wextra -pthread realtime.c -o realtime
./realtime
RMS U=0.625 bound=0.780 sufficient=yes
RTA gate R=1 D=4 pass
RTA speed R=3 D=8 pass
RTA log R=6 D=16 pass
sim horizon=32 busy=20 idle=12 misses=0
gate jobs=8 max_response=1 start_delay=[0,0] jitter=0
speed jobs=4 max_response=3 start_delay=[1,1] jitter=0
log jobs=2 max_response=6 start_delay=[3,3] jitter=0
게이트는 여덟 번, 속도 제어는 네 번, 기록은 두 번 완료된다. CPU 사용 시간은 8×1+4×2+2×2=20칸이다. 32칸 중 사용 비율은 0.625로 이용률 계산과 일치한다. 기록 작업의 최대 응답 시간 6도 분석 결과와 같다.
시작 지터가 모두 0인 이유는 주기가 서로 배수이고 실행량과 릴리스가 고정되어 같은 시간표가 반복되기 때문이다. 실제 장치에서 지터가 없다는 뜻은 아니다. 이 시뮬레이터에는 인터럽트 지연, 캐시 상태, 입출력 대기, 문맥 전환 비용이 없다.
16칸은 세 주기의 최소공배수다. 이 설정에서는 그 경계 전에 모든 작업이 끝나 같은 상태로 돌아오므로 시간표가 반복된다. 그럼에도 일반적인 테스트에서 일정 시간 동안 마감 위반을 못 보았다는 사실만으로 모든 실행을 보장해서는 안 된다. 보장의 범위는 분석의 가정과 실행 시간 상한이 결정한다.
실무에서 자주 틀리는 것
올림 대신 정수 나눗셈을 쓰기
아래 첫 조각은 응답 시간 후보가 주기보다 짧으면 간섭을 0회로 계산한다. 이미 함께 준비된 높은 우선순위 작업 하나를 빠뜨리는 오류다. 다음 조각처럼 올림 나눗셈을 사용한다. 조각들은 완성 코드의 해당 계산을 비교한 것이다.
/* 틀린 계산 */
next += (r / tasks[higher].t) * tasks[higher].c;
/* 고친 계산 */
next += ceil_div(r, tasks[higher].t) * tasks[higher].c;
이용률 경계를 실패 판정선으로 쓰기
충분조건을 넘었다는 사실은 추가 분석이 필요하다는 뜻이다. 바로 거부하면 실행 가능한 태스크 집합까지 제외한다. 고친 조각은 출력만 하고, 완성 코드처럼 개별 응답 시간 검사를 이어간다.
/* 틀린 판정 */
if (utilization > rms_bound_three) {
return 1;
}
/* 고친 처리 */
if (utilization > rms_bound_three) {
puts("RMS bound inconclusive; continue with RTA");
}
마감 시각에 완료한 작업을 실패로 처리하기
완료한 작업의 응답 시간이 D와 같으면 마감을 지킨 것이다. 반면 완료하지 못한 작업은 마감 시각에 남은 실행량이 있으면 실패다. 완료 여부를 구분하지 않고 같은 비교 연산자를 쓰지 않는다.
/* 완료한 작업에 대한 틀린 검사 */
if (response >= tasks[chosen].t) {
return 1;
}
/* 완료한 작업에 대한 고친 검사 */
if (response > tasks[chosen].t) {
return 1;
}
최대 시작 지연을 지터라고 부르기
기록 작업은 항상 세 칸 늦게 시작하므로 최대 시작 지연은 3이고 지터 폭은 0이다. 최댓값만 기록하면 일정한 지연과 실행마다 달라지는 편차를 구분할 수 없다. 실제 로그에도 어떤 기준 시각으로 계산했는지 함께 남긴다.
/* 틀린 지표 */
printf("jitter=%d\n", job->max_start);
/* 이 장의 정의에 맞는 지표 */
printf("jitter=%d\n", job->max_start - job->min_start);
한눈에 보기
| 항목 | 의미 | 확인할 조건 |
|---|---|---|
| WCET | 작업 자체의 CPU 실행 시간 상한 | 입력 경로와 하드웨어·빌드 조건을 명시한다. |
| 응답 시간 | 릴리스부터 완료까지의 시간 | 실행 시간과 다른 작업의 간섭을 포함한다. |
| RMS 이용률 검사 | 주기 기반 고정 우선순위의 충분조건 | 경계를 넘으면 추가 분석한다. |
| 응답 시간 반복 | 간섭 횟수가 안정될 때까지 계산 | 각 태스크의 마감과 비교한다. |
| 시작 지터 폭 | 시작 지연의 최댓값과 최솟값 차이 | 큰 일정 지연도 별도로 확인한다. |
| PC 시뮬레이션 | 설정한 시간 모델의 동작 확인 | 실제 MCU의 WCET 측정을 대신하지 않는다. |
실무 기록에는 태스크별 C, T, D뿐 아니라 블로킹과 인터럽트 비용의 처리 방법도 남긴다. 실행 시간이 늘어나는 변경을 했다면 평균 사용률만 다시 보는 것이 아니라 응답 시간까지 재계산한다. 다음 장에서는 메모리 사용 방식이 실행 시간의 예측 가능성에 어떤 영향을 주는지 이어서 살펴본다.
연습 문제
- 완성 코드에서 log의 실행 예산만 7로 바꾸었다. 전체 이용률을 구하고 충분조건의 통과 여부를 판단하라. 이어서 log의 응답 시간 후보를 수렴할 때까지 계산하라.
- 세 작업에서 측정한 시작 지연이 2ms, 5ms, 3ms다. 이 장의 정의에 따른 지터 폭과 최대 시작 지연을 구하라. 이 값만으로 마감 준수를 판정할 수 있는지도 설명하라.
- 완료 기록은 없고 실행량이 한 칸 남은 작업이 있다. 릴리스 시각이 8이고 상대 마감이 8일 때, 현재 시각 16에서 실패로 처리해야 하는 이유를 설명하라. 같은 시각에 마지막 실행을 마친 경우와 비교하라.
- 협동형 실행기에서 log를 한 칸씩 진행하지 않고 2ms 동안 끝까지 실행하도록 바꾸었다. 3ms에 log가 시작하고 4ms에 gate가 준비된다면 어떤 가정이 깨지는가. gate의 이 실행에서 응답 시간은 얼마인가.
정답과 해설
이용률은 1/4+2/8+7/16=0.9375다. 세 태스크의 충분조건 경계보다 크므로 이용률 검사만으로는 결론을 내릴 수 없다. log의 응답 시간 후보는 7에서 시작하여 11, 14, 15, 15가 된다. 최종 응답 시간 15는 마감 16 이하다. gate와 speed의 응답 시간은 각각 1과 3으로 그대로이므로, 이 장의 비용 없는 선점 모델에서는 전체가 통과한다.
지터 폭은 5−2=3ms이고 최대 시작 지연은 5ms다. 이 값은 첫 실행까지의 대기만 나타낸다. 이후 실행과 선점을 포함한 완료 시각을 모르므로 마감 준수는 판정할 수 없다. 상대 마감과 응답 시간을 함께 알아야 한다.
절대 마감은 8+8=16이다. 현재 시각 16에 실행량이 남았다면 그 시각까지 끝내지 못한 것이므로 실패다. 마지막 실행을 마쳐 16에 완료했다면 응답 시간은 8이며 마감과 같아 성공이다. 경계에서의 완료 처리를 먼저 반영한 뒤 미완료 작업을 검사해야 한다.
준비된 높은 우선순위 작업이 즉시 선점할 수 있다는 가정이 깨진다. log는 3부터 5까지 실행하고 gate는 5부터 6까지 실행한다. gate는 4에 준비되어 6에 완료하므로 응답 시간은 2ms다. 원래 분석값 1ms를 그대로 사용할 수 없다. 이런 비선점 구간을 유지한다면 그로 인한 블로킹을 반영한 분석이 필요하다.
READER FEEDBACK
질문·의견
내용에 관한 질문이나 더 나은 설명을 위한 의견을 남겨 주세요. 오탈자는 위의 제보 양식이 더 빨리 반영됩니다. 이 댓글은 원래 게시글과 같은 자리에 쌓입니다.
댓글 0
아직 댓글이 없습니다. 첫 댓글을 남겨 보세요.