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_cmdAPI를 통해 생성된다. 이때 개발자는 이 데몬이 평생 먹고살 **스택 크기(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 stop과 start를 몇 번 반복하는 것만으로 힙 메모리가 완전히 갉아먹혀(Memory Leak), 멀쩡하게 날아가던 드론이 갑자기 하드 폴트를 뿜고 곤두박질치게 된다. 데몬 컨텍스트는 느슨한 타이밍을 허락하는 대신, 이러한 철저한 메모리와 생명주기 관리 책임을 개발자에게 오롯이 지운다.