

그림을 보면 지시봉(PutPt와 GetPt)이 배열의 맨 아래(노란색 칸)까지 내려갔다가, 다음 번에는 배열의 맨 위(첫 번째 칸)로 휙! 하고 꺾여서 올라가는 화살표가 보이시죠?
이것이 바로 메모리 낭비와 폭발을 막아주는 환형 큐의 핵심입니다.
수정 사항 1 & 2: PutPt와 GetPt는 한 칸씩 전진하다가 배열의 끝(FIFOSIZE)에 도달하면, 다시 인덱스 0으로 돌아가야(Wrap-around) 합니다.
아까 우리가 배운 '택배함' 비유에서 세마포어 하나가 더 추가되었습니다. 여러 명의 택배 기사(Multiple Producers)와 여러 명의 고객(Multiple Consumers)이 동시에 접근할 때 발생하는 충돌(Race Condition)을 막기 위해서입니다.
#define FIFOSIZE 10 // Can be any size
uint32_t volatile *PutPt; // Pointer for next put
uint32_t volatile *GetPt; // Pointer for next get
uint32_t static Fifo[FIFOSIZE]; // Statically allocated array
int32_t CurrentSize; // Number of elements (0 means FIFO empty)
int32_t RoomLeft; // Number of empty spaces (0 means FIFO full)
int32_t FIFOmutex; // Binary semaphore for exclusive access
// ******** OS_Fifo_Init ************
// Initialize FIFO pointers and semaphores
void OS_Fifo_Init(void){
PutPt = GetPt = &Fifo[0]; // Initially empty, both point to start
OS_InitSemaphore(&CurrentSize, 0); // Initially 0 items
OS_InitSemaphore(&RoomLeft, FIFOSIZE); // Initially 10 empty spaces
OS_InitSemaphore(&FIFOmutex, 1); // Initially unlocked (1)
}
// ******** OS_Fifo_Put ************
// Put data into FIFO. Blocks if full.
void OS_Fifo_Put(uint32_t data){
OS_Wait(&RoomLeft); // 1. Wait for empty space
OS_Wait(&FIFOmutex); // 2. Lock FIFO (Critical Section)
*(PutPt) = data; // 3. Put data
PutPt++; // 4. Move pointer to next slot
// Wrap-around logic
if(PutPt == &Fifo[FIFOSIZE]){
PutPt = &Fifo[0]; // Wrap back to the beginning
}
OS_Signal(&FIFOmutex); // 5. Unlock FIFO
OS_Signal(&CurrentSize); // 6. Notify that a new item is available
}
// ******** OS_Fifo_Get ************
// Get data from FIFO. Blocks if empty.
uint32_t OS_Fifo_Get(void){
uint32_t data;
OS_Wait(&CurrentSize); // 1. Wait for available data
OS_Wait(&FIFOmutex); // 2. Lock FIFO (Critical Section)
data = *(GetPt); // 3. Get data
GetPt++; // 4. Move pointer to next slot
// Wrap-around logic
if(GetPt == &Fifo[FIFOSIZE]){
GetPt = &Fifo[0]; // Wrap back to the beginning
}
OS_Signal(&FIFOmutex); // 5. Unlock FIFO
OS_Signal(&RoomLeft); // 6. Notify that a space is freed up
return data;
}
이 코드는 실무에서도 그대로 쓰는 교과서적인 정답입니다. 교수님들이 이 문제를 낼 때는 학생이 코드의 모양만 외웠는지, 아니면 동작 원리를 진짜 이해했는지 걸러내기 위해 두 가지 함정을 팝니다.
OS_Wait(&RoomLeft); // 빈자리 확인
OS_Wait(&FIFOmutex); // 자물쇠부터 잠금
On average over the long term, what is the relationship between the
number of times Wait is called compared to the number of times
Signal is called?
On average over the long term, what is the relationship between the number of times Put is successfully called compared to the number of times Get is successfully called?

이 슬라이드의 존재 이유입니다. Producer Event Thread (ISR)는 타이머 인터럽트처럼 하드웨어에 의해 잠깐 불려 나와서 순식간에 일을 끝내고 사라져야 하는 아주 예민한 녀석입니다. 만약 FIFO가 꽉 찼다고 해서 ISR 안에서 빈자리가 날 때까지 기다리며 Block 되거나 Spin 해버리면? RTOS 스케줄러 전체가 그 자리에 멈춰서 완전히 뻗어버립니다 (System crash).
기다릴 수 없으니 현실적인 선택지는 하나뿐입니다. FIFO가 꽉 차서(CurrentSize == FIFOSIZE) 넣을 공간이 없다면, 새로 들어온 데이터는 과감하게 버려야 합니다. 대신 시스템이 얼마나 데이터를 잃어버렸는지 추적하기 위해 LostData 카운터를 증가시키고, Put 함수는 실패했다는 의미로 Error값(-1)을 리턴하고 곧바로 종료합니다.
이전 슬라이드에서 생산자가 여러 명일 때는 포인터(PutPt)가 꼬이는 Read-Modify-Write 충돌(Race Condition)을 막기 위해 FIFOmutex가 필요하다고 배웠었죠?
하지만 Event Thread 안에서는 Mutex를 획득하려고 기다릴(Wait/Block) 수 없습니다. 방패(Mutex)를 못 쓰니 충돌을 막을 유일한 방법은 "데이터를 집어넣는 Producer를 딱 1개(단일 인터럽트)로 엄격하게 제한"하는 것뿐입니다. (대신, 데이터를 빼가는 Main Thread Consumer는 여러 명이어도 상관없습니다.)
#define FIFOSIZE 10 // Can be any size
uint32_t volatile *PutPt; // Pointer for next put
uint32_t volatile *GetPt; // Pointer for next get
uint32_t static Fifo[FIFOSIZE];
int32_t CurrentSize; // 0 means FIFO empty
int32_t FIFOmutex; // Exclusive access to FIFO for consumers
uint32_t LostData; // Counter for discarded data
// ******** OS_Fifo_Init ************
void OS_Fifo_Init(void){
PutPt = GetPt = &Fifo[0]; // Initialize to Empty
OS_InitSemaphore(&CurrentSize, 0);
OS_InitSemaphore(&FIFOmutex, 1);
LostData = 0; // Initialize error counter
}
// ******** OS_FIFO_Put ************
// Called by Producer (Event Thread / ISR)
// Returns 0 on success, -1 if full (Non-blocking)
int OS_FIFO_Put(uint32_t data){
// 1. Check if full WITHOUT blocking
if(CurrentSize == FIFOSIZE){
LostData++; // Error: Data discarded
return -1;
}
// 2. Put data and advance pointer
*(PutPt) = data;
PutPt++;
// 3. Wrap-around logic
if(PutPt == &Fifo[FIFOSIZE]){
PutPt = &Fifo[0];
}
// 4. Notify consumer
OS_Signal(&CurrentSize);
return 0;
}
// ******** OS_FIFO_Get ************
// Called by Consumer (Main Thread)
// Returns data, blocks if empty
uint32_t OS_FIFO_Get(void){
uint32_t data;
OS_Wait(&CurrentSize); // Block if FIFO is empty
OS_Wait(&FIFOmutex); // Enter Critical Section for GetPt
// 1. Get data and advance pointer
data = *(GetPt);
GetPt++;
// 2. Wrap-around logic
if(GetPt == &Fifo[FIFOSIZE]){
GetPt = &Fifo[0];
}
OS_Signal(&FIFOmutex); // Exit Critical Section
return data;
}

3개에서 시작했던 세마포어가 2개를 거쳐 드디어 딱 1개로 줄었습니다. 시험 문제에서 "생산자가 1명(Event Thread)이고 소비자가 1명(Main Thread)일 때, 코드를 어떻게 가장 간결하게 최적화할 수 있는가?"라는 질문이 나오면 바로 이 코드가 정답입니다.
#define FIFOSIZE 10 // Size of the FIFO
uint32_t PutI; // Index for the next put
uint32_t GetI; // Index for the next get
uint32_t Fifo[FIFOSIZE]; // Statically allocated array
int32_t CurrentSize; // Number of elements (0 = empty, FIFOSIZE = full)
uint32_t LostData; // Counter for discarded data
// ******** OS_FIFO_Init ************
void OS_FIFO_Init(void){
PutI = GetI = 0; // Initialize indices to 0 (Empty)
OS_InitSemaphore(&CurrentSize, 0); // Initially empty
LostData = 0;
}
// ******** OS_FIFO_Put ************
// Called by exactly ONE Producer (Event Thread / ISR)
int OS_FIFO_Put(uint32_t data){
if(CurrentSize == FIFOSIZE){ // Check if full (Non-blocking)
LostData++; // Discard data
return -1; // Return error
} else {
Fifo[PutI] = data; // Put data at current index
PutI = (PutI + 1) % FIFOSIZE; // Wrap-around using modulo
OS_Signal(&CurrentSize); // Notify consumer
return 0; // Success
}
}
// ******** OS_FIFO_Get ************
// Called by exactly ONE Consumer (Main Thread)
uint32_t OS_FIFO_Get(void){
uint32_t data;
OS_Wait(&CurrentSize); // Block if empty
data = Fifo[GetI]; // Get data from current index
GetI = (GetI + 1) % FIFOSIZE; // Wrap-around using modulo
return data;
}
앞선 코드에서는 포인터 주소(PutPt++)를 쓴 다음, 끝에 도달했는지 확인하려고 if 문을 썼었죠?
이번엔 배열의 인덱스 번호(PutI, GetI)를 사용합니다. 그리고 프로그래밍의 마법인 나머지 연산자(Modulo, %)를 사용해 if 문을 완전히 없애버렸습니다.
작동 원리: PutI가 9일 때, (9 + 1) % 10을 계산하면 나머지가 0이 됩니다. if 문 없이도 완벽하게 맨 처음 위치(인덱스 0)로 랩어라운드(Wrap-around) 하는 가장 우아한 방법입니다.