21.6.1. px4::WorkItem 상속 구조와 내부 데이터 추상화
과거의 태스크(Task) 기반 시스템에서는 모듈이 main() 함수 안에서 거대한 while(1) 무한 루프를 돌며 스스로 자신의 스케줄을 챙겨야 했다.
하지만 새롭게 도입된 Work Queue 체제에서는, 모듈이 스스로 스레드 루프를 돌리는 주도권을 포기하고 수동적인 **‘작업 아이템(Work Item)’**으로 전락(?)해야 한다.
이 수동적인 작업 보따리가 되기 위해, 우리의 CustomApp 클래스는 드디어 3번째이자 마지막 부모 클래스인 **px4::WorkItem**의 피를 수혈받게 된다.
1. 궁극의 3중 상속 (Multiple Inheritance)
현대 PX4 모듈의 표준 헤더 파일 선언부 원형은 경악스럽게도 아래와 같은 3중 다중 상속의 위용을 뽐낸다.
#include <px4_platform_common/module.h>
#include <px4_platform_common/module_params.h>
#include <px4_platform_common/px4_work_queue/ScheduledWorkItem.hpp>
class CustomApp :
public ModuleBase<CustomApp>, // 1. 생명주기 및 NSH 커맨드 라우팅
public ModuleParams, // 2. 글로벌 파라미터 동적 바인딩
public px4::ScheduledWorkItem // 3. Work Queue용 작업 보따리
{
public:
CustomApp();
// 이젠 무한 루프가 아니라, 버스가 호출해 줄 때마다
// "딱 1 사이클만" 실행되고 즉시 종료(return)되어야 하는 함수다.
void Run() override;
};
여기서 ScheduledWorkItem은 내부적으로 WorkItem을 상속받은 좀 더 스마트한 자식 클래스이다. (지정된 타이머 시간에 알아서 큐에 들어가는 기능이 추가됨)
이 3번째 부모를 상속받는 순간, 여러분의 모듈은 더 이상 독립적인 프로세스가 아니라, 거대한 스레드 버스(예: wq:rate_ctrl)에 올라탈 준비가 끝난 데이터 구조체가 된다.
2. WorkItem 객체의 추상화: 함수에서 객체로
원래 운영체제(커널)의 큐(Queue) 시스템은 메모리를 할당받은 순수한 C 언어 ’함수 포인터’만을 받아먹고 실행시키는 무식한 구조를 가지고 있다. 커널 입장에서는 C++ 객체(Object)의 메서드(CustomApp::Run()) 같은 고상한 개념을 이해하지 못한다.
px4::WorkItem 부모 클래스는 이 **“C 커널의 거칠고 딱딱한 함수 큐 시스템”**과 “우아한 C++ 객체 지향 메서드(Run())” 사이의 마찰을 없애주는 완충재(Abstraction) 역할을 한다.
WorkItem 클래스 내부는 대략 이렇게 생겼다.
// px4::WorkItem 내부 개념도
class WorkItem {
private:
work_s _work{}; // NuttX 커널이 이해할 수 있는 순수한 C 구조체 덩어리
protected:
// C++ 자식 클래스가 오버라이드해야 할 순수 가상 함수
virtual void Run() = 0;
};
우리가 CustomApp 모듈의 생성자를 호출하는 그 극초기 순간에, 저 WorkItem의 뱃속에 들어있는 깡통 _work 구조체는 커널과 비밀스럽게 교신하여 C++ Run() 메서드의 실행 포인터를 자신의 몸체에 꼼꼼히 묶어버린다.
이 놀라운 바인딩 메커니즘 덕분에, 커널 스케줄러는 그저 C 구조체를 큐에서 꺼내 무심하게 실행할 뿐이지만, 실제로는 내 C++ 객체의 Run() 메서드가 메모리 제약 없이 아름답게 터져 나오는 마법이 완성된다.
이 소름 돋는 커널 큐 연동과 work_s 구조체의 바인딩 원리를 다음 장(21.6.1.1)에서 메스(Scalpel)를 들고 해부해 보자.