Rendezvous, FIFO

Seungyun Lee·2026년 2월 25일

RTOS

목록 보기
10/14

1. 스레드 동기화와 랑데부 (Rendezvous)

두 스레드(Thread 1, Thread 2)가 각자 일을 하다가 특정 지점에서 만나 동시에 다음 작업을 시작(Rendezvous, 랑데부)하도록 맞추는 기술입니다.

Task1의 행동: "나 도착했어!"(OS_Signal(&S1))라고 외치고, "Task2 올 때까지 기다릴게..."(OS_Wait(&S2))하며 멈춥니다.

Task2의 행동: "나 도착했어!"(OS_Signal(&S2))라고 외치고, "Task1 올 때까지 기다릴게..."(OS_Wait(&S1))하며 멈춥니다.

원리: 두 개의 세마포어(S1, S2)를 모두 0으로 초기화하여 사용합니다. 먼저 도착한 스레드는 상대방이 올 때까지 대기(Wait)하고, 도착하면 신호(Signal)를 보냅니다.

세마포어 상태에 따른 3가지 시나리오:

  • S1 = 0, S2 = 0: 두 스레드 모두 아직 도착하지 않았거나, 둘 다 완벽하게 동시에 도착해서 이미 통과한 상태입니다.
  • S1 = -1, S2 = +1: Thread 2가 먼저 도착해서 신호를 보냈고(+1), Thread 1을 기다리며 잠들어 있는(-1) 상태입니다.
  • S1 = +1, S2 = -1: Thread 1이 먼저 도착해서 신호를 보냈고(+1), Thread 2를 기다리며 잠들어 있는(-1) 상태입니다.

2. Producers & Consumers

생산자(데이터를 만드는 스레드)와 소비자(데이터를 처리하는 스레드)가 서로의 작업 속도에 얽매이지 않고 독립적으로 돌아가게(Decoupling) 만드는 구조입니다.

  • 해결책 (FIFO 버퍼): 중간에 FIFO(First-In-First-Out, 선입선출) 방식의 원형 큐(Circular Queue)를 둡니다.
  • 동작 방식: 생산자는 빈 공간이 있는 한 계속 Put(저장)을 하고, 소비자는 데이터가 있는 한 계속 Get(가져오기)을 합니다. 먼저 들어온 데이터가 먼저 나갑니다.

실생활 예시:

  • 인터넷 오디오 스트리밍: 다운로드 스레드가 FIFO에 데이터를 Put하고, 사운드 카드가 FIFO에서 Get하여 끊김 없이 노래를 재생합니다.
  • 프린터 출력: 사용자가 인쇄를 누르면 백그라운드에서 FIFO에 데이터를 넣고, 프린터가 여유가 될 때마다 가져가서 인쇄합니다. 덕분에 인쇄 중에도 컴퓨터를 멈춤 없이 계속 쓸 수 있습니다.

3. 정적(Static) vs 동적(Dynamic) 할당: RTOS의 철칙

FIFO 버퍼의 크기를 어떻게 정할 것인가에 대한 매우 중요한 RTOS 설계 철학입니다.

1. 동적 할당 (Dynamic):

프로그램 실행 중에 malloc과 free를 이용해 버퍼 크기를 맘대로 늘리고 줄이는 방식(Heap 메모리 사용)입니다.
치명적 단점: 메모리가 조각나는 힙 단편화(Heap Fragmentation)가 발생합니다. 이로 인해 메모리를 할당받는 시간이 들쭉날쭉해져서 실행 시간을 예측할 수 없게 됩니다. 또한, 다른 스레드가 메모리를 많이 쓰면 FIFO가 악영향을 받는 등 시스템이 불안정해집니다.

2. 정적 할당 (Static):

코드를 짤 때(Compile time) 버퍼의 최대 크기를 미리 고정해 두는 방식입니다.
RTOS의 선택: RTOS는 성능(Performance)보다 신뢰성과 예측 가능성(Deterministic behavior)이 훨씬 중요합니다. 따라서 이 수업에서는 절대 malloc과 free를 쓰지 않고, 오직 정적 할당(Static allocation)만 사용하여 FIFO를 구현합니다.

4. FIFO 구현과 블로킹(Blocking) 전략

다수의 생산자와 다수의 소비자가 하나의 FIFO를 공유할 때의 상황입니다.

결정성(Determinism): RTOS의 생명은 '언제나 똑같은 입력에 똑같은 시간에 똑같이 반응'하는 것입니다.

힙 단편화(Heap Fragmentation): malloc과 free로 메모리를 동적 할당하면, 메모리 공간이 이빨 빠진 것처럼 조각조각 납니다. 나중에 메모리를 찾을 때 시간이 얼마나 걸릴지 예측할 수 없게 됩니다.

결론: 성능(Performance)을 조금 포기하더라도 신뢰성(Reliability)을 위해, FIFO 버퍼는 무조건 크기가 고정된 배열형태의 정적 할당(Static Allocation)으로 만들어야 합니다.

결국 이번 자료의 핵심은 "예측 불가능한 동적 할당(malloc)을 버리고, 정적으로 할당된 FIFO와 블로킹 세마포어를 이용해 생산자와 소비자를 안전하게 연결하자"는 것입니다.

5. FIFO Implementation

꽉 찬 FIFO와 텅 빈 FIFO의 대처법 (Blocking)

이 시나리오는 생산자(데이터 넣는 놈)와 소비자(데이터 빼는 놈)가 모두 '메인 스레드'일 때를 가정합니다.

생산자 (Fifo_Put): FIFO가 꽉 찼을 때 어떻게 할 것인가?

  1. 버리고 에러 띄우기 (Discard)

  2. 빈자리가 날 때까지 대기 (Block) 👉 이 슬라이드에서 선택한 방법입니다. 데이터를 절대 잃어버리면 안 되기 때문입니다.

소비자 (Fifo_Get): FIFO가 비었을 때 어떻게 할 것인가?

  • 데이터가 들어올 때까지 무조건 대기 (Block) 합니다.

순서 보장: 먼저 들어간 데이터가 먼저 나옵니다 (Order-preserving).

ISR의 치명적 약점
슬라이드 중간에 있는 "cannot be used if the producer is an event thread or ISR" 이 문장이 교수가 파놓은 가장 강력한 함정입니다.

이유: 만약 데이터를 넣는 생산자가 타이머 인터럽트(ISR)나 이벤트 스레드라고 가정해 봅시다. FIFO가 꽉 찼다고 ISR 안에서 빈자리를 기다리며 OS_Wait(Block)을 걸어버리면 어떻게 될까요?

결과: OS의 심장인 인터럽트가 그 자리에 멈춰서 잠들어버리기 때문에, 스케줄러가 완전히 멎고 시스템 전체가 즉사(Deadlock)하게 됩니다.

실전 직관: 따라서 ISR이 생산자일 때는 FIFO가 꽉 찼다고 절대 Block 시키면 안 되고, 과감하게 데이터를 버리거나(Discard) 덮어씌워야 합니다. (아까 Lab 2 OS_MailBox_Send에서 lost++ 하셨던 바로 그 이유입니다!)

The two-pointer FIFO implementation

PutPt (생산자의 지시봉): "다음 물건을 내려놓을 '빈자리'의 주소"를 가리킵니다.
GetPt (소비자의 지시봉): "다음에 내가 가져갈 '물건'이 있는 주소"를 가리킵니다.

uint32_t volatile *Putet:  // put next
uint32_t volatile *Getft: // get next

void Fifo Put (uint32_t data){ // call by value
	*PutPt = data; // Put
	Putpt++;       // next
}

uint32_t Fifo Get (void) { uint32_t data;
	data = *Getft: // return by reference
	GetPt+t;       // next
	return data;   // true if success
}

코드 완벽 해독
1. Fifo_Put(uint32_t data) (물건 넣기)
*PutPt = data;: PutPt가 가리키는 텅 빈 메모리 공간에 새로운 데이터를 쏙 집어넣습니다.
PutPt++;: 자리를 채웠으니, 지시봉을 오른쪽(다음 빈자리)으로 한 칸 딱 이동시킵니다.

  1. Fifo_Get(void) (물건 빼기)
    data = *GetPt;: GetPt가 가리키고 있는 메모리 공간에서 가장 오래된 데이터를 쏙 빼옵니다.
    GetPt++;: 물건을 가져갔으니, 지시봉을 오른쪽(그다음 오래된 물건)으로 한 칸 딱 이동시킵니다.
    return data;: 빼온 데이터를 메인 프로그램에 전달해 줍니다.
profile
RTL, FPGA Engineer

0개의 댓글