21.4.2. 모듈 생명주기 관리 및 원자적(Atomic) 상태 제어

21.4.2. 모듈 생명주기 관리 및 원자적(Atomic) 상태 제어

ModuleBase라는 거인의 어깨 위에서 컴파일 타임의 강력한 보호를 받게 된 여러분의 커스텀 모듈은, 이제 운영체제(NuttX 또는 리눅스) 위에서 하나의 독립된 생명체(Thread/Task)로 태어나게 된다.

하지만 드론 펌웨어에서 모듈의 생사는 데스크톱 프로그램처럼 main 함수가 끝나면 조용히 종료되는 단순한 과정이 아니다. 수십 개의 모듈이 서로 거미줄처럼 데이터를 주고받는 PX4 환경에서는, 한 모듈이 제대로 준비되지 않은 채 데이터를 쏘거나 죽어버리면 전체 시스템이 연쇄 붕괴(Cascading Failure)를 일으킬 수 있다.

따라서 ModuleBase는 자식 클래스에게 엄격하게 통제된 ‘생명주기(Lifecycle)’ 관리 권한을 부여하며, 이 생명주기 상태는 단 한 치의 오차도 없이 원자적(Atomic)으로 제어되어야만 한다.

1. 모듈의 4단계 생명주기 (Lifecycle)

ModuleBase 시스템 안으로 편입된 모듈은 NSH 터미널 명령어를 통해 다음과 같은 4가지의 명확한 상태(State) 사이를 오가게 된다.

  1. 초기화 (Initialization):
    명령어: custom_app start
    메인 스레드가 new CustomApp()을 호출하여 객체 인스턴스를 힙(Heap) 메모리에 할당하고 시작 준비를 마친 단계. 아직 백그라운드 런루프는 돌기 전이다.
  2. 실행 중 (Running):
    모듈의 핵심인 Run() 메서드가 호출되어, 센서를 읽고 알고리즘을 연산하는 while(1) 무한 루프 속에 영구적으로 진입한 상태.
  3. 종료 요청 (Stop Requested):
    명령어: custom_app stop
    사용자나 시스템이 모듈을 죽이려 할 때 발생하는 과도기적 단계. PX4는 운영체제 레벨의 kill 명령어로 메모리를 강제 폭파하는 것을 극도로 혐오한다. 대신 _task_should_exit 같은 플래그(Flag) 변수를 true로 뒤집어, 모듈이 하던 연산을 안전하게 마무리하고 스스로 루프를 탈출하도록 신사적으로 유도한다.
  4. 종료 (Terminated):
    힙 메모리에서 객체가 delete 되어 완전히 소멸한 상태.

2. 글로벌 진실의 방: is_running()

모듈이 위의 4가지 상태 중 정확히 어디에 있는지를 PX4 시스템 전체가 공유하기 위해, ModuleBaseis_running()이라는 아주 중요한 헬퍼(Helper) 함수를 제공한다.

예를 들어, 사용자가 실수로 혹은 고의로 custom_app start를 터미널에 두 번 연속으로 타자 쳤다고 가정해 보자.
만약 생명주기 통제가 느슨하다면 메모리에는 두 개의 동일한 제어기가 동시에 켜져서 모터에 서로 다른 명령을 내리는 호러 영화가 연출될 것이다.

이를 막기 위해 NSH 진입점(task_spawn)에서는 반드시 다음과 같은 방어 코드가 먼저 실행된다.

int CustomApp::task_spawn(int argc, char *argv[])
{
    // 이미 백그라운드에서 실행 중인지 먼저 확인!
    if (is_running()) {
        PX4_WARN("already running");
        return 0; // 추가 스레드 생성을 즉각 거부
    }
    
    // 실행 중이 아니라면 비로소 스레드 생성 (task_id 할당)
    // ...
}

이 코드는 매우 직관적이고 완벽해 보인다. 하지만 여기에 **“두 명의 사용자가 (혹은 두 개의 스크립트가) 0.00001초의 오차도 없이 동시에 custom_app start를 쳤을 때”**라는 픽스호크 멀티스레딩의 악몽 조건이 추가되면 어떻게 될까?

이때 발생하는 무시무시한 레이스 컨디션(Race Condition)을 막기 위한 동기화(Synchronization)의 기술을 다음 단원(21.4.2.1)에서 아슬아슬하게 해부해 보자.