18.3.2.2 uORBDeviceNode 클래스의 cdev::CDev 상속 트리 및 파일 오퍼레이션(file_operations) 구조체 매핑
앞서 18.3.2.1절에서 토픽이 /obj 폴더 하위에 문자 디바이스 노드(Character Device Node)로 마운트된다는 사실을 다루었다. 하지만 운영체제 커널(NuttX 등)은 본질적으로 철저한 절차지향적 언어인 순수 C(C99)로 작성되어 있다. 반면, uORB 미들웨어를 관장하는 uORBDeviceNode는 객체 지향 언어인 C++ 클래스 인스턴스이다.
C 언어로 작성된 커널 가상 파일 시스템(VFS)이 특정한 노드에 접근하려 할 때, 도대체 어떻게 C++ 클래스의 멤버 함수(메서드)를 정확하게 찾아내어 호출할 수 있는 것일까? 이 절차적 세계와 객체 지향적 세계 사이의 거대한 심연을 연결해 주는 마법의 다리가 바로 cdev::CDev 상속 트리와 file_operations 구조체이다.
1. PX4의 하단 인프라: cdev::CDev 기초 클래스
PX4 아키텍트들은 하드웨어 드라이버나 가상 디바이스들을 구현할 때마다 VFS와의 연결고리를 반복해서 작성하는 수고를 덜기 위해, cdev::CDev (Character Device) 라는 추상화된 C++ 기본 클래스(Base Class)를 설계했다.
uORBDeviceNode 클래스의 정의부를 살펴보면, 다음과 같이 cdev::CDev를 부모 클래스로 상속(Inheritance)받고 있음을 알 수 있다.
// uORBDeviceNode.hpp 내부
class uORBDeviceNode : public cdev::CDev {
public:
virtual ssize_t read(file_t *filp, char *buffer, size_t buflen) override;
virtual ssize_t write(file_t *filp, const char *buffer, size_t buflen) override;
virtual int ioctl(file_t *filp, int cmd, unsigned long arg) override;
// ...
};
이 상속 구조의 핵심은 cdev::CDev가 선언해 둔 virtual 즉, 가상 함수들을 uORBDeviceNode가 자신의 특성에 맞게 재정의(Override)한다는 점이다.
2. file_operations 구조체: 함수 포인터 매핑표
유닉스/리눅스 및 NuttX의 커널 공간에서 모든 파일과 장치는 struct file_operations (보통 fops라 부른다) 라는 철저한 C 언어 명세서를 가지고 있어야만 VFS로 동작할 수 있다. 이 구조체 안에는 open, close, read, write, ioctl 시스템 콜이 불렸을 때 커널이 점프해야 할 ’함수 포인터(Function Pointer)’들이 빼곡히 적혀 있다.
cdev::CDev 클래스는 객체가 인스턴스화되고 register_driver()를 호출하는 순간, 이 file_operations C 구조체를 조립하여 커널의 VFS 트리 노드에 등록한다. 그리고 이 때 포인터의 목적지를 놀랍게도 자신의 C++ 가상 함수 래퍼(Wrapper) 들로 묶어둔다 (Binding).
// 커널 관점에서의 file_operations 구조체 형태 (NuttX)
static const struct file_operations g_cdev_fops = {
cdev_open, // 커널 `open()` -> C++ `CDev::open()` 가상 함수 호출로 라우팅
cdev_close, // 커널 `close()` -> C++ `CDev::close()` 가상 함수 호출로 라우팅
cdev_read, // 커널 `read()` -> C++ `CDev::read()` 가상 함수 호출로 라우팅
cdev_write, // 커널 `write()` -> C++ `CDev::write()` 가상 함수 호출로 라우팅
// ...
};
3. 객체 지향과 절차 지향의 경계 붕괴
이 정교한 상속 및 포인터 매핑 작업이 컴파일 타임에 완료되고 나면, 런타임에는 놀라운 일이 벌어진다.
사용자 공간(User Space)의 태스크가 /obj/sensor_gyro0 노드를 대상으로 read() 시스템 콜을 호출한다. 이 요청은 C 언어로 된 커널의 VFS 인터페이스를 타고 내려가 해당 노드에 등록된 file_operations 구조체의 cdev_read 포인터를 건드린다.
이 포인터는 C++의 다형성(Polymorphism) 테이블(vtable)을 거쳐 최종적으로 메모리 상에 존재하는 해당 uORBDeviceNode 객체의 read() 메서드 내부로 진입하게 된다.
결론적으로, 발행자나 구독자가 그저 순수한 파일 입출력을 시도하는 것만으로도, OS 커널은 아무것도 모른 채로 링 버퍼를 관리하는 고도의 C++ 객체 지향 로직을 완벽하게 톱니바퀴처럼 구동시켜 줄 수 있는 것이다. 이것이 uORB 미들웨어 통신이 극도의 경량성과 유연성을 동시에 가지는 비결이다.