18.4.3.2. 버퍼 오버플로우(Overflow) 발생 시 가장 오래된 데이터의 순환적 덮어쓰기(Circular Overwrite) 로직
PX4-Autopilot의 uORB 큐(Queue) 통신망 시스템은 다중 깊이(Depth)의 시계열 배열 단위 링 버퍼 매트릭스를 사용하여 락프리 데이터 이력을 우아하게 유지한다. 하지만 VFS 노드에 물리적으로 할당된 램(RAM) 공간 풀(Pool)은 유한하며, 지정된 큐 사이즈 인계를 초과하여 하드웨어 이벤트 발행 트랜잭션이 폭주(예: 물리 충돌 시 1초 만에 100개의 치명적 하드웨어 에러 메시지가 5칸짜리 로그 큐를 사정없이 덮치는 경우)하는 사태는 비행 시스템 중 언제든 예고 없이 발생할 수 있다. 본 절에서는 이러한 거시적 버퍼 오버플로우(Overflow) 임박 상황에서 커널 노드가 동적 메모리 확장(Dynamic Resizing)이나 스레드 블로킹(Blocking)을 폭력적으로 거부하고, 큐에 남아있던 가장 오래된 잔존 데이터부터 무자비하고 침묵적으로 덮어써 버리는 순환적 덮어쓰기(Circular Overwrite) 생존 아키텍처 규칙을 분석한다.
1. 시스템의 침묵적 포기: 큐 동적 리사이징 결여의 역설적 미학
일반적인 하이레벨 서버/애플리케이션 환경의 퓨어 소프트웨어 큐(예: C++ STL의 std::queue 또는 카프카 메시징 브로커) 기반 체계라면 트래픽 데이터가 쌓여 100% 한계에 도달했을 때, 동적 메모리 풀을 리로케이션(Relocation)하여 큐의 길이를 순간적으로 늘이거나, 혹은 공간이 I/O 스레드에 의해 비워질 때까지 생산자(Publisher) 스레드를 강제로 Wait 대기열에 세우는(Blocking/Push-back) 소프트웨어 예외 처리를 수행할 것이다.
그러나 하드 리얼타임 비행 제어기 코어 커널 관점에서 이러한 유연한 예외 처리 메커니즘은 곧 기체의 물리적 조향 불능과 추락(Crash) 죽음을 의미한다.
- 만약 MAVLink 텔레메트리 송신 스레드(우선순위 낮음)가 라디오 무선 랙(Lag)이 심하게 걸려 uORB 큐 버퍼 공간 데이터를 제때 빼가지 않는다(Consume)고 가정해 보자.
- 이 타이밍에 백그라운드 GPS나 최고 속도 센서 인터럽트 스레드가 큐에 화급히 에러 로그를 넣으려다 공간이 없어 소프트웨어 락(Lock)에 걸려 대기(Block)하게 된다면?
- 텔레메트리 스레드 따위의 하찮은 통신망 병목 현상이 기체의 코어 하드 리얼타임 믹서 및 비행 스레드의 메인 루프 시계마저 역으로 블로킹시켜 정지시킴으로써, 배보다 배꼽이 큰 우선순위 역전(Priority Inversion) 및 시스템 전역 데드락(Deadlock) 참사가 연쇄적으로 파멸적으로 파급 발생하게 된다.
따라서 PX4의 uORBDeviceNode 커널 파이프라인은 버퍼 메모리 공간이 끝끝내 100% 가득 차더라도, 절대로 바쁜 발행자를 돌려세우거나 뮤텍스 대기시키지 않는다. 오직 단 하나의 절대 시스템 규칙, **“지금 방금 센서가 생산한 따끈한 0.001초 포즈(Pose) 제어값이 과거 1초 전의 낡은 이력 데이터보다 시스템 물리 생존에 1,000배 더 생명 가치로 빚어진다”**는 철학에 깊이 입각하여 가장 차갑고 가차 없는 메모리 덮어쓰기(Overwrite)를 결행한다.
2. 원형 버퍼 생태계의 모듈로(Modulo) 순환 타격 메커니즘
이 순환적 덮어쓰기(Circular Overwrite)는 복잡한 if 검사나 커널 예외 처리 콜백 코드조차 전혀 동원하지 않고, 오직 단 한 줄의 모듈로 분기 없는 수학 연산의 순리로만 구현된다.
// 큐 사이즈가 10(_queue_size = 10)일 때의 퍼블리시 덮어쓰기 발생 원리
// 누적 카운터 _generation이 10이 되어 11번째 발행 명령이 떨어지는 역사적 진입 순간
unsigned head_index = _generation % _queue_size; // 10 % 10 = 0 (배열의 기점 인덱스로 회귀)
// 물리적 쓰기 로직은 조용히 찰나의 0번 인덱스 방에 침투하여
// 이 큐의 맨 처음에 쓰여졌던 가장 유서 깊은 산송장 데이터를 파괴하고 방을 빼앗는다.
memcpy(&_buffer[head_index * _o_size], new_data_ptr, _o_size);
이 압도적인 단 한 줄의 논리 오프셋 수학 로직에 의해, 고정된 큐의 길이(예: 10개)를 넘어서는 11번째 새로운 구조체 데이터 해일이 물리 도달하게 되면 논리적 포인터는 매우 자연스럽게 버퍼 배열의 인덱스 0으로 선회 랩어라운드(Wrap-around)하며 원을 그린다. 그리고 마치 애초에 아무 일도 없었다는 듯이 그곳에서 조용히 밀려 잠자고 있던 가장 오래된(Oldest) 데이터 위로 memcpy 탁본을 내리찍어 흔적도 없이 파괴해 버리는 것으로 한 사이클을 마무리한다.
3. 하위 수신자(Subscriber) 측면의 Drop 참탈 감지 메커니즘
이러한 침묵적 덮어쓰기가 수십, 수백 차례 빙글빙글 누적되어 진행되면, 하위 수신자 노드 스레드에 엄청난 재앙적 부조화 데이터 갭(Gap)이 감지된다.
수신 스레드가 꽤 오랜 Sleep 수면 끝에 컨텍스트 스위치되어 깨어나, 습관적인 orb_copy를 시도하며 자신이 쥐고 있던 개인용 책갈피(subscriber_generation)와 전역 공용 책갈피(node_generation)를 수학적으로 빼앗기 비교 연산해 보았더니, 그 인덱스 차이가 물리 큐 사이즈(queue_size) 볼륨보다 훨씬 참혹하게 벌어져 있는 현장에 다다른 것이다.
node_generation= 150 (VFS 커널 노드는 홀로 바쁘게 150번째 최상단 발행 중)subscriber_generation= 100 (느려터진 구독자는 아직 과거 100번대까지밖에 읽지 못했음)queue_size= 10 (버퍼 크기는 불과 10칸)
구독자는 101번부터 140번까지의 유실된 40개의 데이터를 더 이상 커널에 요구할 권조차 없다. 해당 데이터는 원형 연자방아 물리 논리에 의해 이미 노드 RAM 버퍼 안에서 수십 번이나 무자비하게 짓이겨져 완전히 삭제(Overwrite)되었기 때문이다. 이 아찔한 한계 상황에서 구독자 C++ 객체는 어쩔 수 없이 자신이 물리적으로 데이터를 유실(Drop)했음을 아프게 로그로 인정하고, 현재 큐 메모리에 유일하게 생존하여 아직 읽어볼 수 있는 최고령 생존자 인덱스(141번)부터 가장 최신 인덱스(150번) 데이터까지만 바구니에 주워 담아 다음 제어 프로세스를 힘겹게 속행한다.
결론적으로 커널 uORB 큐의 순환적 덮어쓰기(Ring Buffer Circular Overwrite) 데이터 모델은, 무르디무른 소프트웨어 레이어의 메모리 예외 처리가 유발할 수 있는 가장 끔찍한 실시간 블로킹 타이밍 침해 요소들을 극단적으로 통제 봉쇄하면서, 한정된 자원 풀 안에서 시스템 메인 최상위 진리의 신선도(Data Freshness)를 극한으로 사수해 내는 하드 리얼타임 운영체제의 가장 찬란하고 위대한 비정함 모델이다.