21.1.2. 기존 NuttX Task 방식과 최신 Work Queue 방식의 스케줄링 패러다임 전환

21.1.2. 기존 NuttX Task 방식과 최신 Work Queue 방식의 스케줄링 패러다임 전환

PX4-Autopilot의 역사를 관통하는 가장 거대하고 뼈아픈 아키텍처 대공사(Refactoring)를 하나만 꼽으라면, 단연코 기존의 **‘독립 태스크(NuttX Task) 중심 생태계’**에서 현대적인 **‘협력적 워크 큐(Work Queue) 중심 생태계’**로의 스케줄링 패러다임 전환일 것이다.

초창기 PX4(버전 1.8 이전)는 운영체제 수업에서 배우는 가장 직관적이고 고전적인 방식을 채택했다. 새로운 센서 드라이버나 사용자 커스텀 모듈을 추가할 때마다 개발자는 task_spawn() 함수를 무지성으로 호출하여 OS 커널로부터 하나하나의 독립된 스레드(태스크)를 할당받았다.
이 방식은 코드를 짜기에는 매우 편했다. while(1) 루프를 하나 만들어 놓고, 그 안에 sleep(1) 같은 블로킹(Blocking) 함수를 남발하더라도, OS의 선점형 스케줄러(Preemptive Scheduler)가 알아서 문맥을 교환(Context Switching)해주었기 때문이다.

1. 태스크의 역습과 리소스 고갈

하지만 드론에 부착되는 물리적 센서의 개수가 폭발적으로 늘어나고(듀얼 GPS, 트리플 IMU 등), 비행 제어 연산의 주파수가 250Hz에서 1000Hz 이상으로 치솟으면서, 이 평화롭던 태스크 생태계는 픽스호크 보드의 하드웨어를 한계치까지 몰아넣고 결국 자멸하기 시작했다.

수십 개의 센서 드라이버가 제각각 자신만의 태스크를 띄워놓고 OS 스케줄러의 타임 슬라이스(Time Slice)를 쟁탈하기 위해 피 터지게 싸우는 꼴이 연출된 것이다. 스케줄러는 이 50개의 스레드를 1초에 1000번씩 공평하게 번갈아 실행시키기 위해, 끊임없이 A 스레드의 레지스터를 백업하고 B 스레드의 데이터를 캐시(Cache)에 복원하는 미친듯한 ’스위칭 오버헤드(Switching Overhead)’를 감당해야만 했다. 게다가 각 스레드마다 미리 쌓아두어야 하는 메모리 스택(Stack) 용량의 총합은 가뜩이나 부족한 아머지(SRAM)를 바닥내기에 충분했다.

2. 구원자: 워크 큐(Work Queue) 아키텍처의 도입

이 절망적인 메모리 고갈과 CPU 과부하를 타개하기 위해, PX4 메인테이너들은 리눅스 커널의 인터럽트 하프바텀(Half-bottom) 기술에서 영감을 얻은 ‘Work Queue’ 아키텍처를 전격 도입했다.

이는 “50개의 모듈이 모두가 주인공인 양 개별 스레드로 띄워질 필요가 없다“는 깨달음에서 출발했다.
최신 PX4에서는 시스템 부팅 시 용도와 우선순위에 따라 소수의 거대한 ‘스레드 풀(Thread Pool)’ 대장들(예: wq_rate_ctrl, wq_hp_default 등)만을 띄워놓는다. 그리고 여러분이 지은 개별 모듈(Work Item)들은 더 이상 자신만의 방(스레드)을 가지지 못하고, 이 대장 스레드가 관리하는 큐(Queue)에 종이쪽지(Callback Pointer)를 제출한 채 얌전히 순서를 기다리게 된다. 대장 스레드가 깨어나면 큐에 쌓인 종이쪽지들을 차례대로 초고속으로 훑고 나가며 함수를 순차적으로 실행해 주는 이치이다.

이러한 다대일(N:1) 협력형 멀티태스킹 구조로의 강제 이주 덕분에, PX4는 현재 동일한 하드웨어 위에서도 과거보다 5배 이상의 제어 주파수와 훨씬 무거운 매트릭스 연산을 흔들림 없이 감당해 내고 있다.

사용자 정의 앱(Custom App)을 개발하는 우리가 이 패러다임 전환을 뼛속까지 이해해야 하는 이유는 명확하다. 우리가 현대적인 형태의 커스텀 모듈을 설계하기 위해서는, 과거의 고루한 태스크 단위의 코딩 습관을 버리고 이 우아한 Work Queue의 협력형 콜백 모델에 완벽히 동화되어야 하기 때문이다. 이어지는 단원들에서는 태스크 방식이 구체적으로 하드웨어를 어떻게 괴롭혔는지(21.1.2.1 단원), 그리고 Work Queue가 이를 어떻게 구원했는지(21.1.2.2 단원) 그 기술적 원리를 상세히 분석한다.