21.6.4.1. `Run()` 메서드 내부의 파일 입출력 및 폴링(Polling) 제한

21.6.4.1. Run() 메서드 내부의 파일 입출력 및 폴링(Polling) 제한

Work Queue 버스를 올라탄 승객(모듈)이 지켜야 할 가장 엄격한 법률 제1조는 앞서 언급했듯 “스스로 스레드를 멈추지 마라(Non-blocking)” 이다.
하지만 많은 초보 시스템 프로그래머들이 자기도 모르는 사이에, 전혀 블로킹 함수라고 생각하지 못했던 평범한 C/C++ 표준 라이브러리 함수들을 Run() 안에 집어넣어 픽스호크를 공중 분해시키곤 한다.

그 숨겨진 암살자들의 정체는 바로 **파일 입출력(File I/O)**과 폴링 대기(Polling Wait) 코드들이다.

1. 암살자 1: 파일 시스템 I/O (printf, fprintf, open, write)

우리가 가장 흔히 저지르는 실수는 디버깅을 한답시고 Run() 함수 안에 printf("Current Speed: %f\n", speed); 를 찍어버리는 행위다.

일반적인 데스크톱(Linux/Windows) 환경에서 printf는 콘솔 버퍼에 문자를 휙 던지고 끝나는 가벼운 함수다. 하지만 RTOS(NuttX) 환경의 픽스호크에서는 이 한 줄이 지옥문을 연다.
printf나 파일 시스템에 접근하는 write() 함수는 궁극적으로 저수준 하드웨어 드라이버(UART 시리얼 포트나 SD 카드 컨트롤러)를 건드리게 된다. 이때 하드웨어가 아직 이전 메시지를 전송하느라 바쁜 상태(Busy)라면, 운영체제 커널은 자비 없이 내 모듈의 스레드를 멈춰(Sleep) 세우고 하드웨어가 비워질 때까지 무한정 기다리게 만든다.

SD 카드의 경우 파일 쓰기(FAT32) 과정에서 버퍼 플러시(Flush)가 발생하면 짧게는 수 밀리초(ms), 길게는 수십 밀리초까지 스레드가 굳어버린다. 만약 이 짓을 wq_rate_ctrl 스레드 안에서 벌였다면, 자이로 센서는 그 수십 밀리초 동안 단 한 번도 값을 읽어오지 못하고 기체는 제어력을 상실한다.

해결책: Run() 함수 안에서는 절대 표준 I/O 함수를 쓰지 마라. 대신 비동기 메시지 로깅 전용 시스템인 PX4_INFO 매크로나 uLog 시스템(비동기 링 버퍼 사용)을 활용해야 한다.

2. 암살자 2: 조건 무한 폴링 (while(안끝남) {})

센서에서 데이터를 읽어오거나 외부 칩셋(예: I2C 통신)과 대화할 때 흔히 짜는 코드 패턴이 있다.

// [X] 절대 금지: 동기식(Synchronous) 폴링 대기
void CustomApp::Run() {
    // I2C 칩에게 온도 측정 명령 전송
    request_temperature_to_i2c();
    
    // 온도가 다 측정될 때까지 멍하니 루프를 돌면서 기다림 (Polling)
    while (!is_temperature_ready()) { 
        // 그냥 무식하게 낭비되는 시간!
    }
    
    float temp = read_temperature();
}

칩셋이 온도를 측정하는 데 2ms가 걸린다고 가정하자. 이 2ms 동안 내 모듈이 while 문 안에서 CPU 클럭을 갉아먹으며 갇혀 있는 바람에, 같은 버스를 탄 다른 제어 모듈들은 실행 기회를 박탈당한다.

해결책 (비동기 상태 기계): 결코 기다리지 말고 즉각 반환(Return)하라.

// [O] 비동기(Asynchronous) 설계 + 상태 기계(State Machine)
void CustomApp::Run() {
    if (_state == STATE_REQUEST) {
        request_temperature_to_i2c();
        _state = STATE_WAITING; 
        return;  // 즉각 버스에서 하차! (다음 사이클로 미룸)
    }
    
    if (_state == STATE_WAITING) {
        if (is_temperature_ready()) { // 온도가 다 되었나? (논블로킹 체크)
            float temp = read_temperature();
            _state = STATE_IDLE; // 다시 초기 상태로
        }
        return; // 아직 안 되었어도 기다리지 않고 즉각 하차!
    }
}

이처럼 Run() 함수는 진입하자마자 자신이 해야 할 찰나의 조건 검사만 아주 짧게 수행하고, 미련 없이 즉시 return 하여 운전대를 넘겨주는 우아함을 갖춰야 한다.

이렇게 치명적인 I/O 대기와 폴링이 Work Queue 스레드 전체를 마비시키는 이 무서운 연쇄 작용(Deadlock 시나리오)에 대해 다음 장(21.6.4.1.1)에서 아키텍처 레벨로 더 깊이 분석해 보자.