Ready List 자료구조 분석 — Linked List·Bitmap·O(1) Scheduler
#한 줄 요약
“Ready list는 스케줄러의 거의 전부” — 다음에 실행할 task를 얼마나 빨리 찾느냐가 곧 RTOS의 latency 한계입니다.
#스케줄러가 푸는 문제
매 tick마다, 매 system call마다, 매 ISR-exit마다 스케줄러가 같은 질문을 던집니다.
“지금 ready 상태인 task 중 가장 높은 priority는 누구인가?”
이 질문에 대한 답이 일정한 시간 안에 나오지 않으면 RTOS의 determinism은 무너집니다. 그래서 ready list 자료구조는 다음 세 조건을 모두 만족해야 합니다.
- 다음 task 찾기가 O(1) 또는 O(log N)
- task 삽입·삭제도 O(1)
- 임베디드답게 메모리가 작아야 함
이 조건을 어떻게 만족하느냐로 RTOS 마다 색깔이 나뉩니다.
#가장 단순한 시도 — 정렬된 단일 list
List_t xReadyList; // priority 내림차순 정렬
// 다음 task — headTCB_t *next = listGET_OWNER_OF_HEAD_ENTRY(&xReadyList);
// insert — 적절한 위치 찾기for (item = head; item != end; item = item->next) { if (item->priority < new_task->priority) break;}찾기는 O(1)이지만 삽입이 O(N) 입니다. block/unblock이 빈번한 RTOS에서는 받아들이기 어렵습니다.
#FreeRTOS 기본 — Array of Lists (Generic)
각 priority 별로 별도 doubly-linked list를 둡니다.
List_t pxReadyTasksLists[configMAX_PRIORITIES];32 priority면 list 32개입니다. 같은 priority 안에서는 round-robin이 자연스럽게 됩니다.
#다음 task 찾기
// configUSE_PORT_OPTIMISED_TASK_SELECTION = 0UBaseType_t uxTopReadyPriority = 0;while (listLIST_IS_EMPTY(&pxReadyTasksLists[uxTopReadyPriority])) { if (++uxTopReadyPriority >= configMAX_PRIORITIES) break;}pxCurrentTCB = listGET_OWNER_OF_NEXT_ENTRY( &pxReadyTasksLists[uxTopReadyPriority]);위에서부터 비어있지 않은 list를 찾을 때까지 순회합니다. O(P) — priority 수에 비례합니다. 32 priority면 최악의 경우 32회 비교가 필요합니다.
#Insert / Remove
vListInsert(&pxReadyTasksLists[priority], &task->xStateListItem);uxListRemove(&task->xStateListItem);doubly-linked list라 양쪽 다 O(1)입니다.
이 generic 방식은 어떤 CPU에서도 동작하는 안전한 선택입니다. Cortex-M0처럼 비트 연산이 빈약한 코어가 그 대상입니다.
#FreeRTOS 최적화 — Bitmap + CLZ
configUSE_PORT_OPTIMISED_TASK_SELECTION = 1로 켜면 Cortex-M3 이상에서 진짜 O(1) 으로 바뀝니다.
volatile UBaseType_t uxTopReadyPriority; // bit N = priority N에 ready task 있음List_t pxReadyTasksLists[configMAX_PRIORITIES];uxTopReadyPriority는 32 bit bitmap입니다. priority 5에 task가 하나라도 있으면 bit 5가 켜집니다.
Ready list의 자료구조를 그림으로 보면 이렇습니다. priority bitmap의 set bit 위치가 곧 list array의 index가 되고, CLZ 한 명령으로 가장 높은 priority를 찾습니다.
#CLZ — 1 cycle로 가장 높은 priority 찾기
// ARM CLZ (Count Leading Zeros) intrinsicunsigned int uxTopPriority = (31UL - __clz(uxTopReadyPriority));pxCurrentTCB = listGET_OWNER_OF_NEXT_ENTRY( &pxReadyTasksLists[uxTopPriority]);__clz(x)는 상위 비트부터 0의 개수를 셉니다. ARMv7-M 이상의 CLZ 명령은 1 cycle입니다. 32 priority도 1 cycle에 답이 나옵니다.
생성되는 어셈블리는 두 줄짜리입니다.
clz r1, r0 @ r1 = leading zero count of bitmaprsb r1, r1, #31 @ r1 = 31 - r1#bitmap 유지
Insert와 remove 때 bitmap을 함께 갱신합니다.
// Insert — 비어있던 list라면 bit setportRECORD_READY_PRIORITY(priority, uxTopReadyPriority);vListInsertEnd(&pxReadyTasksLists[priority], item);
// Remove — list가 비면 bit clearuxListRemove(item);if (listLIST_IS_EMPTY(&pxReadyTasksLists[priority])) { portRESET_READY_PRIORITY(priority, uxTopReadyPriority);}portRECORD_READY_PRIORITY는 보통 uxTopReadyPriority |= (1UL << priority) 한 줄, reset도 &= ~(1UL << priority) 한 줄입니다. 단일 명령으로 끝납니다.
💡 CLZ는 ARMv7-M (Cortex-M3/M4/M7), ARMv8-M부터 있습니다. Cortex-M0/M0+ (ARMv6-M)에는 없어서 자동으로 generic 모드로 떨어집니다. 이 차이를 모르고 M0에서 최적화를 켜면 빌드가 깨집니다.
#uC/OS-III — 8 × 8 Bitmap + Lookup Table
CLZ 명령이 없던 시절의 우아한 답입니다. 256 priority를 2단 bitmap으로 다룹니다.
CPU_INT08U OSPrioRdyGrp; // group bit (1 byte)CPU_INT08U OSPrioTbl[OS_CFG_PRIO_MAX / 8]; // detail (32 byte for 256 prio)priority p를 group = p / 8, bit = p % 8로 나눕니다. group에 ready task가 있으면 OSPrioRdyGrp의 해당 bit가 켜집니다.
#Lookup Table
// 8-bit pattern → 가장 낮은 bit 위치static const CPU_INT08U OSUnMapTbl[256] = { 0, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0, /* ... 256 entries ... */};
CPU_INT08U group = OSUnMapTbl[OSPrioRdyGrp];CPU_INT08U bit = OSUnMapTbl[OSPrioTbl[group]];CPU_INT08U prio = (group << 3) + bit;LUT 두 번 lookup으로 priority가 나옵니다. 8-bit MCU에서도 진정한 O(1)입니다. 256 byte LUT가 필요하지만, ROM에 두면 RAM은 거의 안 씁니다.
#Linux CFS — Red-Black Tree
Linux Completely Fair Scheduler는 priority 대신 virtual runtime을 기준으로 합니다. RB tree에 task들을 vruntime 순서로 넣고, 가장 왼쪽 노드가 다음 task입니다.
struct cfs_rq { struct rb_root_cached tasks_timeline; struct sched_entity *curr;};
// 다음 task — cached leftmostse = rb_first_cached(&cfs_rq->tasks_timeline);
// insert / remove__enqueue_entity(cfs_rq, se); // O(log N)__dequeue_entity(cfs_rq, se); // O(log N)찾기는 cache 덕에 O(1), 삽입·삭제는 O(log N)입니다. task 1000개라도 약 10 비교면 끝납니다.
CFS는 fair share를 추구하므로 RTOS와 목적이 다릅니다. 임베디드 RTOS가 priority-O(1)을 고집하는 이유는 throughput이 아니라 latency가 목표이기 때문입니다.
#한 표로 비교
| RTOS | 자료구조 | 다음 task | Insert | 최대 prio | TCB 오버헤드 |
|---|---|---|---|---|---|
| FreeRTOS (generic) | Array of lists | O(P) | O(1) | 32 | ~24 B |
| FreeRTOS (Cortex-M opt) | Bitmap + list | O(1) | O(1) | 32 | ~24 B |
| uC/OS-III | 8×8 bitmap + LUT | O(1) | O(1) | 256 | ~20 B |
| RT-Thread | Bitmap + list | O(1) | O(1) | 32 | ~24 B |
| Zephyr | Priority queue | O(1) | O(1)~O(log N) | 64 | 가변 |
| Linux CFS | RB tree | O(1) cached | O(log N) | 40 nice | 수백 B |
임베디드 RTOS는 거의 예외 없이 bitmap + linked-list-per-priority 조합으로 수렴합니다.
#Wait list — Ready list와 짝
ready list만 있으면 RTOS는 동작하지 않습니다. semaphore·mutex·queue 각자가 대기 중 task를 보관하는 wait list를 가집니다.
typedef struct { int count; List_t xTasksWaitingToTake; // priority-sorted} Semaphore_t;
// 자원 해제 시 — 가장 높은 priority부터 wakeTCB_t *next = listGET_OWNER_OF_HEAD_ENTRY(&xTasksWaitingToTake);move_to_ready_list(next);wait list는 priority-sorted로 두는 게 일반적입니다. wake 시 head만 보면 가장 우선해야 할 task가 즉시 나옵니다.
#TCB가 두 list에 동시 link
여기서 흥미로운 디테일이 있습니다. 한 task는 동시에 ready list와 wait list 양쪽에 들어가야 할 때가 있습니다.
typedef struct { /* ... */ ListItem_t xStateListItem; // ready / blocked / suspended 중 하나 ListItem_t xEventListItem; // 특정 이벤트 wait list} tskTCB;FreeRTOS는 TCB에 두 개의 ListItem을 둡니다. 하나는 상태 list용, 다른 하나는 이벤트 list용입니다. 이렇게 하면 timeout이 걸린 semaphore wait의 경우 delayed list와 wait list 양쪽에 자연스럽게 들어갑니다. 둘 중 어느 쪽에서 먼저 깨어나든 일관된 처리가 가능합니다.
#Delayed list — Timeout이 있는 task
vTaskDelay()나 timeout 있는 xSemaphoreTake()를 호출한 task는 delayed list로 갑니다. tick마다 expired task를 ready로 옮깁니다.
static List_t xDelayedTaskList1; // wake-up tick 오름차순static List_t xDelayedTaskList2; // tick overflow 대비 swap
// tick ISR 안while (xTickCount >= listGET_ITEM_VALUE_OF_HEAD_ENTRY(&xDelayedTaskList1)) { TCB_t *t = listGET_OWNER_OF_HEAD_ENTRY(&xDelayedTaskList1); uxListRemove(&t->xStateListItem); move_to_ready_list(t);}list 두 개를 두는 이유는 tick counter wrap-around 때문입니다. wrap이 일어나는 순간 두 list를 교환합니다. 자세한 동작은 2-08편에서 다룹니다.
#자주 하는 실수
⚠️ priority를 32단계 다 쓰기
대부분의 시스템은 5~10단계로 충분합니다. priority를 잘게 나눌수록 메모리도 늘고 우선순위 설계도 어려워집니다. 같은 priority의 task는 round-robin으로 도니 합쳐도 큰 문제가 없습니다.
⚠️ wait list 순서를 가정
priority-sorted라고 가정했는데 실제 구현이 FIFO면 가장 우선해야 할 task가 맨 뒤에 있을 수 있습니다. RTOS 별로 정책이 다르니 문서 확인이 필요합니다.
⚠️ Generic mode와 Opt mode를 혼동
Cortex-M0에서 configUSE_PORT_OPTIMISED_TASK_SELECTION = 1로 두면 CLZ가 없어 빌드가 깨집니다. 반대로 Cortex-M4에서 generic mode로 두면 *공짜 O(1)*을 버리는 셈입니다.
#정리
- Ready list는 priority 별 doubly-linked list 배열이 사실상 표준입니다.
- Bitmap + CLZ가 Cortex-M3+ 에서 O(1) 스케줄러를 만듭니다. 단 두 명령이면 답이 나옵니다.
- uC/OS-III의 8×8 bitmap + LUT는 CLZ가 없는 시대의 우아한 답입니다.
- Linux CFS는 RB tree + vruntime으로 fairness를 추구합니다. RTOS와는 목표 자체가 다릅니다.
- TCB가 두 ListItem을 가져 ready list와 wait list 양쪽에 동시에 link 됩니다. 임베디드 linked-list의 트릭입니다.
다음 편은 Blocked List 자료구조 — timeout 정렬과 delta list 기법을 다룹니다.
#관련 항목
Practical RTOS Internals · 12 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
관련 글
TLSF Allocator 분석 — Two-Level Segregated Fit O(1)
Masmano 2004의 TLSF 알고리즘을 풀어봅니다. Bitmap과 CLZ 명령으로 alloc·free·coalesce 모두 O(1)을 보장하며, 자동차·로봇·RT 게임의 표준 dynamic allocator가 된 이유를 살펴봅니다.
Scheduler Latency 측정 기법 — GPIO Toggle·DWT·ftrace·cyclictest
ISR 종료 → ready task 실행까지의 시간. 측정 방법과 worst-case 추적.
Scheduler 알고리즘 구현 추적 — Next-Task Selection 로직
FreeRTOS pxCurrentTCB 결정. CLZ 최적화, tie-breaking, scheduler entry points.