본문으로 건너뛰기
Practical RTOS Internals · 29/53

Event Group 분석 — Bit Flag·AND/OR Wait·Sync Barrier

· Hawk · 3분 읽기

#한 줄 요약

“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 cycle
Total ≈ 60 + N × 40 cycle
WaitBits (즉시 만족): 30 cycle
WaitBits (block + wake): ~200 cycle

Critical 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

  1. 1Practical RTOS Internals — 실시간 커널 내부 분석 시리즈 소개
  2. 2RTOS가 필요한 이유 — 일반 OS와의 결정적 차이
  3. 3Task와 Thread 개념 — TCB·상태 머신·생명 주기 분석
  4. 4실시간 스케줄링 알고리즘 비교 — RR·Priority·EDF·RMS
  5. 5Preemption과 Cooperation — 강제 전환 vs 자발 양보
  6. 6인터럽트와 RTOS — ISR Context·Deferred Processing·FromISR API
  7. 7동기화 기초 분석 — Critical Section·Mutual Exclusion·Race Condition
  8. 8Semaphore 개념 분해 — Counting·Binary·P/V 연산
  9. 9Mutex 개념 분해 — Ownership·Recursive·Priority Inheritance
  10. 10큐와 메시지 패싱 — Producer-Consumer·Ring Buffer·전달 의미
  11. 11실시간성 분석 — Latency·Jitter·Deadline·WCET·RMA
  12. 12Ready List 자료구조 분석 — Linked List·Bitmap·O(1) Scheduler
  13. 13Blocked List 자료구조 — Timeout 정렬·Delta List·Two-List Scheme
  14. 14Scheduler 알고리즘 구현 추적 — Next-Task Selection 로직
  15. 15Context Switch 원리 분석 — 레지스터 저장·복원·Stack Frame
  16. 16ARM Cortex-M Context Switch — PendSV·MSP/PSP 어셈블리 추적
  17. 17ARM Cortex-A Context Switch — Mode 전환·SVC·Banked Registers
  18. 18RISC-V Context Switch 분석 — ECALL·mret·CSR
  19. 19RTOS Tick과 타이머 — SysTick·Generic Timer·configTICK_RATE_HZ
  20. 20Tickless 모드 구현 — Idle Tick Suppression·Sleep·Wake 보정
  21. 21Scheduler Latency 측정 기법 — GPIO Toggle·DWT·ftrace·cyclictest
  22. 22RTOS Tracing과 Observability — Tracealyzer·SystemView·ITM/ETM
  23. 23Critical Section 구현 비교 — IRQ Disable·BASEPRI·Spinlock
  24. 24Semaphore 내부 구현 추적 — Counter·Wait List·ISR-Safe Variant
  25. 25Mutex 내부 구현 추적 — Owner·Recursion Count·ISR 금지
  26. 26Priority Inversion 문제 — Mars Pathfinder 사례·Bounded vs Unbounded
  27. 27Priority Inheritance 구현 — Inherit·Disinherit·Chain
  28. 28Priority Ceiling Protocol — Immediate vs Original 비교
  29. 29Queue 내부 구현 추적 — Ring Buffer·2 Wait Lists·Atomic Send/Receive
  30. 30Event Group 분석 — Bit Flag·AND/OR Wait·Sync Barrier
  31. 31ISR-Safe API 설계 — FromISR 패턴·Higher Priority Wake·Deferred Work
  32. 32Deadlock 분석 — 4 조건·Wait-for Graph·Lock Ordering·Timeout
  33. 33Stream Buffer와 Message Buffer — FreeRTOS 10의 Lock-Free SPSC
  34. 34실시간 메모리 요구사항 — Determinism·Fragmentation·WCET
  35. 35FreeRTOS Heap_1~5 분석 — 5종 Allocator의 구조와 트레이드오프
  36. 36TLSF Allocator 분석 — Two-Level Segregated Fit O(1)
  37. 37Static Allocation — 컴파일 타임으로 동적 위험 제거하기
  38. 38Memory Pool — Fixed-Size Block Allocator의 단순함과 강력함
  39. 39Stack Overflow 탐지 — Canary·MPU·Watermark 3중 방어
  40. 40SMP RTOS 설계 — Ready List·Affinity·IPI·Load Balancing
  41. 41SMP Spinlock 구현 — LDREX/STREX·Ticket Lock·MCS·WFE/SEV
  42. 42Software Timer 분석 — Daemon Task·자료구조·ISR-Safe API
  43. 43RTOS System Call — SVC·ECALL·User/Kernel 분리·FreeRTOS-MPU
  44. 44TrustZone과 TF-M — Secure/Non-Secure·NSC Veneer·PSA
  45. 45AMP와 OpenAMP — Heterogeneous SoC·RPMsg·remoteproc
  46. 46C++ in RTOS — RAII·std::thread·ETL·Coroutine
  47. 47FreeRTOS 소스 분석 — tasks.c·queue.c·port.c 추적
  48. 48Zephyr 커널 분석 — k_thread·k_sem·Driver Model
  49. 49RT-Thread 분석 — Object 모델·Components·Smart·Studio
  50. 50RTOS 포팅 가이드 — 새 아키텍처에 옮기는 절차
  51. 51RTOS 선택 가이드 — Footprint·License·Certification·Ecosystem
  52. 52Apache NuttX 분석 — POSIX·PX4·NASA Ingenuity
  53. 53PREEMPT_RT Linux — Mainline 6.12·Xenomai 4·EVL