Event Group 분석 — Bit Flag·AND/OR Wait·Sync Barrier
#한 줄 요약
“Event Group은 24-bit flag와 wait list로 구성된다” — 여러 task를 한 번에 wake할 수 있다는 점이 핵심입니다.
이번 글에서는 Event Group의 자료구조와 동작을 살펴봅니다. SetBits, WaitBits, Sync barrier까지 한꺼번에 다룹니다.
#자료 구조
typedef struct EventGroupDef_t { EventBits_t uxEventBits; // 24-bit flag (8 bits = control) List_t xTasksWaitingForBits; // 대기 task list
UBaseType_t uxEventGroupNumber; // debug uint8_t ucStaticallyAllocated;} EventGroup_t;
typedef uint32_t EventBits_t;#define eventEVENT_BITS_CONTROL_BYTES 0xff000000UL // 상위 8-bit 예약24-bit는 사용자 flag로 쓰이고, 나머지는 컨트롤 비트(clear-on-exit, wait-for-all, unblocked)로 예약돼 있습니다.
#SetBits — Bit 켜기와 Wake
EventBits_t xEventGroupSetBits(EventGroupHandle_t xEventGroup, const EventBits_t uxBitsToSet) { EventBits_t uxBitsToClear = 0; BaseType_t xMatchFound = pdFALSE;
vTaskSuspendAll(); { ListItem_t *pxListItem = listGET_HEAD_ENTRY(&xEventGroup->xTasksWaitingForBits);
eventGroup->uxEventBits |= uxBitsToSet;
/* 대기 task 순회 — 조건 만족 시 wake */ while (pxListItem != listGET_END_MARKER(&xEventGroup->xTasksWaitingForBits)) { EventBits_t uxBitsWaitedFor = listGET_LIST_ITEM_VALUE(pxListItem); EventBits_t uxControlBits = uxBitsWaitedFor & eventEVENT_BITS_CONTROL_BYTES; uxBitsWaitedFor &= ~eventEVENT_BITS_CONTROL_BYTES;
if ((uxControlBits & eventWAIT_FOR_ALL_BITS) == 0) { /* OR — 하나라도 match */ if ((uxBitsWaitedFor & xEventGroup->uxEventBits) != 0) { xMatchFound = pdTRUE; } } else { /* AND — 모두 match */ if ((uxBitsWaitedFor & xEventGroup->uxEventBits) == uxBitsWaitedFor) { xMatchFound = pdTRUE; } }
if (xMatchFound) { if (uxControlBits & eventCLEAR_EVENTS_ON_EXIT_BIT) { uxBitsToClear |= uxBitsWaitedFor; } vTaskRemoveFromUnorderedEventList(pxListItem, xEventGroup->uxEventBits | eventUNBLOCKED_DUE_TO_BIT_SET); } pxListItem = pxNext; xMatchFound = pdFALSE; }
/* Clear-on-exit 누적 적용 */ xEventGroup->uxEventBits &= ~uxBitsToClear; } xTaskResumeAll();
return xEventGroup->uxEventBits;}핵심은 모든 대기 task를 순회하면서 조건을 만족하는 task를 모두 wake한다는 점입니다. Queue나 Mutex와 다른 부분이 바로 여기입니다.
#WaitBits — AND/OR/Clear-on-exit
EventBits_t xEventGroupWaitBits(EventGroupHandle_t xEventGroup, const EventBits_t uxBitsToWaitFor, const BaseType_t xClearOnExit, const BaseType_t xWaitForAllBits, TickType_t xTicksToWait) { EventBits_t uxReturn;
vTaskSuspendAll(); { EventBits_t uxCurrentEventBits = xEventGroup->uxEventBits;
/* 즉시 만족하는가? */ if (xWaitForAllBits == pdFALSE) { /* OR */ if ((uxCurrentEventBits & uxBitsToWaitFor) != 0) { uxReturn = uxCurrentEventBits; if (xClearOnExit) { xEventGroup->uxEventBits &= ~uxBitsToWaitFor; } xTaskResumeAll(); return uxReturn; } } else { /* AND */ if ((uxCurrentEventBits & uxBitsToWaitFor) == uxBitsToWaitFor) { uxReturn = uxCurrentEventBits; if (xClearOnExit) { xEventGroup->uxEventBits &= ~uxBitsToWaitFor; } xTaskResumeAll(); return uxReturn; } }
/* 만족하지 않음 — block */ if (xTicksToWait != 0) { EventBits_t uxControlBits = 0; if (xClearOnExit) uxControlBits |= eventCLEAR_EVENTS_ON_EXIT_BIT; if (xWaitForAllBits) uxControlBits |= eventWAIT_FOR_ALL_BITS;
vTaskPlaceOnUnorderedEventList(&xEventGroup->xTasksWaitingForBits, uxBitsToWaitFor | uxControlBits, xTicksToWait); } } xTaskResumeAll(); portYIELD_WITHIN_API();
/* Wake 후 — list item value에 결과가 인코딩돼 있음 */ uxReturn = uxTaskResetEventItemValue(); return uxReturn & ~eventEVENT_BITS_CONTROL_BYTES;}#Sync Barrier — Rendezvous Pattern
EventBits_t xEventGroupSync(EventGroupHandle_t xEventGroup, const EventBits_t uxBitsToSet, const EventBits_t uxBitsToWaitFor, TickType_t xTicksToWait);
/* 사용 예 — 3 task barrier */#define TASK_A_DONE (1 << 0)#define TASK_B_DONE (1 << 1)#define TASK_C_DONE (1 << 2)#define ALL_DONE (TASK_A_DONE | TASK_B_DONE | TASK_C_DONE)
void task_a(void *p) { /* ... work ... */ xEventGroupSync(eg, TASK_A_DONE, ALL_DONE, portMAX_DELAY); /* 셋 다 도착해야 여기에 도달 */}Set과 Wait, Auto-clear가 원자적으로 묶여 있습니다. 3개 task가 모두 도착하면 세 task가 동시에 wake됩니다.
#ISR-safe variant
BaseType_t xEventGroupSetBitsFromISR(EventGroupHandle_t eg, EventBits_t bits, BaseType_t *pxHigherPriorityTaskWoken);ISR에서 task를 깨우는 작업은 timer service task에 defer됩니다. daemon task가 실제 set 처리를 맡습니다. 이렇게 해야 ISR이 짧게 유지됩니다.
ISR → xTimerPendFunctionCallFromISR(set_bits_fn, eg, bits) → Daemon task가 깨어나 실제 SetBits 호출#사용 예 — Wi-Fi 연결 상태 머신
#define BIT_CONNECTED (1 << 0)#define BIT_GOT_IP (1 << 1)#define BIT_DNS_READY (1 << 2)#define BIT_ALL_READY (BIT_CONNECTED | BIT_GOT_IP | BIT_DNS_READY)
void wifi_event_handler(wifi_event_t e) { if (e == WIFI_CONNECTED) xEventGroupSetBits(net_eg, BIT_CONNECTED); if (e == WIFI_GOT_IP) xEventGroupSetBits(net_eg, BIT_GOT_IP); if (e == WIFI_DNS_READY) xEventGroupSetBits(net_eg, BIT_DNS_READY);}
void app_task(void *p) { /* 세 조건이 모두 만족될 때까지 대기 */ xEventGroupWaitBits(net_eg, BIT_ALL_READY, pdFALSE, pdTRUE /* AND */, portMAX_DELAY);
start_mqtt_client();}여러 event에 의존하는 task를 동기화할 때 binary semaphore를 3개 두는 것보다 훨씬 깔끔합니다.
#Performance
SetBits:- vTaskSuspendAll: 5 cycle- iterate wait list: 30 cycle × N waiters- match check: 10 cycle × N- portYIELD_WITHIN_API: 50 cycleTotal ≈ 60 + N × 40 cycle
WaitBits (즉시 만족): 30 cycleWaitBits (block + wake): ~200 cycleCritical section 대신 scheduler suspend를 사용하므로 ISR 잠금이 발생하지 않습니다.
#자주 하는 실수
⚠️ 24-bit를 넘는 사용
(1 << 24) 이상은 컨트롤 비트와 충돌합니다. configUSE_16_BIT_TICKS=1 환경에서는 8-bit만 가능합니다.
⚠️ Clear-on-exit와 Wait-for-all 혼동
xClearOnExit=pdTRUE + xWaitForAllBits=pdFALSE 조합은 wait한 모든 bit를 clear합니다(현재 set된 bit만이 아닙니다). 의도와 다른 동작이 자주 일어나는 지점입니다.
⚠️ Polling 식 SetBits
while(...) xEventGroupSetBits(eg, X); 패턴은 wait list를 매번 walk해 CPU를 낭비합니다. 상태가 변할 때만 호출합니다.
⚠️ Race — Test-and-set이 없음
bits = xEventGroupGetBits(eg);if (bits & BIT_X) { /* 여기서 다른 task가 clear할 수 있음 */ do_stuff();}xEventGroupWaitBits + ClearOnExit로 원자성을 확보하는 것이 안전합니다.
#정리
- Event Group은 24-bit flag와 wait list 조합입니다.
- AND / OR / Clear-on-exit 세 조합으로 다양한 동기화 패턴을 만들 수 있습니다.
- SetBits는 모든 대기 task를 동시에 wake합니다(queue/mutex와 다른 점입니다).
- xEventGroupSync는 barrier 역할을 합니다. 다중 task rendezvous에 적합합니다.
- ISR에서의 set은 daemon task에 defer됩니다.
다음 편은 ISR-safe API, 즉 FromISR 함수군의 내부 구현입니다.
#관련 항목
Practical RTOS Internals · 30 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
관련 글
PREEMPT_RT Linux — Mainline 6.12·Xenomai 4·EVL
2024년 9월 Linux 6.12 mainline에 합류한 PREEMPT_RT의 핵심 변경을 정리하고, Xenomai 4·EVL과 함께 RTOS와의 선택 기준을 비교합니다. threaded IRQ·sleeping spinlock·cyclictest까지 한 지도에 모읍니다.
Apache NuttX 분석 — POSIX·PX4·NASA Ingenuity
NuttX의 POSIX-compliant 구조를 따라가며 PX4 autopilot과 NASA Ingenuity 화성 헬리콥터 채택 배경을 정리합니다. Flat/Protected/Kernel 빌드, VFS, 네트워크, NSH, micro-ROS 통합까지 한 지도로 모읍니다.
RTOS 선택 가이드 — Footprint·License·Certification·Ecosystem
FreeRTOS·Zephyr·ThreadX·RT-Thread·NuttX·VxWorks·QNX·INTEGRITY·SafeRTOS·µC/OS·PX5를 한 표에 모아 비교합니다. IoT·자동차·항공·산업·의료·웨어러블·드론별 추천과 결정 기준을 정리합니다.