Blocked List 자료구조 — Timeout 정렬·Delta List·Two-List Scheme
#한 줄 요약
Blocked list는 timeout 기준으로 정렬해 둡니다. 매 tick마다 head만 확인하면 되므로 O(1)로 만료된 task만 깨울 수 있습니다.
#요구사항
vTaskDelay(100)을 호출하면 다음 동작이 필요합니다.
- Task가 Blocked list로 이동합니다.
- 100 tick이 지나면 자동으로 wake합니다.
- Tick ISR은 매 ms마다 검사를 돌리므로 충분히 빨라야 합니다.
가장 단순한 방법은 매 tick마다 모든 blocked task의 deadline을 비교하는 것입니다. O(N)이라 task가 많아지면 곧 한계에 부딪힙니다.
#Sorted Delta List
짧은 timeout이 앞에 오도록 정렬하고, 절대 카운터 대신 직전 노드와의 차이(delta)를 저장합니다.
Sorted by absolute timeout:[A: 50ms]→[B: 80ms]→[C: 200ms]
Delta list (시간 변화 누적):[A: 50ms]→[B: 30ms (=80-50)]→[C: 120ms (=200-80)]Tick마다 head의 delta만 1 감소시킵니다. O(1)로 끝납니다.
if (head_delta-- == 0) { wake(head); advance_head();}새 task가 들어올 때만 O(N)으로 위치를 찾습니다. 임베디드에서는 추가 빈도가 낮으므로 amortized 비용이 충분히 작습니다.
#FreeRTOS — Two-List Scheme
Tick wraparound가 골치 아픈 문제입니다. uint32_t tick은 약 49일이면 한 바퀴를 돕니다. wrap 이후에는 과거의 timeout 값이 마치 expired처럼 보이게 됩니다.
해결책은 list 두 개를 동시에 운영하는 것입니다.
List_t xDelayedTaskList1; // 현재 tick wrap 전List_t xDelayedTaskList2; // 다음 wrap 후List_t *pxDelayedTaskList = &xDelayedTaskList1;List_t *pxOverflowDelayedTaskList = &xDelayedTaskList2;#Insert
xTimeToWake = xTickCount + xTicksToDelay;if (xTimeToWake < xTickCount) { // overflow! vListInsert(pxOverflowDelayedTaskList, &task->xStateListItem);} else { vListInsert(pxDelayedTaskList, &task->xStateListItem);}overflow가 발생할 timeout은 overflow list로 넣어 두고, wraparound 시점에 두 list를 swap합니다.
#Tick ISR
xTickCount++;if (xTickCount == 0) { // wraparound swap(pxDelayedTaskList, pxOverflowDelayedTaskList);}
while (xTickCount >= listGET_ITEM_VALUE_OF_HEAD_ENTRY(pxDelayedTaskList)) { TCB_t *task = listGET_OWNER_OF_HEAD_ENTRY(pxDelayedTaskList); uxListRemove(&task->xStateListItem); move_to_ready(task);}sorted list와 매 tick head 확인의 조합입니다. 보통은 O(1)이고, 같은 tick에 여러 개가 만료될 때만 만료 개수에 비례합니다.
#ListItem_t의 xItemValue
FreeRTOS의 list 항목은 xItemValue를 갖습니다.
typedef struct ListItem { TickValue_t xItemValue; // sort key — 여기에 timeout 저장 struct ListItem *pxNext; struct ListItem *pxPrevious; void *pvOwner; // 일반 TCB_t* struct List *pxContainer; // 현재 속한 list} ListItem_t;vListInsert는 xItemValue를 비교해 위치를 정합니다. 한 번에 O(N)이지만 task 추가 자체가 드물어서 amortized 비용은 충분히 낮습니다.
#End Marker — List 순회 종료
typedef struct { UBaseType_t uxNumberOfItems; ListItem_t *pxIndex; // 현재 순회 위치 MiniListItem_t xListEnd; // sentinel} List_t;xListEnd.xItemValue = portMAX_DELAY는 모든 정상 task보다 큰 값이므로 항상 list의 끝에 위치합니다. 순회는 이 end marker에 도달하면 종료됩니다.
#Suspended List vs Blocked List
List_t xPendingReadyList; // ISR이 wake했지만 scheduler 못 깨운 taskList_t xSuspendedTaskList; // vTaskSuspend()된 taskList_t pxReadyTasksLists[N]; // readyList_t xDelayedTaskList1, xDelayedTaskList2; // blocked with timeout각 list는 서로 배타적입니다. task는 한 번에 하나의 list에만 속합니다.
#Wait List — Sync Object 측
typedef struct Semaphore { int count; List_t xTasksWaitingToTake; // 자원 대기 task들} Semaphore_t;자원 부족으로 blocked된 task는 두 개의 list에 동시에 link됩니다.
xDelayedTaskList에 timeout 처리용으로 들어갑니다.xStateListItem을 통해 연결됩니다.xTasksWaitingToTake에 자원 wake용으로 들어갑니다.xEventListItem을 통해 연결됩니다.
자원이 release되거나 timeout이 발생하면 양쪽에서 제거된 뒤 ready list로 이동합니다.
#Timer List
xTimerCreate()로 생성한 software timer도 별도의 list(pxCurrentTimerList)를 사용합니다. 정렬 방식은 동일합니다.
Timer task가 주기적으로 깨어나서 만료된 timer의 callback을 호출하고, 다음 만료 시점까지 다시 잠듭니다.
#Scheduler Suspended 시
vTaskSuspendAll()을 호출한 동안에는 tick 카운트만 진행되고 ready list로의 이동은 보류됩니다. Resume 시점에 놓친 wake를 한꺼번에 처리합니다.
void vTaskSuspendAll(void) { ++uxSchedulerSuspended; }
BaseType_t xTaskResumeAll(void) { --uxSchedulerSuspended; if (uxSchedulerSuspended == 0) { // missed tick 보충 while (xPendedTicks > 0) { xTaskIncrementTick(); xPendedTicks--; } }}#메모리 효율
| 자료구조 | 메모리 |
|---|---|
List_t | 20 byte (Cortex-M) |
ListItem_t | 24 byte |
MiniListItem_t (sentinel) | 16 byte |
task 10개에 sync object 5개를 잡아도 list 인프라가 차지하는 메모리는 약 500 byte 정도입니다. 임베디드 환경에서도 부담이 작은 수준입니다.
#자주 하는 실수
⚠️ Timeout을 너무 짧게 잡습니다
vTaskDelay(0)은 yield와 같은 의미입니다. vTaskDelay(1)은 1 tick 뒤에 wake되는데, 실제 시간은 tick rate에 따라 달라집니다. 1 kHz면 1 ms, 100 Hz면 10 ms입니다.
⚠️ Wraparound 시점을 가정합니다
uptime이 49일을 넘기면 wraparound가 발생합니다. xTickCount + delay 형태의 상대 timeout만 사용하고, 절대 시점끼리의 비교는 피해야 합니다.
⚠️ Wait list를 peek한 뒤 그대로 사용합니다
listGET_OWNER_OF_HEAD_ENTRY(...)로 가져온 task가 timeout으로 제거되면 포인터가 stale 상태가 됩니다. 반드시 critical section 안에서 다뤄야 합니다.
⚠️ Long sorted insert를 간과합니다
100개 이상의 task가 동시에 blocked 상태이면서 매번 새 task가 추가되면 add마다 O(N) 비용이 나옵니다. 드문 상황이지만 고우선·고빈도 timeout을 사용할 때는 주의가 필요합니다.
#정리
- Blocked list는 timeout 기준으로 정렬되며, tick마다 head만 확인합니다.
- Two-list scheme으로 tick wraparound를 안전하게 처리합니다.
ListItem_t의xItemValue가 정렬 키(timeout)로 동작합니다.- TCB는 state list와 event list에 동시에 link됩니다.
- sync object의 wait list도 같은 자료구조를 사용합니다.
다음 편은 scheduler 알고리즘 구현입니다. 다음 task를 선택하는 로직을 다룹니다.
#관련 항목
Practical RTOS Internals · 13 of 53
- 1 Practical RTOS Internals — 실시간 커널 내부 분석 시리즈 소개
- 2 RTOS가 필요한 이유 — 일반 OS와의 결정적 차이
- 3 Task와 Thread 개념 — TCB·상태 머신·생명 주기 분석
- 4 실시간 스케줄링 알고리즘 비교 — RR·Priority·EDF·RMS
- 5 Preemption과 Cooperation — 강제 전환 vs 자발 양보
- 6 인터럽트와 RTOS — ISR Context·Deferred Processing·FromISR API
- 7 동기화 기초 분석 — Critical Section·Mutual Exclusion·Race Condition
- 8 Semaphore 개념 분해 — Counting·Binary·P/V 연산
- 9 Mutex 개념 분해 — Ownership·Recursive·Priority Inheritance
- 10 큐와 메시지 패싱 — Producer-Consumer·Ring Buffer·전달 의미
- 11 실시간성 분석 — Latency·Jitter·Deadline·WCET·RMA
- 12 Ready List 자료구조 분석 — Linked List·Bitmap·O(1) Scheduler
- 13 Blocked List 자료구조 — Timeout 정렬·Delta List·Two-List Scheme
- 14 Scheduler 알고리즘 구현 추적 — Next-Task Selection 로직
- 15 Context Switch 원리 분석 — 레지스터 저장·복원·Stack Frame
- 16 ARM Cortex-M Context Switch — PendSV·MSP/PSP 어셈블리 추적
- 17 ARM Cortex-A Context Switch — Mode 전환·SVC·Banked Registers
- 18 RISC-V Context Switch 분석 — ECALL·mret·CSR
- 19 RTOS Tick과 타이머 — SysTick·Generic Timer·configTICK_RATE_HZ
- 20 Tickless 모드 구현 — Idle Tick Suppression·Sleep·Wake 보정
- 21 Scheduler Latency 측정 기법 — GPIO Toggle·DWT·ftrace·cyclictest
- 22 RTOS Tracing과 Observability — Tracealyzer·SystemView·ITM/ETM
- 23 Critical Section 구현 비교 — IRQ Disable·BASEPRI·Spinlock
- 24 Semaphore 내부 구현 추적 — Counter·Wait List·ISR-Safe Variant
- 25 Mutex 내부 구현 추적 — Owner·Recursion Count·ISR 금지
- 26 Priority Inversion 문제 — Mars Pathfinder 사례·Bounded vs Unbounded
- 27 Priority Inheritance 구현 — Inherit·Disinherit·Chain
- 28 Priority Ceiling Protocol — Immediate vs Original 비교
- 29 Queue 내부 구현 추적 — Ring Buffer·2 Wait Lists·Atomic Send/Receive
- 30 Event Group 분석 — Bit Flag·AND/OR Wait·Sync Barrier
- 31 ISR-Safe API 설계 — FromISR 패턴·Higher Priority Wake·Deferred Work
- 32 Deadlock 분석 — 4 조건·Wait-for Graph·Lock Ordering·Timeout
- 33 Stream Buffer와 Message Buffer — FreeRTOS 10의 Lock-Free SPSC
- 34 실시간 메모리 요구사항 — Determinism·Fragmentation·WCET
- 35 FreeRTOS Heap_1~5 분석 — 5종 Allocator의 구조와 트레이드오프
- 36 TLSF Allocator 분석 — Two-Level Segregated Fit O(1)
- 37 Static Allocation — 컴파일 타임으로 동적 위험 제거하기
- 38 Memory Pool — Fixed-Size Block Allocator의 단순함과 강력함
- 39 Stack Overflow 탐지 — Canary·MPU·Watermark 3중 방어
- 40 SMP RTOS 설계 — Ready List·Affinity·IPI·Load Balancing
- 41 SMP Spinlock 구현 — LDREX/STREX·Ticket Lock·MCS·WFE/SEV
- 42 Software Timer 분석 — Daemon Task·자료구조·ISR-Safe API
- 43 RTOS System Call — SVC·ECALL·User/Kernel 분리·FreeRTOS-MPU
- 44 TrustZone과 TF-M — Secure/Non-Secure·NSC Veneer·PSA
- 45 AMP와 OpenAMP — Heterogeneous SoC·RPMsg·remoteproc
- 46 C++ in RTOS — RAII·std::thread·ETL·Coroutine
- 47 FreeRTOS 소스 분석 — tasks.c·queue.c·port.c 추적
- 48 Zephyr 커널 분석 — k_thread·k_sem·Driver Model
- 49 RT-Thread 분석 — Object 모델·Components·Smart·Studio
- 50 RTOS 포팅 가이드 — 새 아키텍처에 옮기는 절차
- 51 RTOS 선택 가이드 — Footprint·License·Certification·Ecosystem
- 52 Apache NuttX 분석 — POSIX·PX4·NASA Ingenuity
- 53 PREEMPT_RT Linux — Mainline 6.12·Xenomai 4·EVL
관련 글
Scheduler 알고리즘 구현 추적 — Next-Task Selection 로직
FreeRTOS pxCurrentTCB 결정. CLZ 최적화, tie-breaking, scheduler entry points.
같은 시리즈에서 이어 읽기
Context Switch 원리 분석 — 레지스터 저장·복원·Stack Frame
Context switch는 결국 CPU의 모든 가시 상태를 task 스택에 통째로 복제하는 일입니다. 어디서 발생하고, 무엇을 저장하고, 비용은 얼마인지 아키텍처 중립적으로 정리합니다.
같은 시리즈에서 이어 읽기
Timer Wheel 분석 — Hashed·Hierarchical·O(1) Tick
Timer wheel 자료구조. Hashed wheel·hierarchical wheel·O(1) tick·Linux jiffies.
공통 태그 기반 추천