21.1.1.2.1. 비동기식 I/O 처리를 전담하는 백그라운드 데몬(Daemon)의 생명주기

21.1.1.2.1. 비동기식 I/O 처리를 전담하는 백그라운드 데몬(Daemon)의 생명주기

사용자 정의 애플리케이션을 기성 리눅스 서버에서 개발하던 습관대로 짜게 되면 가장 많이 만나게 되는 형태가 바로 ‘백그라운드 데몬(Background Daemon)’ 컨텍스트이다.
PX4 생태계에서 ’데몬’이란, 기체가 켜질 때 백그라운드로 스폰(Spawn)되어 무한 루프(while(!_task_should_exit))를 돌며 이벤트를 조용히 기다리다가, MAVLink 명령이나 센서 신호가 떨어지면 비동기적(Asynchronous)으로 깨어나 작업을 처리하는 독립 태스크(Task)를 일컫는다.

여러분만의 센서 로깅 모듈이나, 릴레이 스위치를 켜고 끄는 페이로드 트리거(Payload Trigger) 모듈을 설계한다면, 십중팔구 이 데몬 아키텍처를 선택하게 될 것이다. 이 데몬의 생명주기(Lifecycle)를 정확히 통제하지 못하면 쓰레드 좀비(Zombie Thread)가 되어 메모리 릭(Leak)을 유발하거나, 기체의 전원이 꺼질 때 SD 카드의 파일시스템을 깨뜨리는 원흉이 된다.

1. 탄생 (Instantiation & Spawning)

PX4의 데몬은 NSH(NuttShell) 창에서 사용자가 custom_app start를 타이핑하거나, 초기 부팅 스크립트(init.d/rc.mc_apps 등)에 의해 호출될 때 탄생한다.

  • 진입점(Entry Point): custom_app_main(int argc, char *argv[]) 함수가 프로세스의 시작점이다.
  • 태스크 생성(task_spawn): C++의 start() 메서드 내부에서, 데몬의 뇌 역할을 할 독립적인 쓰레드(혹은 NuttX Task)가 px4_task_spawn_cmd API를 통해 생성된다. 이때 개발자는 이 데몬이 평생 먹고살 **스택 크기(Stack Size, 예: 2000 Bytes)**와 **우선순위(Priority, 보통 SCHED_PRIORITY_DEFAULT)**를 OS 커널에 제출해야 한다.
  • 이름표 달기: 런타임에 이 데몬을 top 명령어로 추적할 수 있도록 커널 태스크 리스트에 고유한 이름(예: custom_app)을 등록한다.

2. 런루프와 비동기 I/O 대기 (Run-loop & Polling)

성공적으로 메모리 공간을 할당받고 환생한 데몬은 갓 태어난 직후부터 죽을 때까지 Run() 함수 안에서 무한 루프를 돌게 된다. 여기서 ’비동기식 I/O 처리’의 철학이 적용된다. 데몬은 루프를 숨 가쁘게 수십만 번 돌며 CPU를 태우는 폴링(Busy-waiting)을 절대 해서는 안 된다.

  • 잠들기 (px4_poll): uORB에서 메시지가 오거나(예: GPS 데이터), 소켓(Socket)에 새로운 이더넷 패킷이 들어올 때까지 데몬은 poll() 함수를 호출해 스스로를 딥슬립(Deep Sleep) 상태로 빠뜨린다. 이때 데몬의 CPU 점유율은 정확히 0%가 된다.
  • 비동기적 깨어남: 물리적인 인터럽트나 다른 코어 모듈의 통신 액션이 발생하면, OS 스케줄러가 잠들어있던 이 데몬의 뒷덜미를 잡아 깨운다(Wake-up).
  • 빠른 처리와 재수면: 데몬은 눈을 뜨자마자 I/O 버퍼를 비우거나 MAVLink 메시지를 포장해서 발송한 뒤, 주저하지 않고 다시 poll() 수면 상태로 들어간다.

이렇게 하면 데몬 모듈이 10개가 띄워져 있든 100개가 띄워져 있든 간에, 실제 CPU 타임을 소모하는 모듈은 ‘비동기 I/O 이벤트가 발생한’ 특정 데몬뿐이게 되므로 전체 비행 제어 루프의 프레임 레이트를 떨어뜨리지 않는다.

3. 우아한 소멸 (Graceful Shutdown)

펌웨어 업데이트를 위해 PX4가 재시동되거나, 사용자가 NSH에서 custom_app stop을 명시적으로 타이핑했을 때, 데몬은 절대 즉시 사살(kill -9)되어서는 안 된다.

  • 종료 시그널 전달: stop() 함수는 내부적으로 데몬의 _task_should_exit 플래그 변수를 true로 바꾼다.
  • 정리 정돈 (Cleanup): 무한 루프를 돌던 데몬은 다음 턴에 플랫 메모리 검사를 통해 자신이 ’죽어야 할 때’가 되었음을 깨닫는다. 그 즉시 열어두었던 SD 카드 파일 핸들(File Handle)을 닫고(close()), uORB 구독자(Subscriber) 노드를 해제하며, malloc으로 빌려왔던 동적 힙(Heap) 메모리를 OS에 전부 반납(free())한다.
  • 자살 (Thread Exit): 짐을 모두 챙긴 데몬은 마침내 무한 루프 블록을 빠져나와 함수의 끝(return 0;)에 도달함으로써 스스로를 안락사시킨다.

초보 개발자들은 이 ‘우아한 소멸’ 과정을 대충 뭉개버리곤 한다. 그렇게 되면 기체가 켜져 있는 동안 custom_app stopstart를 몇 번 반복하는 것만으로 힙 메모리가 완전히 갉아먹혀(Memory Leak), 멀쩡하게 날아가던 드론이 갑자기 하드 폴트를 뿜고 곤두박질치게 된다. 데몬 컨텍스트는 느슨한 타이밍을 허락하는 대신, 이러한 철저한 메모리와 생명주기 관리 책임을 개발자에게 오롯이 지운다.