### 0.0.1 poll() 시스템 콜 기반 비동기 직렬 포트 읽기 및 읽기 버퍼(Read Buffer) 오버플로우 방지 기법
운영체제의 스케줄러가 아무리 뛰어나도, 하드웨어 UART 핀으로 맹렬하게 쏟아져 들어오는 115200bps의 바이트 스트림을 소프트웨어가 제때 비워주지 않으면, 하드웨어 수신 버퍼(RX FIFO)나 커널의 소프트웨어 버퍼(Software Ring Buffer)는 순식간에 포화되어 오버플로우(Overflow)를 일으킨다. GPS 데이터의 중간 유실은 곧바로 체크섬(Checksum) 에러로 이어지고, 위치 갱신 주기를 파괴한다.
이를 방어하기 위해 PX4의 백엔드 파서는 POSIX 인터페이스의 꽃인 poll() 함수와 꼼꼼한 버퍼 관리 기법을 동원한다.
0.1 poll() 멀티플렉싱(Multiplexing) 시스템 콜의 작동 원리
리눅스나 NuttX와 같은 호환(POSIX-compliant) OS 환경에서, 수로(Channel)에 물(Data)이 들어왔는지 끊임없이 들여다보는 비지-웨이팅(Busy-waiting) 방식은 금기다. GPSProvider를 상속받은 호스트 코드(gps_thread_main 등)는 px4_poll() (또는 poll())을 호출하여 커널에게 파수꾼 역할을 위임한다.
- 블로킹 타이머(Blocking Timer):
poll()함수는 배열 형태인 파일 디스크립터 집합(struct pollfd)과 타임아웃(Timeout) 밀리초(ms) 인자를 받는다. GPS 드라이버는 직렬 포트fd를 모니터링 대상으로 지정하고,POLLIN(읽을 데이터 있음) 이벤트를 기다린다. - 타임아웃은 보통 GPS의 예상 갱신 주기(예: 10Hz면 100ms)보다 약간 길게(수십~수백 밀리초 여유) 설정된다. 이 기간 포트에 데이터가 전혀 들어오지 않는다면
poll()은0을 반환하여 통신 두절(Timeout) 상태임을 메인 로직에 알린다. - 만약 단 1바이트라도 수신 버퍼에 도착하면 하드웨어 인터럽트가 발생하고, 커널은 대기 중이던
poll()의 빗장을 풀어> 0값을 즉시 반환하며 깨워준다.
0.2 비동기 논블로킹(Non-blocking) 버퍼 긁어오기
poll()이 수문이 열렸음을 알리면, 드라이버는 지체 없이 쌓인 물을 모조리 빼내야 한다.
- 이때
read(fd, buffer, sizeof(buffer))시스템 콜이 호출된다. 여기서 중요한 점은, 직렬 포트가 O_NONBLOCK 플래그로 열려 있거나(또는poll직후라 방해받지 않는 구간에서) 실행되며, 커널 버퍼에 쌓여 있는 만큼의 바이트 뭉텅이(보통 한 번에 기십 바이트 배열)를 한 호흡에 모조리 사용자 버퍼(User-space array)로 퍼 올린다는 것이다. - 한 번 루프마다
read()의 반환값(실제 읽어 들인 바이트 수) 변수nread를 획득한다.
0.3 버퍼 오버플로우 보크(Balk) 및 1바이트 주입의 예술
이제 사용자 스코프의 임시 배열(uint8_t buf[GPS_MAX_PAYLOAD])에 직렬 포트로부터 건져낸 데이터 덩어리가 담겼다. 앞 절에서 배운 바와 같이, UbxGps 같은 하위 파서의 상태 머신은 바이트 덩어리를 소화하지 못하고 오직 1바이트(parse_char()) 단위로만 먹이를 받아먹는다.
- 반복 주입(Iterative Injection): 드라이버는
read()로 한꺼번에 끌어올린 배열 크기(nread)만큼for반복문을 맹렬하게 구동한다.for (int i = 0; i < nread; i++) { int ret = helper->receive(buf[i]); if (ret > 0) { // 메시지 완성, uORB 발행(Publish) 로직 수행 } }
2. **커널 버퍼 구출 작전(Rescue Operation)**:
이 "커널 뭉텅이 획득(`read`) $\rightarrow$ 사용자 배열 순회 $\rightarrow$ 1바이트씩 상태 머신(`receive`) 투입" 매커니즘은 매우 정교한 속도 조절 밸브 역할을 한다. 커널의 소프트웨어 링 버퍼(Ring Buffer) 사이즈는 통상 수백 바이트 정도로 매우 작다.
드라이버가 `read()`로 한 번에 버퍼를 비워주지 않고 `read(fd, &char, 1)` 식으로 루프를 돌면 커널에서 사용자 공간으로 넘어오는 시스템 콜 오버헤드 때문에 버퍼를 퍼내는 속도가 데이터 유입 속도를 따라가지 못해(Buffer Overrun) 앞부분이 덮어씌워지는 대참사가 발생한다.
3. **오버플로우 방지 결론**:
결국 덩어리로 퍼서(시스템 콜 1회 최소화) 배열에 임시 저장하고, 그 배열을 루프 돌려 C++ 메모리 파서(퍼포먼스 무제한)에 투입하는 방식은, **I/O 병목의 해소**와 **상태 머신 파싱의 정밀함**이라는 두 마리 토끼(오버플로우 방지와 메모리 절약)를 완벽하게 동시에 잡아내는 아키텍처적 절충안인 셈이다.