21.9.3.2.1. `VEHICLE_CMD_PREFLIGHT_REBOOT_SHUTDOWN` 커맨드 수신 시 힙 메모리 해제 및 진행 중인 워커 스레드를 강제 인터럽트하여 안전하게 종료하는 클린업(Cleanup) 시퀀스

21.9.3.2.1. VEHICLE_CMD_PREFLIGHT_REBOOT_SHUTDOWN 커맨드 수신 시 힙 메모리 해제 및 진행 중인 워커 스레드를 강제 인터럽트하여 안전하게 종료하는 클린업(Cleanup) 시퀀스

앞 장에서 우리는 셧다운 시그널(REBOOT_SHUTDOWN)을 감지했을 때 request_stop() 함수를 날려 스스로 제동장치를 당기는 모습을 보았다.
이 제동장치가 시스템의 근간까지 완벽하게 파고들어 메모리 한 톨 남기지 않고 모듈을 성불(Cleanup)시키는 C++ 관점에서의 파괴 메커니즘을 꼼꼼히 뜯어보자.

1. 방아쇠 당기기: 스케줄러 박살 내기

ModuleBase 내부의 request_stop() 함수가 불리는 순간, 모듈의 숨결이었던 무한 쳇바퀴(WorkQueue 스케줄)가 강제로 멈추게 된다.

// [부모 클래스 ModuleBase.hpp 내부 깊숙한 곳의 작동 원리]
void request_stop() {
    _task_should_exit.store(true); // "태스크 종료해라" 깃발을 올림
    ScheduleClear(); // 현재 예약되어 있던 Run() 함수의 재호출을 영구적으로 취소!
}

ScheduleClear()가 실행되면, 운영체제의 워크 큐(Work Queue) 관리자는 “아, 20ms 뒤에 PayloadAutoDrop::Run()을 다시 불러주기로 약속했었는데, 그 약속을 파기하겠다“며 캘린더에서 우리 모듈의 이름을 북북 지워버린다.

더 이상 Run() 함수가 불리지 않으므로 모듈의 생명(Execution Context)은 완전히 멈추게 되고, 이윽고 C++ 객체의 라이프사이클에 따라 소멸자(Destructor)가 경건하게 호출된다.

2. 장례식: 소멸자(Destructor)의 역할

PayloadAutoDrop 클래스의 소멸자 ~PayloadAutoDrop()는 죽음의 제단에서 마지막으로 주변 정리를 하는 막중한 임무를 띤다.

2.1 단계: 동적 할당 메모리(Heap) 해제

만약 모듈을 만들면서 힙(Heap) 메모리에 동적으로 배열이나 거대한 데이터 객체를 띄워놓았다면(new, malloc), 소멸자에서 반드시 delete를 통해 커널에게 반납해야 한다.
다행히 최신 PX4 아키텍처는 대부분 정적 햘당(Stack, BSS)이나 사전에 예약된 구조체 블록을 쓰기 때문에 힙 메모리 파편화를 유발하는 동적 할당 자체를 지양하고 있다.

2.2 단계: uORB 폴링(uORB Subscription) 연결고리 해제

uORB 구독권(Subscription)들은 C++ 생성 시점에 uORB 버스(게시판)에 “나 누구누군데, 데이터 들어오면 나한테 줘” 라고 계약서(Link)를 써둔 상태다.
이 링크를 끊지 않고 객체만 죽어버리면, uORB 버스는 이미 죽은 모듈의 메모리 주소로 계속해서 데이터를 밀어 넣으려다가 ’Segmentation Fault(접근 위반)’를 내며 비행기 전체를 추락시킨다.

하지만 여기서 우아한 C++ RAII(Resource Acquisition Is Initialization) 패턴이 빛을 발한다.

// [PayloadAutoDrop.hpp 클래스 선언부]
class PayloadAutoDrop : public ModuleBase<PayloadAutoDrop>, public ModuleParams {
    ...
private:
    // 이 변수들은 RAII 패턴의 혜택을 받는 아름다운 C++ 래퍼(Wrapper) 객체들이다.
    uORB::SubscriptionInterval _parameter_update_sub{ORB_ID(parameter_update), 1_s};
    uORB::Subscription _vehicle_status_sub{ORB_ID(vehicle_status)};
    uORB::Subscription _vehicle_command_sub{ORB_ID(vehicle_command)};
    ...
};

우리가 uORB::Subscription 클래스 객체를 사용해 토픽을 열었다면, 우리가 손수 unsubscribe() 같은 함수를 부르지 않더라도 셧다운 시 소멸자가 호출되면서 내부적으로 모든 uORB 파이프라인의 계약을 완벽히 끊어버린다.

3. 최종 해산

소멸자가 모두 끝나는 그 순간, payload_autodrop 프로세스는 RAM에서 먼지처럼 흩어지게 되며, NuttX 운영체제는 텅 빈 메모리를 징발하여 다음 비행(Reboot)을 위한 깨끗한 도화지를 준비하게 된다.

이것이 클래스 1 항공 소프트웨어가 가져야 할 ’생로병사(Birth to Death)’의 완벽한 굴레다.


이로써 기초 모듈의 생성부터 FSM 아키텍처링, 파라미터 튜닝, 서보 모터 구동, uORB 구조체 최적화, 그리고 펌웨어 데몬화(Daemonization)부터 셧다운(Shutdown) 안정성까지 **PX4 커스텀 모듈 개발의 ‘A to Z’**를 단 하나도 빠짐없이 모두 관통해 냈다.

하지만 코드가 눈에 보이지 않게 돌아간다고 끝은 아니다. “내 코드가 루프 한 바퀴를 도는 데 과연 정확히 몇 마이크로초(us) 가 걸리는가?”
이제 조종사가 아닌 병리학자의 시선으로 펌웨어 깊은 안쪽을 현미경으로 들여다볼 시간이다. 마지막 챕터, ’커스텀 모듈의 딥(Deep) 성능 분석과 GDB 트러블슈팅(21.10)’의 세계로 진입하자.