19.8.3.3. 스레드 컨텍스트(Thread Context) 간의 데이터 경쟁(Data Race) 방지

19.8.3.3. 스레드 컨텍스트(Thread Context) 간의 데이터 경쟁(Data Race) 방지

19.8.3.1 단원의 타임스탬프 누락이나 19.8.3.2 단원의 타임아웃 0 버그가 터미널 에러나 CPU 다운이라는 비교적 명백한 징후(Symptom)를 토해내는 착한(?) 버그였다면, 지금 소개할 세 번째 버그는 멀티 코어 운영체제(RTOS)의 태생적 심연에서 기어나오는 가장 지능적이고 악랄한 살인마, **‘데이터 경쟁(Data Race)’**이다.

드론의 텔레메트리나 로그를 까보았을 때 이 버그의 현상은 아주 기괴하게 나타난다. 드론의 GPS 좌표가 99%는 정상적으로 들어오다가, 수 분에 한 번씩 수직이나 수평으로 수백 킬로미터(km)를 순간이동 하듯 값이 미친 듯이 튀어버리고(Spike), 그 즉시 원상 복구되는 현상이다. EKF 필터는 이 찰나의 스파이크 값을 진짜로 믿어버리고 모터 출력을 순간적으로 최대치로 뽑아내어 드론 스스로 프레임을 박살 내게 만든다.

이 현상의 99.9% 원인은 센서 하드웨어 고장이 아니다. 퍼블리셔가 데이터를 링 버퍼에 쓰고(Write) 있는 아주 찰나의 마이크로초 순간에, 전혀 다른 스레드의 구독자가 락(Lock)도 없이 그 메모리에 난입하여 반쯤 쓰인 텍스트 조각을 **읽어(Read)**가 버린, C++ 고유의 동기화 실패(Synchronization Failure) 참사이다.

1. 메모리 찢어짐(Tearing)의 물리학

sensor_gps_s라는 구조체 안에 위도(lat), 경도(lon), 고도(alt) 값을 넣는다고 쳐보자.
퍼블리셔 데몬 스레드가 VFS 링 버퍼에 데이터를 복사(memcpy)하는 행위는 CPU 클럭 물리적으로 절대 한 번의 틱(Atomic)에 일어나지 않는다. 위도 값을 먼저 덮어쓰고, 그다음 경도를 덮어쓰고, 마지막 고도를 덮어쓰기까지 치명적인 수십 나노초의 지연(Delay)이 발생한다.

[비극의 시나리오]

  1. **퍼블리셔(Thread A)**가 이전 데이터(서울 좌표)가 적혀있던 메모리에, 최신 데이터(부산 좌표)를 덮어쓰기 시작한다. 먼저 위도를 ’부산 위도’로 덮어썼다.
  2. 이 찰나의 순간! RTOS 스케줄러가 퍼블리셔를 일시 절전(Preemption) 시켜버리고, 하필이면 **구독자(Thread B)**를 깨워버린다.
  3. 구독자 스레드는 신이 나서 그 메모리 버퍼를 통째로 딥 카피해 가져간다.
  4. 결과물: 구독자가 들고 도망친 구조체 안에는 **‘부산 위도’**와 미처 지워지지 않은 **‘서울 경도’**가 프랑켄슈타인처럼 기괴하게 섞여 들어가 버렸다! 이 가상의 괴물 좌표는 지구상에 존재하지 않는 태평양 한가운데의 좌표를 가리킨다.

2. uORB의 락(Lock) 프리 아키텍처와 방어선

놀랍게도 PX4의 설계자들은 위와 같은 참사를 막겠다며 값비싼 Mutex(뮤텍스)나 세마포어(Semaphore)로 링 버퍼 전체를 콱 틀어막는 멍청한 짓을 하지 않았다. 뮤텍스로 블로킹(Blocking)을 시도하면 1000Hz로 도는 데몬들이 서로 락이 풀리길 기다리며 병목(Starvation)에 걸리기 때문이다.

대신 uORB는 내부적으로 ‘세상에서 가장 빠르고 우아한 락 프리(Lock-free) 버전 컨트롤’ 시스템을 탑재하고 있다. 개발자인 당신이 이 방패를 작동시키려면, 절대로 구조체의 메모리 주소 포인터를 직접 들고 가서 캐스팅하여 읽으려는(Raw Pointer Access) 악마의 유혹을 뿌리쳐야 한다.

// [최악의 안티 패턴: 데이터 레이스의 주범]
sensor_gps_s* raw_ptr = (sensor_gps_s*) _gps_sub.get_raw_pointer(); 
// 포인터로 원본 메모리에 직접 접근하여 값을 읽어오려 함. 언제 값이 반쯤 찢어질지 모른다!
double my_lat = raw_ptr->lat; 

uORB 버퍼의 무결성을 보장하는 유일하고도 절대적인 해독제는, 우리가 19.5 단원에서부터 지겹도록 호출해 온 copy() 혹은 orb_copy() 함수뿐이다.

// [완벽한 방어 패턴: orb_copy의 원자적(Atomic) 딥 카피]
sensor_gps_s safe_data;

// 이 copy() 함수 내부에 uORB의 락 프리 메모리 방어막이 쳐져 있다.
// 만약 퍼블리셔가 반쯤 쓰고 있는 중이면, copy()는 이전 버전의 온전한 데이터를 주거나
// 최신 쓰기가 완벽히 끝날 때까지 CPU 스핀(Spin) 회피 로직으로 데이터를 찢김 없이 안전하게 뽑아준다.
_gps_sub.copy(&safe_data); 

double my_lat = safe_data.lat; 

명심하라. 멀티스레드 코어 환경에서 uORB 파이프라인의 생사는 오직 이 copy 인터페이스 안에 봉인되어 있다. NSH 콘솔이나 C++ 구조체에서 void* 메모리 주소를 캐스팅하려는 순간, 당신은 OS의 보호막을 스스로 찢어발기고 자신의 기체를 태평양 한가운데로 공간 이동시키는 치명적인 악성 코드의 창조주가 될 뿐이다.