18.6.1.2 `uORBDeviceNode::poll()` 내부의 `poll_wait()` 호출 및 세마포어(Semaphore) 슬립(Sleep) 상태 전이

18.6.1.2 uORBDeviceNode::poll() 내부의 poll_wait() 호출 및 세마포어(Semaphore) 슬립(Sleep) 상태 전이

사용자 스페이스에서 호출된 px4_poll()은 결국 OS 커널 레벨의 실제 poll() 시스템 콜로 이어지며, 커널 스케줄러는 배열에 등록된 각각의 디바이스 파일 디스크립터(FD)를 향해 VFS(가상 파일 시스템)를 거쳐 디바이스 드라이버 본연의 poll() 메서드를 호출한다.

즉, uORB 프레임워크에서는 cdev::CDev를 상속받아 등록된 uORBDeviceNode::poll(file_t *filp, pollfd *fds, bool setup) C++ 가상 함수가 그 종착지가 된다.

1. poll_wait(): 커널과의 연결 고리(Hook)

uORBDeviceNode::poll() 메서드는 이름 그대로 폴링 이벤트를 셋업(Setup)하거나 해제(Teardown)하는 두 가지 역할을 수행한다.

새로운 px4_poll() 대기가 시작되어 setup == true 파라미터와 함께 호출되었을 때, 이 함수의 가장 핵심적인 임무는 바로 커널에서 제공하는 poll_wait() API를 묶어두는(Hooking) 것이다.

// uORBDeviceNode::poll 내부 로직 (슈도코드)
int uORBDeviceNode::poll(file_t *filp, pollfd *fds, bool setup)
{
    if (setup) {
        // 1. 이미 데이터가 버퍼에 도착해 있다면 즉시 반환 (Sleep 방지)
        if (has_new_data(filp)) {
            fds->revents |= POLLIN;
            return OK;
        }

        // 2. 대기자 명단(Wait Queue)에 구독자의 pollfd를 등록
        poll_wait(filp, fds, true);
    } else {
        // Teardown: 대기자 명단에서 해제
        poll_wait(filp, fds, false);
    }
    return OK;
}

여기서 호출되는 poll_wait(filp, fds, true)는 해당 구독 내역(fds)을 uORB 디바이스 노드가 내부적으로 관리하는 폴링 대기자 명단(Poll Wait Queue / Semaphore Waiter List) 에 조용히 밀어 넣는 행위이다.

2. 세마포어(Semaphore)를 통한 스케줄러(Scheduler)의 Sleep 상태 전이

모든 낚싯대(FD 배열)에 대해 poll_wait() 등록이 완료되었음에도 불구하고 어디에서도 당장 읽을 데이터가 없다면, 커널 스케줄러는 드디어 최종 결단을 내린다. 해당 스레드가 사용하는 스택 메모리와 레지스터 컨텍스트를 보존한 채, 프로세스 상태를 ’실행 중(Running)’에서 ‘대기 중(Waiting)’ 으로 전이시킨다.

이때 하부 커널 구현체(NuttX 등)에서는 본질적으로 세마포어(Semaphore) 기반의 블로킹(Blocking) 메커니즘이 작동한다.
스레드는 내부적으로 카운트가 0인 세마포어를 상대로 sem_wait() 또는 nxsem_wait()를 호출하고 영면에 빠진다. 이 순간부터 해당 퍼블리셔 스레드는 CPU 스케줄러의 실행 큐(Run Queue)에서 완전히 배제되며, 타임아웃 시간이 만료되거나 외부에서 세마포어 카운트를 올려주기 전까지는 영원히 CPU 사이클을 단 1%도 소모하지 않는 완벽한 슬립(Sleep) 상태를 유지하게 된다.

이러한 세마포어 기반의 깊은 수면(Deep Sleep) 설계가 바로, 수백 개의 PX4 백그라운드 스레드가 존재함에도 실제 메인 비행 제어 루프의 하드 리얼타임 레이턴시(Latency)를 전혀 갉아먹지 않게 만드는 운영체제 스케줄러 차원의 궁극적인 최적화 마법이다.