Preemption과 Cooperation — 강제 전환 vs 자발 양보
#한 줄 요약
“Preemption은 실시간성을, Cooperation은 단순성을 제공합니다.” 둘 중 하나를 선택합니다. 임베디드 RTOS는 거의 다 preemptive를 씁니다.
#Preemption — 강제 전환
높은 priority task가 ready 되면 현재 running task를 강제로 중단시킵니다. 두 가지 트리거가 있습니다.
#Tick Preemption
시스템 tick (1 ms) → tick ISR → scheduler 호출 → 다음 task 결정. Higher priority ready가 있으면 전환됩니다.
매 tick(보통 1-10 ms)마다 scheduler를 확인합니다. Time slice 만료 또는 higher-priority 등장 시 전환됩니다.
#IRQ Preemption
ISR 종료 시 portYIELD_FROM_ISR(needYield)가 즉시 scheduler를 호출합니다.
ISR이 higher-priority task를 깨우면 (xSemaphoreGiveFromISR 등) ISR 종료 시 즉시 그 task로 전환됩니다.
#Cooperation — 자발 양보
각 task가 명시적 yield로만 전환합니다.
void taskA(void *arg) { while (1) { do_work(); taskYIELD(); // 다른 task에 양보 }}또는 blocking API가 implicit yield 역할을 합니다. vTaskDelay(), xQueueReceive() 등은 내부적으로 yield합니다.
#Cooperative만 사용 시
configUSE_PREEMPTION = 0 // FreeRTOSTick 인터럽트는 time keeping만 담당하고 task 전환 트리거 역할을 하지 않습니다. 오직 task가 yield할 때만 scheduler가 동작합니다.
#비교 — Preemptive vs Cooperative 타임라인
#비교 표
| 항목 | Preemptive | Cooperative |
|---|---|---|
| 전환 트리거 | tick·IRQ·yield | yield만 |
| 실시간성 | High | Best-effort |
| Race condition | 자주 (atomicity 필요) | 적음 (전환점 명시) |
| Code 복잡도 | Higher (sync 필수) | Lower |
| 응답성 | µs 단위 | task의 yield 빈도에 의존 |
| 디버그 | 어려움 (어디서 전환할지 모름) | 쉬움 |
| 사용처 | 99% 임베디드 RTOS | 작은 cooperative kernel, Lua coroutine |
#Trade-off — 예시
#시나리오: 1 task가 5 ms 작업 + 다른 task가 1 ms 주기 PID
Preemptive의 경우:
- PID가 매 ms 깨어나 즉시 실행됩니다.
- 1 ms PID를 만족합니다.
- 5 ms 작업이 조각조각 진행되어 6-7 ms 안에 끝납니다.
Cooperative의 경우:
- 5 ms 작업이 통째로 실행되고, 그 동안 PID를 못 합니다.
- PID가 최대 5 ms 지연되어 deadline을 miss합니다.
- 5 ms 작업이 깨끗하게 5 ms에 끝납니다.
Deadline이 있으면 preemptive를 쓰고, 없으면 cooperative를 씁니다.
#Preemption Disable (Critical Section)
Preemptive RTOS에서도 짧은 구간은 preemption disable이 필요합니다.
taskENTER_CRITICAL();shared_counter++;shared_buffer[idx++] = value;taskEXIT_CRITICAL();taskENTER_CRITICAL()은 *interrupt mask (BASEPRI)*와 scheduler suspend를 합친 것입니다. 짧게 유지하는 게 핵심입니다.
#ISR과 Preemption
ISR은 task가 아닙니다. 단순히 현재 task를 잠시 빼앗는 코드입니다. ISR 도중에는 다음 규칙이 적용됩니다.
- task 전환을 하지 않습니다 (ISR이 끝날 때까지).
- 다른 ISR이 더 높은 priority면 nested IRQ가 가능합니다.
- ISR이 task wake 시 ISR 끝 직후 scheduler를 호출합니다.
void TIM1_IRQHandler(void) { BaseType_t xHigherPriorityTaskWoken = pdFALSE;
xSemaphoreGiveFromISR(semHandle, &xHigherPriorityTaskWoken); /* 위 호출이 higher-priority task를 ready로 만들면 xHigherPriorityTaskWoken = pdTRUE 설정됨 */
portYIELD_FROM_ISR(xHigherPriorityTaskWoken); /* pdTRUE면 ISR 끝나면서 task 전환 발생 */}#Tick Rate 선택
| Tick Rate | Period | Trade-off |
|---|---|---|
| 100 Hz | 10 ms | 저전력, 낮은 정밀도 |
| 1 kHz | 1 ms | 표준 — 대부분 RTOS 기본 |
| 10 kHz | 0.1 ms | 고정밀, ISR overhead 큼 |
FreeRTOS의 configTICK_RATE_HZ = 1000이 기본값입니다. 100 Hz면 *vTaskDelay(1)*이 10 ms 지연임에 주의해야 합니다.
#Tickless Mode
Idle 시 tick interrupt를 멈추면 CPU sleep을 더 길게 할 수 있습니다. 배터리 동작에 필수입니다.
configUSE_TICKLESS_IDLE = 1configEXPECTED_IDLE_TIME_BEFORE_SLEEP = 2 // 2 tick 이상 idle 시 sleep다음 task의 wake 시간을 알아내 그 시점까지 timer를 동적으로 설정합니다. 정밀한 hw timer가 필요합니다.
#SMP에서 Preemption
다중 코어에서는 코어별 독립적으로 preemption이 일어납니다. 한 코어의 ISR이 다른 코어의 task에 직접 영향을 주지 않습니다. **Inter-Processor Interrupt (IPI)**로 다른 코어를 깨웁니다.
예 — Core 0: ISR → IPI → Core 1 wake → Core 1의 ready task 실행.
#흔한 함정
⚠️ ISR에서 long work
Preemptive RTOS에서도 ISR 도중에는 어떤 task도 실행하지 못합니다. ISR은 짧게 유지하고(수 µs), 긴 작업은 deferred task로 넘깁니다.
⚠️ Critical Section 너무 김
taskENTER_CRITICAL() 안에 printf 같은 수 ms 작업을 넣으면 그 동안 모든 ISR과 task가 막힙니다. 짧게 유지해야 합니다.
⚠️ Cooperative에서 무한 루프
Yield 없는 무한 루프는 시스템을 행 상태로 만듭니다. while(1)에 항상 wait API나 yield를 넣습니다.
⚠️ Tick rate 너무 높음
10 kHz tick은 매 100 µs마다 ISR이 발생합니다. CPU 부담이 커집니다. 1 kHz로 충분합니다.
#정리
- Preemptive는 tick과 IRQ에서 강제로 전환하여 실시간성을 보장합니다.
- Cooperative는 yield만으로 전환되어 단순하지만 deadline을 보장하지 못합니다.
- 임베디드 RTOS는 거의 preemptive를 사용합니다.
- Critical section은 짧게 유지해야 합니다. preemption disable 구간이 latency를 결정합니다.
- Tickless mode가 배터리 동작의 핵심입니다.
다음 편에서는 인터럽트와 RTOS를 다룹니다. ISR context, deferred processing, FromISR API를 살펴봅니다.
#관련 항목
Practical RTOS Internals · 5 of 53
- 1Practical RTOS Internals — 실시간 커널 내부 분석 시리즈 소개
- 2RTOS가 필요한 이유 — 일반 OS와의 결정적 차이
- 3Task와 Thread 개념 — TCB·상태 머신·생명 주기 분석
- 4실시간 스케줄링 알고리즘 비교 — RR·Priority·EDF·RMS
- 5Preemption과 Cooperation — 강제 전환 vs 자발 양보
- 6인터럽트와 RTOS — ISR Context·Deferred Processing·FromISR API
- 7동기화 기초 분석 — Critical Section·Mutual Exclusion·Race Condition
- 8Semaphore 개념 분해 — Counting·Binary·P/V 연산
- 9Mutex 개념 분해 — Ownership·Recursive·Priority Inheritance
- 10큐와 메시지 패싱 — Producer-Consumer·Ring Buffer·전달 의미
- 11실시간성 분석 — Latency·Jitter·Deadline·WCET·RMA
- 12Ready List 자료구조 분석 — Linked List·Bitmap·O(1) Scheduler
- 13Blocked List 자료구조 — Timeout 정렬·Delta List·Two-List Scheme
- 14Scheduler 알고리즘 구현 추적 — Next-Task Selection 로직
- 15Context Switch 원리 분석 — 레지스터 저장·복원·Stack Frame
- 16ARM Cortex-M Context Switch — PendSV·MSP/PSP 어셈블리 추적
- 17ARM Cortex-A Context Switch — Mode 전환·SVC·Banked Registers
- 18RISC-V Context Switch 분석 — ECALL·mret·CSR
- 19RTOS Tick과 타이머 — SysTick·Generic Timer·configTICK_RATE_HZ
- 20Tickless 모드 구현 — Idle Tick Suppression·Sleep·Wake 보정
- 21Scheduler Latency 측정 기법 — GPIO Toggle·DWT·ftrace·cyclictest
- 22RTOS Tracing과 Observability — Tracealyzer·SystemView·ITM/ETM
- 23Critical Section 구현 비교 — IRQ Disable·BASEPRI·Spinlock
- 24Semaphore 내부 구현 추적 — Counter·Wait List·ISR-Safe Variant
- 25Mutex 내부 구현 추적 — Owner·Recursion Count·ISR 금지
- 26Priority Inversion 문제 — Mars Pathfinder 사례·Bounded vs Unbounded
- 27Priority Inheritance 구현 — Inherit·Disinherit·Chain
- 28Priority Ceiling Protocol — Immediate vs Original 비교
- 29Queue 내부 구현 추적 — Ring Buffer·2 Wait Lists·Atomic Send/Receive
- 30Event Group 분석 — Bit Flag·AND/OR Wait·Sync Barrier
- 31ISR-Safe API 설계 — FromISR 패턴·Higher Priority Wake·Deferred Work
- 32Deadlock 분석 — 4 조건·Wait-for Graph·Lock Ordering·Timeout
- 33Stream Buffer와 Message Buffer — FreeRTOS 10의 Lock-Free SPSC
- 34실시간 메모리 요구사항 — Determinism·Fragmentation·WCET
- 35FreeRTOS Heap_1~5 분석 — 5종 Allocator의 구조와 트레이드오프
- 36TLSF Allocator 분석 — Two-Level Segregated Fit O(1)
- 37Static Allocation — 컴파일 타임으로 동적 위험 제거하기
- 38Memory Pool — Fixed-Size Block Allocator의 단순함과 강력함
- 39Stack Overflow 탐지 — Canary·MPU·Watermark 3중 방어
- 40SMP RTOS 설계 — Ready List·Affinity·IPI·Load Balancing
- 41SMP Spinlock 구현 — LDREX/STREX·Ticket Lock·MCS·WFE/SEV
- 42Software Timer 분석 — Daemon Task·자료구조·ISR-Safe API
- 43RTOS System Call — SVC·ECALL·User/Kernel 분리·FreeRTOS-MPU
- 44TrustZone과 TF-M — Secure/Non-Secure·NSC Veneer·PSA
- 45AMP와 OpenAMP — Heterogeneous SoC·RPMsg·remoteproc
- 46C++ in RTOS — RAII·std::thread·ETL·Coroutine
- 47FreeRTOS 소스 분석 — tasks.c·queue.c·port.c 추적
- 48Zephyr 커널 분석 — k_thread·k_sem·Driver Model
- 49RT-Thread 분석 — Object 모델·Components·Smart·Studio
- 50RTOS 포팅 가이드 — 새 아키텍처에 옮기는 절차
- 51RTOS 선택 가이드 — Footprint·License·Certification·Ecosystem
- 52Apache NuttX 분석 — POSIX·PX4·NASA Ingenuity
- 53PREEMPT_RT Linux — Mainline 6.12·Xenomai 4·EVL
관련 글
RTOS가 필요한 이유 — 일반 OS와의 결정적 차이
Super-loop는 모든 작업이 직렬화되어 deadline을 보장하지 못합니다. RTOS는 preemption과 우선순위로 실시간성을 확보합니다.
RTOS 포팅 가이드 — 새 아키텍처에 옮기는 절차
FreeRTOS와 Zephyr의 port 계층을 따라가며 새 아키텍처에 RTOS를 옮기는 절차를 정리합니다. initial stack frame, context switch assembly, tick source, critical section primitive까지 한 번에 잡습니다.
C++ in RTOS — RAII·std::thread·ETL·Coroutine
RTOS C API를 C++ 객체로 감싸는 패턴을 정리합니다. RAII MutexGuard와 ScopedIRQDisable, std::thread/std::mutex의 한계와 직접 xTaskCreate가 결정성을 갖는 이유, ETL로 STL을 대체하는 법, C++20 coroutine을 RTOS 위에 얹는 방식까지 다룹니다.
이 글을 참조하는 글 (8)
- PROFINET 개요 분석 — RT·IRT 클래스와 실시간 등급— Industrial Ethernet 심화
- EtherCAT Master 구현 비교 — SOEM·IgH·TwinCAT 분석— Industrial Ethernet 심화
- 산업용 통신 실시간 요구사항 — Determinism·Jitter·Cycle Time— Industrial Ethernet 심화
- 산업용 이더넷 분석 — 일반 이더넷과 결정성 요구의 차이— Industrial Ethernet 심화
- Tickless 모드 구현 — Idle Tick Suppression·Sleep·Wake 보정— Practical RTOS Internals
- RTOS Tick과 타이머 — SysTick·Generic Timer·configTICK_RATE_HZ— Practical RTOS Internals
- 인터럽트와 RTOS — ISR Context·Deferred Processing·FromISR API— Practical RTOS Internals
- 실시간 스케줄링 알고리즘 비교 — RR·Priority·EDF·RMS— Practical RTOS Internals